ghost-bridge 1.0.1 → 1.1.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
@@ -94,7 +94,9 @@ Typical prompts:
94
94
  | `capture_screenshot` | Visual inspection and UI debugging |
95
95
  | `get_page_content` | Text, HTML, and structured DOM extraction |
96
96
  | `get_interactive_snapshot` | Find clickable and editable elements |
97
- | `dispatch_action` | Click, fill, press, scroll, hover, select |
97
+ | `dispatch_action` | Click, fill, press, scroll, hover, or select; supports selector-based and batched actions |
98
+ | `eval_script` | Execute JavaScript, wait for returned promises, and cap arbitrary output |
99
+ | `page_request` | Send an authenticated page-context request and wait for the response in one call |
98
100
  | `bind_tab` | Bind a Chrome tab as a named target such as `cases` or `app` |
99
101
  | `unbind_tab` | Remove a named target binding |
100
102
  | `list_targets` | Show named targets and their per-tab session status |
@@ -113,11 +115,36 @@ Typical prompts:
113
115
 
114
116
  Recommended flow:
115
117
 
116
- 1. Start with `inspect_page`
118
+ 1. Start with `inspect_page`; its compact response includes a small set of actionable refs
117
119
  2. Use `capture_screenshot` for visual issues
118
120
  Default is optimized for transfer with JPEG; switch to `png` for pixel-level checks
119
121
  3. Use `get_page_content` for DOM or text extraction
120
- 4. Use `get_interactive_snapshot` before `dispatch_action`
122
+ 4. Use `get_interactive_snapshot` only when the refs returned by `inspect_page` are insufficient
123
+ 5. Put consecutive fills/clicks into one `dispatch_action.actions` call and request `snapshotAfter` when the next page state is needed
124
+
125
+ Round-trip-efficient examples:
126
+
127
+ ```json
128
+ {
129
+ "target": "app",
130
+ "actions": [
131
+ { "selector": "input[name=email]", "action": "fill", "value": "user@example.com" },
132
+ { "selector": "input[name=password]", "action": "fill", "value": "secret" },
133
+ { "selector": "button[type=submit]", "action": "click", "waitMs": 1200 }
134
+ ],
135
+ "snapshotAfter": true
136
+ }
137
+ ```
138
+
139
+ For API calls that need the page's login state, prefer `page_request`. If custom asynchronous JavaScript is still needed, return the promise from `eval_script` instead of storing a result on `window` and polling it in another tool call:
140
+
141
+ ```javascript
142
+ (async () => {
143
+ const response = await fetch('/api/items', { credentials: 'include' })
144
+ const data = await response.json()
145
+ return data.items.map(({ id, name }) => ({ id, name }))
146
+ })()
147
+ ```
121
148
 
122
149
  Notes:
123
150
 
package/dist/cli.js CHANGED
@@ -5451,6 +5451,13 @@ var require_lib = __commonJS({
5451
5451
  });
5452
5452
 
5453
5453
  // lib/utils.js
5454
+ var utils_exports = {};
5455
+ __export(utils_exports, {
5456
+ getExtensionPath: () => getExtensionPath,
5457
+ getServerCommandConfig: () => getServerCommandConfig,
5458
+ getServerPath: () => getServerPath,
5459
+ getUserExtensionDir: () => getUserExtensionDir
5460
+ });
5454
5461
  import path from "path";
5455
5462
  import os2 from "os";
5456
5463
  import { fileURLToPath } from "url";
@@ -6011,6 +6018,8 @@ import { fileURLToPath as fileURLToPath2 } from "url";
6011
6018
  import path5 from "path";
6012
6019
  import fs5 from "fs";
6013
6020
  import os7 from "os";
6021
+ import net from "net";
6022
+ import { spawn } from "child_process";
6014
6023
  var __filename = fileURLToPath2(import.meta.url);
6015
6024
  var __dirname = path5.dirname(__filename);
6016
6025
  var packageJsonParams = JSON.parse(
@@ -6036,22 +6045,54 @@ program2.command("extension").description("Show the path to the Chrome extension
6036
6045
  process.exit(1);
6037
6046
  }
6038
6047
  });
6039
- program2.command("start").description("Start the Ghost Bridge MCP server directly").action(async () => {
6048
+ function waitForTcp(port, timeoutMs = 15e3) {
6049
+ return new Promise((resolve) => {
6050
+ const deadline = Date.now() + timeoutMs;
6051
+ const tryOnce = () => {
6052
+ const sock = net.connect(port, "127.0.0.1");
6053
+ sock.once("connect", () => {
6054
+ sock.destroy();
6055
+ resolve(true);
6056
+ });
6057
+ sock.once("error", () => {
6058
+ sock.destroy();
6059
+ if (Date.now() > deadline) resolve(false);
6060
+ else setTimeout(tryOnce, 300);
6061
+ });
6062
+ };
6063
+ tryOnce();
6064
+ });
6065
+ }
6066
+ program2.command("start").description("Start the resident ghost-bridge daemon (idempotent)").action(async () => {
6040
6067
  try {
6041
- console.log(source_default.blue("Starting Ghost Bridge Server..."));
6042
- const serverPath = path5.join(__dirname, "../src/server.js");
6043
- if (fs5.existsSync(serverPath)) {
6044
- await import(serverPath);
6045
- } else {
6046
- const rootServerPath = path5.join(__dirname, "../server.js");
6047
- if (fs5.existsSync(rootServerPath)) {
6048
- await import(rootServerPath);
6049
- } else {
6050
- throw new Error("Could not find server.js");
6051
- }
6068
+ const { getServerPath: getServerPath2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
6069
+ const serverPath = getServerPath2();
6070
+ if (!fs5.existsSync(serverPath)) {
6071
+ throw new Error(`\u672A\u627E\u5230 server: ${serverPath}`);
6072
+ }
6073
+ const entry = readPortInfoFile();
6074
+ if (entry && isProcessAlive(entry.pid)) {
6075
+ console.log(source_default.green(`\u5E38\u9A7B\u670D\u52A1\u5DF2\u5728\u8FD0\u884C (PID: ${entry.pid}, \u7AEF\u53E3: ${entry.port})`));
6076
+ return;
6077
+ }
6078
+ console.log(source_default.blue(`\u6B63\u5728\u542F\u52A8\u5E38\u9A7B\u670D\u52A1 (${serverPath})...`));
6079
+ const child = spawn(process.execPath, [serverPath], {
6080
+ detached: true,
6081
+ stdio: "ignore",
6082
+ env: { ...process.env, GHOST_BRIDGE_DAEMON: "1" }
6083
+ });
6084
+ child.unref();
6085
+ const port = Number(process.env.GHOST_BRIDGE_PORT || 33333);
6086
+ const ok = await waitForTcp(port);
6087
+ if (!ok) {
6088
+ console.error(source_default.red(`\u670D\u52A1\u672A\u80FD\u5728\u9884\u671F\u65F6\u95F4\u5185\u5728\u7AEF\u53E3 ${port} \u5C31\u7EEA\uFF0C\u53EF\u67E5\u770B ~/.ghost-bridge/daemon.log`));
6089
+ process.exit(1);
6052
6090
  }
6091
+ const fresh = readPortInfoFile();
6092
+ console.log(source_default.green(`\u2705 ghost-bridge \u5E38\u9A7B\u670D\u52A1\u5DF2\u542F\u52A8 (PID: ${fresh?.pid ?? "-"}, \u7AEF\u53E3: ${port})`));
6093
+ console.log(source_default.dim("\u65E5\u5FD7: ~/.ghost-bridge/daemon.log | \u505C\u6B62: ghost-bridge stop"));
6053
6094
  } catch (error) {
6054
- console.error(source_default.red("Error starting server:"), error);
6095
+ console.error(source_default.red("Error starting daemon:"), error);
6055
6096
  process.exit(1);
6056
6097
  }
6057
6098
  });
package/dist/server.js CHANGED
@@ -28541,6 +28541,8 @@ var BASE_PORT = Number(process.env.GHOST_BRIDGE_PORT || 33333);
28541
28541
  var DEFAULT_WS_TOKEN = "ghost-bridge-local";
28542
28542
  var WS_TOKEN = process.env.GHOST_BRIDGE_TOKEN || DEFAULT_WS_TOKEN;
28543
28543
  var RESPONSE_TIMEOUT = 8e3;
28544
+ var DEFAULT_EVAL_OUTPUT_LENGTH = 8e3;
28545
+ var MAX_EVAL_OUTPUT_LENGTH = 5e4;
28544
28546
  var PORT_INFO_FILE = process.env.GHOST_BRIDGE_PORT_INFO || path2.join(os.tmpdir(), "ghost-bridge-port.json");
28545
28547
  var SERVER_STARTED_AT = (/* @__PURE__ */ new Date()).toISOString();
28546
28548
  var SERVER_ENTRY_PATH = fileURLToPath2(import.meta.url);
@@ -29150,6 +29152,32 @@ function compact(value) {
29150
29152
  function out(data) {
29151
29153
  return jsonText(compact(data));
29152
29154
  }
29155
+ function clampNumber(value, fallback, min, max) {
29156
+ const number3 = Number(value);
29157
+ if (!Number.isFinite(number3)) return fallback;
29158
+ return Math.min(max, Math.max(min, Math.round(number3)));
29159
+ }
29160
+ function boundedOut(data, maxLength = DEFAULT_EVAL_OUTPUT_LENGTH) {
29161
+ const text = out(data) ?? "undefined";
29162
+ const limit = clampNumber(maxLength, DEFAULT_EVAL_OUTPUT_LENGTH, 200, MAX_EVAL_OUTPUT_LENGTH);
29163
+ if (text.length <= limit) return text;
29164
+ const markerBudget = 100;
29165
+ const headLength = Math.max(100, Math.floor((limit - markerBudget) * 0.8));
29166
+ const tailLength = Math.max(50, limit - markerBudget - headLength);
29167
+ const omitted = Math.max(0, text.length - headLength - tailLength);
29168
+ return out({
29169
+ truncated: true,
29170
+ originalLength: text.length,
29171
+ content: `${text.slice(0, headLength)}
29172
+ ... [\u5DF2\u7701\u7565 ${omitted} \u4E2A\u5B57\u7B26] ...
29173
+ ${text.slice(-tailLength)}`,
29174
+ hint: "\u9700\u8981\u66F4\u591A\u7ED3\u679C\u65F6\u8BF7\u6536\u7A84\u8FD4\u56DE\u5B57\u6BB5\uFF1B\u4E0D\u8981\u901A\u8FC7\u8FDE\u7EED\u8F6E\u8BE2\u5206\u7247\u83B7\u53D6\u5927\u5BF9\u8C61"
29175
+ });
29176
+ }
29177
+ function truncateUrl(url2, maxLen = 200) {
29178
+ if (typeof url2 !== "string" || url2.length <= maxLen) return url2;
29179
+ return url2.slice(0, maxLen) + "\u2026";
29180
+ }
29153
29181
  function shrinkListTabs(res, { fullUrl } = {}) {
29154
29182
  if (!res || !Array.isArray(res.tabs)) return res;
29155
29183
  const trimTab = (t) => {
@@ -29230,7 +29258,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
29230
29258
  tools: [
29231
29259
  {
29232
29260
  name: "inspect_page",
29233
- description: "\u9875\u9762\u5206\u6790\u5165\u53E3\uFF1A\u8FD4\u56DE\u5143\u6570\u636E\u3001\u7ED3\u6784\u8BA1\u6570\u4E0E\u53EF\u4EA4\u4E92\u5143\u7D20\u6570\u91CF\uFF08\u7D27\u51D1\u6A21\u5F0F\uFF09\u3002\u9700\u8981\u5B8C\u6574 page/interactive \u6570\u636E\u4F20 detail:true\u3002",
29261
+ description: "\u9875\u9762\u5206\u6790\u5165\u53E3\uFF1A\u4E00\u6B21\u8FD4\u56DE\u5143\u6570\u636E\u3001\u7ED3\u6784\u8BA1\u6570\u548C\u5C11\u91CF\u53EF\u76F4\u63A5\u64CD\u4F5C\u7684\u5143\u7D20 ref\uFF0C\u901A\u5E38\u65E0\u9700\u518D\u8C03\u7528 get_interactive_snapshot\u3002detail:true \u8FD4\u56DE\u5B8C\u6574\u7ED3\u6784\u3002",
29234
29262
  inputSchema: {
29235
29263
  type: "object",
29236
29264
  properties: {
@@ -29245,7 +29273,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
29245
29273
  },
29246
29274
  maxElements: {
29247
29275
  type: "number",
29248
- description: "detail \u6A21\u5F0F\u4E0B\u53EF\u4EA4\u4E92\u5143\u7D20\u4E0A\u9650\uFF0C\u9ED8\u8BA4 30"
29276
+ description: "\u8FD4\u56DE\u53EF\u4EA4\u4E92\u5143\u7D20\u4E0A\u9650\uFF0C\u9ED8\u8BA4 20"
29277
+ },
29278
+ includeElements: {
29279
+ type: "boolean",
29280
+ description: "\u7D27\u51D1\u6A21\u5F0F\u662F\u5426\u9644\u5E26\u53EF\u64CD\u4F5C\u5143\u7D20\uFF0C\u9ED8\u8BA4 true"
29249
29281
  },
29250
29282
  detail: {
29251
29283
  type: "boolean",
@@ -29395,13 +29427,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
29395
29427
  },
29396
29428
  {
29397
29429
  name: "eval_script",
29398
- description: "\u5728\u76EE\u6807\u9875\u6267\u884C\u53EA\u8BFB JS \u8868\u8FBE\u5F0F\uFF08\u8C28\u614E\u4F7F\u7528\uFF09\u3002",
29430
+ description: "\u5728\u76EE\u6807\u9875\u6267\u884C JS\u3002\u9AD8\u6210\u672C\u5DE5\u5177\uFF1A\u540C\u4E00\u76EE\u6807\u7684\u8BFB\u53D6/\u64CD\u4F5C\u5E94\u5408\u5E76\u8FDB\u4E00\u6B21 code\uFF1B\u5F02\u6B65\u4EE3\u7801\u76F4\u63A5\u8FD4\u56DE Promise\uFF0C\u672C\u5DE5\u5177\u9ED8\u8BA4\u7B49\u5F85\u5B8C\u6210\uFF0C\u7981\u6B62\u5199 window \u4E34\u65F6\u53D8\u91CF\u540E\u518D\u6B21\u8F6E\u8BE2\u3002\u7ED3\u679C\u9ED8\u8BA4\u6700\u591A 8000 \u5B57\u7B26\u3002",
29399
29431
  inputSchema: {
29400
29432
  type: "object",
29401
- properties: { target: TARGET_ARG, code: { type: "string" } },
29433
+ properties: {
29434
+ target: TARGET_ARG,
29435
+ code: { type: "string", description: "JS \u8868\u8FBE\u5F0F\uFF1B\u5F02\u6B65\u793A\u4F8B\uFF1A(async()=>await fetch(...).then(r=>r.json()))()" },
29436
+ awaitPromise: { type: "boolean", description: "\u7B49\u5F85 Promise \u5B8C\u6210\uFF0C\u9ED8\u8BA4 true" },
29437
+ timeoutMs: { type: "number", description: "Promise \u7B49\u5F85\u4E0A\u9650\uFF0C\u9ED8\u8BA4 10000\uFF0C\u6700\u5927 30000" },
29438
+ maxOutputLength: { type: "number", description: "\u5E8F\u5217\u5316\u7ED3\u679C\u5B57\u7B26\u4E0A\u9650\uFF0C\u9ED8\u8BA4 8000\uFF0C\u6700\u5927 50000" }
29439
+ },
29402
29440
  required: ["code"]
29403
29441
  }
29404
29442
  },
29443
+ {
29444
+ name: "page_request",
29445
+ description: "\u4F7F\u7528\u5F53\u524D\u9875\u9762\u767B\u5F55\u6001\u53D1\u8D77 fetch \u5E76\u7B49\u5F85\u54CD\u5E94\uFF0C\u4E00\u6B21\u8FD4\u56DE\u7ED3\u679C\uFF1B\u9875\u9762\u63A5\u53E3\u8C03\u7528\u4F18\u5148\u7528\u5B83\uFF0C\u907F\u514D eval_script \u53D1\u8D77\u8BF7\u6C42\u540E\u518D\u8F6E\u8BE2\u3002",
29446
+ inputSchema: {
29447
+ type: "object",
29448
+ properties: {
29449
+ target: TARGET_ARG,
29450
+ url: { type: "string", description: "\u76F8\u5BF9\u6216\u7EDD\u5BF9 HTTP(S) URL" },
29451
+ method: { type: "string", description: "\u9ED8\u8BA4 GET" },
29452
+ headers: { type: "object", description: "\u8BF7\u6C42\u5934" },
29453
+ body: { description: "\u5B57\u7B26\u4E32\u6216 JSON \u5BF9\u8C61\uFF1B\u5BF9\u8C61\u4F1A\u81EA\u52A8 JSON.stringify" },
29454
+ responseType: { type: "string", enum: ["auto", "json", "text"], description: "\u9ED8\u8BA4 auto" },
29455
+ timeoutMs: { type: "number", description: "\u9ED8\u8BA4 10000\uFF0C\u6700\u5927 30000" },
29456
+ maxOutputLength: { type: "number", description: "\u54CD\u5E94\u5185\u5BB9\u5B57\u7B26\u4E0A\u9650\uFF0C\u9ED8\u8BA4 8000\uFF0C\u6700\u5927 50000" }
29457
+ },
29458
+ required: ["url"]
29459
+ }
29460
+ },
29405
29461
  {
29406
29462
  name: "list_network_requests",
29407
29463
  description: "\u5217\u51FA\u6355\u83B7\u7684\u7F51\u7EDC\u8BF7\u6C42\uFF0C\u652F\u6301 URL/\u65B9\u6CD5/\u72B6\u6001/\u7C7B\u578B\u8FC7\u6EE4\uFF1B\u8D85\u957F URL \u81EA\u52A8\u6458\u8981\u3002",
@@ -29481,7 +29537,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
29481
29537
  },
29482
29538
  {
29483
29539
  name: "get_page_content",
29484
- description: "\u63D0\u53D6\u9875\u9762\u6587\u672C/HTML/\u7ED3\u6784\u5316\u6570\u636E\uFF0C\u6BD4\u622A\u56FE\u8F7B\u91CF\u3002\u9ED8\u8BA4\u4E0A\u9650 8000 \u5B57\u7B26\uFF1B\u4E0D\u591F\u65F6\u8C03\u5927 maxLength \u6216\u7528 offset \u7FFB\u9875\u3001selector \u6536\u7A84\u3002\u4E0D\u53CD\u6620 CSS\uFF0C\u4E0D\u542B iframe\u3002",
29540
+ description: "\u63D0\u53D6\u9875\u9762\u6587\u672C/HTML/\u7ED3\u6784\u5316\u6570\u636E\uFF0C\u6BD4\u622A\u56FE\u8F7B\u91CF\u3002text \u6A21\u5F0F\u9012\u5F52\u6536\u96C6\u540C\u6E90 iframe \u6587\u672C\uFF08\u9002\u5408\u6587\u6863\u7C7B\u9875\u9762\uFF09\u3002\u9ED8\u8BA4\u4E0A\u9650 8000 \u5B57\u7B26\uFF1B\u4E0D\u591F\u65F6\u8C03\u5927 maxLength \u6216\u7528 offset \u7FFB\u9875\u3001selector \u6536\u7A84\u3002\u4E0D\u53CD\u6620 CSS\u3002",
29485
29541
  inputSchema: {
29486
29542
  type: "object",
29487
29543
  properties: {
@@ -29534,7 +29590,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
29534
29590
  },
29535
29591
  {
29536
29592
  name: "dispatch_action",
29537
- description: "\u5BF9 get_interactive_snapshot \u8FD4\u56DE\u7684 ref \u6267\u884C\u64CD\u4F5C\uFF1Aclick/fill/press/scroll/select/hover/focus\u3002\u26A0\uFE0F \u5FC5\u987B\u5148\u5FEB\u7167\u62FF ref\uFF1B\u64CD\u4F5C\u540E\u5EFA\u8BAE\u590D\u67E5\u9875\u9762\u72B6\u6001\u3002",
29593
+ description: "\u6267\u884C\u5355\u4E2A\u6216\u4E00\u6279\u4EA4\u4E92\u52A8\u4F5C\u3002\u53EF\u7528\u5FEB\u7167 ref \u6216 CSS selector \u5B9A\u4F4D\uFF1B\u4F18\u5148\u7528 actions \u6279\u91CF\u5B8C\u6210\u8FDE\u7EED\u586B\u5199/\u70B9\u51FB\uFF0CsnapshotAfter:true \u53EF\u5728\u540C\u4E00\u6B21\u8C03\u7528\u8FD4\u56DE\u64CD\u4F5C\u540E\u5143\u7D20\uFF0C\u51CF\u5C11\u6A21\u578B\u5F80\u8FD4\u3002",
29538
29594
  inputSchema: {
29539
29595
  type: "object",
29540
29596
  properties: {
@@ -29543,6 +29599,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
29543
29599
  type: "string",
29544
29600
  description: "\u5143\u7D20 ref\uFF0C\u5982 'e1'\uFF08\u6765\u81EA get_interactive_snapshot\uFF09"
29545
29601
  },
29602
+ selector: {
29603
+ type: "string",
29604
+ description: "CSS \u9009\u62E9\u5668\uFF1B\u5DF2\u77E5\u9009\u62E9\u5668\u65F6\u53EF\u66FF\u4EE3 ref\uFF0C\u7701\u53BB\u524D\u7F6E\u5FEB\u7167"
29605
+ },
29546
29606
  action: {
29547
29607
  type: "string",
29548
29608
  enum: ["click", "fill", "press", "scroll", "select", "hover", "focus"]
@@ -29566,9 +29626,43 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
29566
29626
  waitMs: {
29567
29627
  type: "number",
29568
29628
  description: "\u64CD\u4F5C\u540E\u7B49\u5F85 ms\uFF0C\u9ED8\u8BA4 500\uFF0C\u6700\u5927 3000"
29629
+ },
29630
+ actions: {
29631
+ type: "array",
29632
+ maxItems: 20,
29633
+ description: "\u987A\u5E8F\u6267\u884C\u7684\u52A8\u4F5C\uFF0C\u6700\u591A 20 \u4E2A\uFF1B\u6BCF\u9879\u652F\u6301 ref/selector\u3001action\u3001value/key\u3001deltaX/deltaY\u3001waitMs",
29634
+ items: {
29635
+ type: "object",
29636
+ properties: {
29637
+ ref: { type: "string" },
29638
+ selector: { type: "string" },
29639
+ action: { type: "string", enum: ["click", "fill", "press", "scroll", "select", "hover", "focus"] },
29640
+ value: { type: "string" },
29641
+ key: { type: "string" },
29642
+ deltaX: { type: "number" },
29643
+ deltaY: { type: "number" },
29644
+ waitMs: { type: "number" }
29645
+ },
29646
+ required: ["action"]
29647
+ }
29648
+ },
29649
+ stopOnError: {
29650
+ type: "boolean",
29651
+ description: "\u6279\u91CF\u52A8\u4F5C\u5931\u8D25\u65F6\u7ACB\u5373\u505C\u6B62\uFF0C\u9ED8\u8BA4 true"
29652
+ },
29653
+ snapshotAfter: {
29654
+ type: "boolean",
29655
+ description: "\u5728\u6700\u540E\u4E00\u4E2A\u52A8\u4F5C\u540E\u8FD4\u56DE\u65B0\u7684\u4EA4\u4E92\u5FEB\u7167\uFF0C\u9ED8\u8BA4 false"
29656
+ },
29657
+ snapshotSelector: {
29658
+ type: "string",
29659
+ description: "\u9650\u5236\u64CD\u4F5C\u540E\u5FEB\u7167\u8303\u56F4"
29660
+ },
29661
+ snapshotMaxElements: {
29662
+ type: "number",
29663
+ description: "\u64CD\u4F5C\u540E\u5FEB\u7167\u5143\u7D20\u4E0A\u9650\uFF0C\u9ED8\u8BA4 20"
29569
29664
  }
29570
- },
29571
- required: ["ref", "action"]
29665
+ }
29572
29666
  }
29573
29667
  }
29574
29668
  ]
@@ -29578,7 +29672,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
29578
29672
  const args = request.params.arguments || {};
29579
29673
  try {
29580
29674
  if (name === "inspect_page") {
29581
- const { target, selector, includeInteractive = true, maxElements = 30, detail = false } = args;
29675
+ const { target, selector, includeInteractive = true, maxElements = 20, includeElements = true, detail = false } = args;
29582
29676
  const snapshot = await askChrome("inspectPageSnapshot", {
29583
29677
  target,
29584
29678
  selector,
@@ -29593,13 +29687,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
29593
29687
  const interactiveCount = Array.isArray(interactive?.elements) ? interactive.elements.length : Array.isArray(interactive) ? interactive.length : void 0;
29594
29688
  const summary = {
29595
29689
  title: page?.metadata?.title,
29596
- url: page?.metadata?.url,
29690
+ url: truncateUrl(page?.metadata?.url),
29597
29691
  description: page?.metadata?.description,
29598
29692
  links,
29599
29693
  buttons,
29600
29694
  forms,
29601
29695
  interactiveCount
29602
29696
  };
29697
+ const elements = includeElements && Array.isArray(interactive?.elements) ? interactive.elements : void 0;
29603
29698
  return {
29604
29699
  content: [
29605
29700
  {
@@ -29612,7 +29707,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
29612
29707
  nextStepHint: "\u89C6\u89C9\u7528 capture_screenshot\uFF1B\u70B9\u51FB/\u8F93\u5165\u7528 get_interactive_snapshot + dispatch_action\uFF1B\u8BF7\u6C42/\u6027\u80FD\u7528 list_network_requests / perf_metrics\u3002"
29613
29708
  } : {
29614
29709
  summary,
29615
- nextStepHint: "\u4F20 detail:true \u67E5\u770B\u5B8C\u6574 page/interactive\uFF1B\u89C6\u89C9\u7528 capture_screenshot\uFF1B\u4EA4\u4E92\u5148 get_interactive_snapshot \u518D dispatch_action\u3002"
29710
+ interactive: elements ? { viewport: interactive?.viewport, elements } : void 0,
29711
+ nextStepHint: "\u53EF\u76F4\u63A5\u7528 interactive.elements \u7684 ref \u8C03 dispatch_action\uFF1B\u8FDE\u7EED\u52A8\u4F5C\u653E\u8FDB actions\uFF0C\u64CD\u4F5C\u540E\u9700\u7EE7\u7EED\u5B9A\u4F4D\u65F6\u4F20 snapshotAfter:true\u3002"
29616
29712
  }
29617
29713
  )
29618
29714
  }
@@ -29758,8 +29854,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
29758
29854
  return { content: [{ type: "text", text: out(res) }] };
29759
29855
  }
29760
29856
  if (name === "eval_script") {
29761
- const res = await askChrome("eval", { target: args.target, code: args.code });
29762
- return { content: [{ type: "text", text: out(res) }] };
29857
+ const timeoutMs = clampNumber(args.timeoutMs, 1e4, 100, 3e4);
29858
+ const res = await askChrome("eval", {
29859
+ target: args.target,
29860
+ code: args.code,
29861
+ awaitPromise: args.awaitPromise !== false,
29862
+ timeoutMs
29863
+ }, { timeoutMs: timeoutMs + 3e3 });
29864
+ return { content: [{ type: "text", text: boundedOut(res, args.maxOutputLength) }] };
29865
+ }
29866
+ if (name === "page_request") {
29867
+ const timeoutMs = clampNumber(args.timeoutMs, 1e4, 100, 3e4);
29868
+ const maxOutputLength = clampNumber(args.maxOutputLength, DEFAULT_EVAL_OUTPUT_LENGTH, 200, MAX_EVAL_OUTPUT_LENGTH);
29869
+ const res = await askChrome("pageRequest", {
29870
+ target: args.target,
29871
+ url: args.url,
29872
+ method: args.method,
29873
+ headers: args.headers,
29874
+ body: args.body,
29875
+ responseType: args.responseType,
29876
+ timeoutMs,
29877
+ maxOutputLength
29878
+ }, { timeoutMs: timeoutMs + 3e3 });
29879
+ return { content: [{ type: "text", text: boundedOut(res, maxOutputLength) }] };
29763
29880
  }
29764
29881
  if (name === "list_network_requests") {
29765
29882
  const { target, filter, method, status, resourceType, limit, priorityMode = "debug" } = args;
@@ -29818,7 +29935,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
29818
29935
  }
29819
29936
  if (offset > 0 && mode === "text") {
29820
29937
  const selectorExpr = selector ? `(document.querySelector(${JSON.stringify(selector)}) || document.body)` : "document.body";
29821
- const code = `(function(){try{var text=(${selectorExpr}).innerText||"";var start=${offset};var end=${offset + maxLength};return {content:text.slice(start,end),offset:start,totalLength:text.length,hasMore:text.length>end};}catch(e){return {error:e.message}}})()`;
29938
+ const code = `(function(){try{function ct(el){var t=el.innerText||el.textContent||"";try{var fs=el.querySelectorAll("iframe");for(var i=0;i<fs.length;i++){try{var d=fs[i].contentDocument;if(d&&d.body)t+="\\n\\n"+ct(d.body);}catch(e){}}}catch(e){}return t;}var text=ct(${selectorExpr});var start=${offset};var end=${offset + maxLength};return {content:text.slice(start,end),offset:start,totalLength:text.length,hasMore:text.length>end};}catch(e){return {error:e.message}}})()`;
29822
29939
  const res2 = await askChrome("eval", { target, code });
29823
29940
  return { content: [{ type: "text", text: out(res2) }] };
29824
29941
  }
@@ -29832,6 +29949,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
29832
29949
  return { content: [{ type: "text", text: out(res2) }] };
29833
29950
  }
29834
29951
  const res = await askChrome("getPageContent", { target, mode, selector, maxLength, includeMetadata });
29952
+ if (typeof res?.metadata?.url === "string" && res.metadata.url.length > 200) {
29953
+ res.metadata.url = truncateUrl(res.metadata.url);
29954
+ res.metadata.urlTruncated = true;
29955
+ }
29835
29956
  return { content: [{ type: "text", text: out(res) }] };
29836
29957
  }
29837
29958
  if (name === "get_interactive_snapshot") {
@@ -29840,8 +29961,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
29840
29961
  return { content: [{ type: "text", text: out(res) }] };
29841
29962
  }
29842
29963
  if (name === "dispatch_action") {
29843
- const { target, ref, action, value, key, deltaX, deltaY, waitMs } = args;
29844
- const res = await askChrome("dispatchAction", { target, ref, action, value, key, deltaX, deltaY, waitMs }, { timeoutMs: 1e4 });
29964
+ const steps = Array.isArray(args.actions) ? args.actions : [args];
29965
+ if (!steps.length || steps.length > 20) throw new Error("actions \u6570\u91CF\u5FC5\u987B\u5728 1-20 \u4E4B\u95F4");
29966
+ const totalWaitMs = steps.reduce((sum, step) => sum + clampNumber(step.waitMs, 500, 0, 3e3), 0);
29967
+ const timeoutMs = Math.min(45e3, 1e4 + totalWaitMs);
29968
+ const res = await askChrome("dispatchAction", args, { timeoutMs });
29845
29969
  return { content: [{ type: "text", text: out(res) }] };
29846
29970
  }
29847
29971
  return { content: [{ type: "text", text: `\u672A\u77E5\u5DE5\u5177\uFF1A${name}` }] };
@@ -989,10 +989,90 @@ async function handleSymbolicHints(params = {}) {
989
989
 
990
990
  async function handleEval(params = {}) {
991
991
  const target = await ensureAttached(params)
992
- const { result } = await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
993
- expression: params.code,
994
- returnByValue: true,
995
- })
992
+ const timeoutMs = Math.min(30000, Math.max(100, Number(params.timeoutMs) || 10000))
993
+ const { result, exceptionDetails } = await withTimeout(
994
+ chrome.debugger.sendCommand(target, "Runtime.evaluate", {
995
+ expression: params.code,
996
+ returnByValue: true,
997
+ awaitPromise: params.awaitPromise !== false,
998
+ }),
999
+ timeoutMs,
1000
+ "eval_script"
1001
+ )
1002
+ if (exceptionDetails) {
1003
+ throw new Error(exceptionDetails.exception?.description || exceptionDetails.text || "脚本执行失败")
1004
+ }
1005
+ return result?.value
1006
+ }
1007
+
1008
+ async function handlePageRequest(params = {}) {
1009
+ const target = await ensureAttached(params)
1010
+ if (!params.url || typeof params.url !== 'string') throw new Error("page_request 需要提供 url")
1011
+ const timeoutMs = Math.min(30000, Math.max(100, Number(params.timeoutMs) || 10000))
1012
+ const maxOutputLength = Math.min(50000, Math.max(200, Number(params.maxOutputLength) || 8000))
1013
+ const method = String(params.method || 'GET').toUpperCase()
1014
+ const responseType = ['auto', 'json', 'text'].includes(params.responseType) ? params.responseType : 'auto'
1015
+ const headers = params.headers && typeof params.headers === 'object' ? { ...params.headers } : {}
1016
+ let body = params.body
1017
+ if (body !== undefined && body !== null && typeof body !== 'string') {
1018
+ body = JSON.stringify(body)
1019
+ if (!Object.keys(headers).some((name) => name.toLowerCase() === 'content-type')) {
1020
+ headers['Content-Type'] = 'application/json'
1021
+ }
1022
+ }
1023
+
1024
+ const expression = `(async function() {
1025
+ const controller = new AbortController();
1026
+ const timer = setTimeout(() => controller.abort(), ${timeoutMs});
1027
+ try {
1028
+ const url = new URL(${JSON.stringify(params.url)}, window.location.href);
1029
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
1030
+ throw new Error('仅支持 HTTP(S) URL');
1031
+ }
1032
+ const response = await fetch(url.href, {
1033
+ method: ${JSON.stringify(method)},
1034
+ headers: ${JSON.stringify(headers)},
1035
+ body: ${body === undefined || body === null ? 'undefined' : JSON.stringify(String(body))},
1036
+ credentials: 'include',
1037
+ signal: controller.signal,
1038
+ });
1039
+ const text = await response.text();
1040
+ const contentType = response.headers.get('content-type') || '';
1041
+ const truncated = text.length > ${maxOutputLength};
1042
+ const content = truncated ? text.slice(0, ${maxOutputLength}) : text;
1043
+ let data = content;
1044
+ if (!truncated && (${JSON.stringify(responseType)} === 'json' || (${JSON.stringify(responseType)} === 'auto' && contentType.includes('json')))) {
1045
+ try { data = JSON.parse(content); } catch (e) {
1046
+ if (${JSON.stringify(responseType)} === 'json') throw new Error('响应不是有效 JSON: ' + e.message);
1047
+ }
1048
+ }
1049
+ return {
1050
+ ok: response.ok,
1051
+ status: response.status,
1052
+ statusText: response.statusText,
1053
+ url: response.url,
1054
+ contentType,
1055
+ originalLength: text.length,
1056
+ truncated,
1057
+ data,
1058
+ };
1059
+ } finally {
1060
+ clearTimeout(timer);
1061
+ }
1062
+ })()`
1063
+
1064
+ const { result, exceptionDetails } = await withTimeout(
1065
+ chrome.debugger.sendCommand(target, "Runtime.evaluate", {
1066
+ expression,
1067
+ returnByValue: true,
1068
+ awaitPromise: true,
1069
+ }),
1070
+ timeoutMs + 500,
1071
+ "page_request"
1072
+ )
1073
+ if (exceptionDetails) {
1074
+ throw new Error(exceptionDetails.exception?.description || exceptionDetails.text || "页面请求失败")
1075
+ }
996
1076
  return result?.value
997
1077
  }
998
1078
 
@@ -1351,6 +1431,15 @@ async function handleGetPageContent(params = {}) {
1351
1431
  async function handleGetInteractiveSnapshot(params = {}) {
1352
1432
  const { target, session } = await ensureAttachedSession(params)
1353
1433
  const { selector, includeText = true, maxElements = 100 } = params
1434
+ const value = await evaluateInteractiveSnapshot(target, { selector, includeText, maxElements })
1435
+ if (value) {
1436
+ value.target = describeCommandTarget(params, session)
1437
+ value.tabId = session.tabId
1438
+ }
1439
+ return value
1440
+ }
1441
+
1442
+ async function evaluateInteractiveSnapshot(target, { selector, includeText = true, maxElements = 100 } = {}) {
1354
1443
  const expression = GhostBridgeDom.buildInteractiveSnapshotExpression({ selector, includeText, maxElements })
1355
1444
 
1356
1445
  const { result } = await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
@@ -1359,12 +1448,7 @@ async function handleGetInteractiveSnapshot(params = {}) {
1359
1448
  })
1360
1449
 
1361
1450
  if (result?.value?.error) throw new Error(result.value.error)
1362
- const value = result?.value
1363
- if (value) {
1364
- value.target = describeCommandTarget(params, session)
1365
- value.tabId = session.tabId
1366
- }
1367
- return value
1451
+ return result?.value
1368
1452
  }
1369
1453
 
1370
1454
  // ========== DOM 交互:动作分发器 ==========
@@ -1375,16 +1459,59 @@ async function handleDispatchAction(params = {}) {
1375
1459
  throw new Error("已绑定命名 target 时,dispatch_action 必须提供 target,避免跨页面误用 ref")
1376
1460
  }
1377
1461
  const target = await ensureAttached(params)
1378
- const { ref, action, value, key, deltaX, deltaY, waitMs = 500 } = params
1462
+ const isBatch = Array.isArray(params.actions)
1463
+ const actions = isBatch ? params.actions : [params]
1464
+ if (!actions.length || actions.length > 20) throw new Error("actions 数量必须在 1-20 之间")
1465
+
1466
+ const results = []
1467
+ for (let index = 0; index < actions.length; index++) {
1468
+ try {
1469
+ results.push(await executeDispatchAction(target, actions[index]))
1470
+ } catch (e) {
1471
+ if (!isBatch) throw e
1472
+ results.push({ index, success: false, error: e.message })
1473
+ if (params.stopOnError !== false) break
1474
+ }
1475
+ }
1476
+
1477
+ const pageAfter = await readPageState(target)
1478
+ const response = isBatch
1479
+ ? {
1480
+ success: results.length === actions.length && results.every((item) => item.success),
1481
+ completed: results.filter((item) => item.success).length,
1482
+ total: actions.length,
1483
+ results,
1484
+ pageAfter,
1485
+ }
1486
+ : { ...results[0], pageAfter }
1487
+
1488
+ if (params.snapshotAfter) {
1489
+ response.snapshotAfter = await evaluateInteractiveSnapshot(target, {
1490
+ selector: params.snapshotSelector,
1491
+ includeText: true,
1492
+ maxElements: Math.min(100, Math.max(1, Number(params.snapshotMaxElements) || 20)),
1493
+ })
1494
+ }
1495
+
1496
+ return response
1497
+ }
1498
+
1499
+ async function executeDispatchAction(target, step = {}) {
1500
+ const { ref, selector, action, value, key, deltaX, deltaY, waitMs = 500 } = step
1379
1501
 
1380
- if (!ref) throw new Error("需要提供 ref(元素标识,如 'e1')")
1502
+ if (!ref && !selector) throw new Error("需要提供 ref 或 selector")
1503
+ if (ref && !/^e\d+$/.test(String(ref))) throw new Error(`无效的 ref: ${ref}`)
1381
1504
  if (!action) throw new Error("需要提供 action(动作类型:click/fill/press/scroll/select/hover/focus)")
1382
1505
 
1506
+ const locator = ref ? `[data-ghost-ref="${ref}"]` : String(selector)
1507
+ const locatorExpression = JSON.stringify(locator)
1508
+ const locatorLabel = ref || selector
1509
+
1383
1510
  // Step 1: 实时获取目标元素的最新坐标和状态
1384
1511
  const locateExpression = `(function() {
1385
1512
  try {
1386
- const el = document.querySelector('[data-ghost-ref="${ref}"]');
1387
- if (!el) return { error: '元素未找到,ref 可能已失效,请重新获取快照' };
1513
+ const el = document.querySelector(${locatorExpression});
1514
+ if (!el) return { error: '元素未找到:' + ${JSON.stringify(locatorLabel)} };
1388
1515
  // 关键修复:确保元素在视口内,否则超出屏幕的坐标无法被 CDP 模拟点击
1389
1516
  el.scrollIntoView({ block: 'center', inline: 'center' });
1390
1517
  const rect = el.getBoundingClientRect();
@@ -1408,12 +1535,12 @@ async function handleDispatchAction(params = {}) {
1408
1535
 
1409
1536
  const loc = locResult?.value
1410
1537
  if (!loc || loc.error) throw new Error(loc?.error || "无法定位元素")
1411
- if (loc.disabled) throw new Error(`元素 ${ref} 已被禁用 (disabled)`)
1538
+ if (loc.disabled) throw new Error(`元素 ${locatorLabel} 已被禁用 (disabled)`)
1412
1539
 
1413
1540
  const cx = loc.cx
1414
1541
  const cy = loc.cy
1415
1542
 
1416
- let actionResult = { ref, action, success: true }
1543
+ let actionResult = { ...(ref ? { ref } : { selector }), action, success: true }
1417
1544
 
1418
1545
  // Step 2: 根据动作类型执行 CDP 命令
1419
1546
  if (action === "click") {
@@ -1424,7 +1551,7 @@ async function handleDispatchAction(params = {}) {
1424
1551
  await chrome.debugger.sendCommand(target, "Input.dispatchMouseEvent", {
1425
1552
  type: "mouseReleased", x: cx, y: cy, button: "left", clickCount: 1,
1426
1553
  })
1427
- actionResult.detail = `已点击 ${ref} (${loc.tag}) 坐标 (${cx}, ${cy})`
1554
+ actionResult.detail = `已点击 ${locatorLabel} (${loc.tag}) 坐标 (${cx}, ${cy})`
1428
1555
 
1429
1556
  } else if (action === "fill") {
1430
1557
  if (value === undefined || value === null) throw new Error("fill 动作需要提供 value 参数")
@@ -1438,7 +1565,7 @@ async function handleDispatchAction(params = {}) {
1438
1565
  // 全选并清空已有内容
1439
1566
  await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
1440
1567
  expression: `(function() {
1441
- const el = document.querySelector('[data-ghost-ref="${ref}"]');
1568
+ const el = document.querySelector(${locatorExpression});
1442
1569
  if (el) { el.focus(); el.select && el.select(); }
1443
1570
  })()`,
1444
1571
  })
@@ -1449,14 +1576,14 @@ async function handleDispatchAction(params = {}) {
1449
1576
  // 强制触发 input/change 事件(兼容 React/Vue)
1450
1577
  await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
1451
1578
  expression: `(function() {
1452
- const el = document.querySelector('[data-ghost-ref="${ref}"]');
1579
+ const el = document.querySelector(${locatorExpression});
1453
1580
  if (el) {
1454
1581
  el.dispatchEvent(new Event('input', { bubbles: true }));
1455
1582
  el.dispatchEvent(new Event('change', { bubbles: true }));
1456
1583
  }
1457
1584
  })()`,
1458
1585
  })
1459
- actionResult.detail = `已在 ${ref} (${loc.tag}) 中填入 "${String(value).slice(0, 50)}"`
1586
+ actionResult.detail = `已在 ${locatorLabel} (${loc.tag}) 中填入 "${String(value).slice(0, 50)}"`
1460
1587
 
1461
1588
  } else if (action === "press") {
1462
1589
  // 模拟键盘按键
@@ -1464,7 +1591,7 @@ async function handleDispatchAction(params = {}) {
1464
1591
  // 先确保元素聚焦
1465
1592
  await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
1466
1593
  expression: `(function() {
1467
- const el = document.querySelector('[data-ghost-ref="${ref}"]');
1594
+ const el = document.querySelector(${locatorExpression});
1468
1595
  if (el) el.focus();
1469
1596
  })()`,
1470
1597
  })
@@ -1474,7 +1601,7 @@ async function handleDispatchAction(params = {}) {
1474
1601
  await chrome.debugger.sendCommand(target, "Input.dispatchKeyEvent", {
1475
1602
  type: "keyUp", key: keyName,
1476
1603
  })
1477
- actionResult.detail = `已在 ${ref} 上按下 ${keyName}`
1604
+ actionResult.detail = `已在 ${locatorLabel} 上按下 ${keyName}`
1478
1605
 
1479
1606
  } else if (action === "scroll") {
1480
1607
  const dx = deltaX || 0
@@ -1482,36 +1609,36 @@ async function handleDispatchAction(params = {}) {
1482
1609
  await chrome.debugger.sendCommand(target, "Input.dispatchMouseEvent", {
1483
1610
  type: "mouseWheel", x: cx, y: cy, deltaX: dx, deltaY: dy,
1484
1611
  })
1485
- actionResult.detail = `已在 ${ref} 位置滚动 (${dx}, ${dy})`
1612
+ actionResult.detail = `已在 ${locatorLabel} 位置滚动 (${dx}, ${dy})`
1486
1613
 
1487
1614
  } else if (action === "select") {
1488
1615
  // 下拉框选择
1489
1616
  if (value === undefined) throw new Error("select 动作需要提供 value 参数")
1490
1617
  await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
1491
1618
  expression: `(function() {
1492
- const el = document.querySelector('[data-ghost-ref="${ref}"]');
1619
+ const el = document.querySelector(${locatorExpression});
1493
1620
  if (el && el.tagName === 'SELECT') {
1494
1621
  el.value = ${JSON.stringify(String(value))};
1495
1622
  el.dispatchEvent(new Event('change', { bubbles: true }));
1496
1623
  }
1497
1624
  })()`,
1498
1625
  })
1499
- actionResult.detail = `已在 ${ref} 选择值 "${value}"`
1626
+ actionResult.detail = `已在 ${locatorLabel} 选择值 "${value}"`
1500
1627
 
1501
1628
  } else if (action === "hover") {
1502
1629
  await chrome.debugger.sendCommand(target, "Input.dispatchMouseEvent", {
1503
1630
  type: "mouseMoved", x: cx, y: cy,
1504
1631
  })
1505
- actionResult.detail = `已将鼠标悬停到 ${ref} (${cx}, ${cy})`
1632
+ actionResult.detail = `已将鼠标悬停到 ${locatorLabel} (${cx}, ${cy})`
1506
1633
 
1507
1634
  } else if (action === "focus") {
1508
1635
  await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
1509
1636
  expression: `(function() {
1510
- const el = document.querySelector('[data-ghost-ref="${ref}"]');
1637
+ const el = document.querySelector(${locatorExpression});
1511
1638
  if (el) el.focus();
1512
1639
  })()`,
1513
1640
  })
1514
- actionResult.detail = `已聚焦到 ${ref}`
1641
+ actionResult.detail = `已聚焦到 ${locatorLabel}`
1515
1642
 
1516
1643
  } else {
1517
1644
  throw new Error(`不支持的动作类型: ${action},可选: click/fill/press/scroll/select/hover/focus`)
@@ -1522,7 +1649,10 @@ async function handleDispatchAction(params = {}) {
1522
1649
  await sleep(Math.min(waitMs, 3000))
1523
1650
  }
1524
1651
 
1525
- // Step 4: 获取操作后状态摘要
1652
+ return actionResult
1653
+ }
1654
+
1655
+ async function readPageState(target) {
1526
1656
  const { result: afterResult } = await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
1527
1657
  expression: `(function() {
1528
1658
  return {
@@ -1533,11 +1663,7 @@ async function handleDispatchAction(params = {}) {
1533
1663
  })()`,
1534
1664
  returnByValue: true,
1535
1665
  })
1536
- if (afterResult?.value) {
1537
- actionResult.pageAfter = afterResult.value
1538
- }
1539
-
1540
- return actionResult
1666
+ return afterResult?.value
1541
1667
  }
1542
1668
 
1543
1669
  // 处理来自服务器的命令
@@ -1578,6 +1704,7 @@ async function handleCommand(message) {
1578
1704
  else if (command === "findByString") result = await handleFindByString(params)
1579
1705
  else if (command === "symbolicHints") result = await handleSymbolicHints(params)
1580
1706
  else if (command === "eval") result = await handleEval(params)
1707
+ else if (command === "pageRequest") result = await handlePageRequest(params)
1581
1708
  else if (command === "listNetworkRequests") result = await handleListNetworkRequests(params)
1582
1709
  else if (command === "getNetworkDetail") result = await handleGetNetworkDetail(params)
1583
1710
  else if (command === "clearNetworkRequests") result = await handleClearNetworkRequests(params)
@@ -1825,6 +1952,24 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1825
1952
  return false
1826
1953
  })
1827
1954
 
1955
+ // ========== 定时兜底重连 ==========
1956
+ // offscreen 的重连循环在 daemon 重启/长时间找不到服务后偶发停摆(现象:手动点 Connect 才恢复)。
1957
+ // 每分钟检查一次连接状态,未连接则重新触发完整连接流程,保证无人值守时也能自动恢复
1958
+ chrome.alarms.create('ghost-bridge-keepalive', { delayInMinutes: 1, periodInMinutes: 1 })
1959
+ chrome.alarms.onAlarm.addListener(async (alarm) => {
1960
+ if (alarm.name !== 'ghost-bridge-keepalive') return
1961
+ if (!state.enabled) return
1962
+ try {
1963
+ const status = await chrome.runtime.sendMessage({ type: 'getOffscreenStatus' }).catch(() => null)
1964
+ if (!status || !status.connected) {
1965
+ log('定时兜底:连接未建立,重新触发连接流程')
1966
+ await startBridgeConnection()
1967
+ }
1968
+ } catch (e) {
1969
+ log(`定时兜底重连失败:${e.message}`)
1970
+ }
1971
+ })
1972
+
1828
1973
  // ========== 唤醒探活钩子 ==========
1829
1974
  // 系统锁屏/睡眠唤醒后,WebSocket 可能处于半开状态(onclose 不触发、徽章仍显示已连接),
1830
1975
  // 通知 offscreen 立即发一次心跳:无响应则关闭死链并马上重连,不等 15 秒周期心跳超时
@@ -299,9 +299,26 @@
299
299
  }
300
300
 
301
301
  if (mode === 'text') {
302
- let text = targetElement.innerText || targetElement.textContent || '';
302
+ // 递归收集同源 iframe 内的文本:跨域 iframe 访问 contentDocument 会抛错,跳过即可。
303
+ // 大量文档类页面(钉钉文档、italent 等)正文都在 iframe 里,不递归会拿到空文本,
304
+ // 迫使模型退化为整页截图读文档——那是长会话里最昂贵的 token 开销
305
+ function collectText(el) {
306
+ let text = el.innerText || el.textContent || '';
307
+ try {
308
+ var frames = el.querySelectorAll('iframe');
309
+ for (var i = 0; i < frames.length; i++) {
310
+ try {
311
+ var doc = frames[i].contentDocument;
312
+ if (doc && doc.body) text += '\\n\\n' + collectText(doc.body);
313
+ } catch (e) {}
314
+ }
315
+ } catch (e) {}
316
+ return text;
317
+ }
318
+ let text = collectText(targetElement);
303
319
  text = text.replace(/\\n{3,}/g, '\\n\\n').trim();
304
320
  result.contentLength = text.length;
321
+ result.includesIframes = true;
305
322
  const truncated = smartTruncateText(text, maxLength);
306
323
  result.content = truncated.content;
307
324
  result.truncated = truncated.truncated;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Ghost Bridge",
4
- "version": "1.0.1",
4
+ "version": "1.1.0",
5
5
  "description": "Zero-restart Chrome debugger bridge for Claude MCP, optimized for no-sourcemap production debugging.",
6
6
  "permissions": [
7
7
  "debugger",
@@ -10,7 +10,8 @@
10
10
  "storage",
11
11
  "tabs",
12
12
  "offscreen",
13
- "idle"
13
+ "idle",
14
+ "alarms"
14
15
  ],
15
16
  "host_permissions": [
16
17
  "ws://localhost/*",
@@ -33,4 +34,4 @@
33
34
  "background": {
34
35
  "service_worker": "background.js"
35
36
  }
36
- }
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghost-bridge",
3
- "version": "1.0.1",
3
+ "version": "1.1.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.",