ghost-bridge 1.1.0 → 1.2.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
@@ -16,6 +16,8 @@ Most browser-capable AI tools start a separate browser. Ghost Bridge connects AI
16
16
  - Inspect page structure, text, screenshots, errors, and network traffic
17
17
  - Search and extract script sources, even in production bundles
18
18
  - Click, type, scroll, and submit forms on the current page
19
+ - Locate elements by role/name, label, placeholder, text, test ID, or CSS across open Shadow DOM and same-origin iframes
20
+ - Run `locate → act → wait → snapshot` as one browser call instead of model-driven polling
19
21
  - Bind multiple Chrome tabs as named targets and operate them independently
20
22
  - Share one Chrome transport across multiple MCP clients
21
23
 
@@ -94,7 +96,7 @@ Typical prompts:
94
96
  | `capture_screenshot` | Visual inspection and UI debugging |
95
97
  | `get_page_content` | Text, HTML, and structured DOM extraction |
96
98
  | `get_interactive_snapshot` | Find clickable and editable elements |
97
- | `dispatch_action` | Click, fill, press, scroll, hover, or select; supports selector-based and batched actions |
99
+ | `dispatch_action` | Locate, act, wait, and verify in one call; supports semantic locators and batches |
98
100
  | `eval_script` | Execute JavaScript, wait for returned promises, and cap arbitrary output |
99
101
  | `page_request` | Send an authenticated page-context request and wait for the response in one call |
100
102
  | `bind_tab` | Bind a Chrome tab as a named target such as `cases` or `app` |
@@ -115,12 +117,12 @@ Typical prompts:
115
117
 
116
118
  Recommended flow:
117
119
 
118
- 1. Start with `inspect_page`; its compact response includes a small set of actionable refs
119
- 2. Use `capture_screenshot` for visual issues
120
+ 1. When the target is describable, call `dispatch_action` directly with a semantic `locator`
121
+ 2. Use `inspect_page` when the page is unfamiliar or a locator is ambiguous; its compact response includes actionable refs
122
+ 3. Use `capture_screenshot` for visual issues
120
123
  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` 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
+ 4. Use `get_page_content` for DOM or text extraction
125
+ 5. Put consecutive fills/clicks into one `dispatch_action.actions` call; add `waitFor` to the action that changes state and `snapshotAfter` when the resulting UI is needed
124
126
 
125
127
  Round-trip-efficient examples:
126
128
 
@@ -128,14 +130,42 @@ Round-trip-efficient examples:
128
130
  {
129
131
  "target": "app",
130
132
  "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 }
133
+ {
134
+ "locator": { "role": "textbox", "label": "Email" },
135
+ "action": "fill",
136
+ "value": "user@example.com"
137
+ },
138
+ {
139
+ "locator": { "role": "textbox", "label": "Password" },
140
+ "action": "fill",
141
+ "value": "secret"
142
+ },
143
+ {
144
+ "locator": { "role": "button", "name": "Sign in" },
145
+ "action": "click",
146
+ "waitFor": {
147
+ "type": "element",
148
+ "locator": { "text": "Signed in" },
149
+ "state": "visible",
150
+ "timeoutMs": 10000
151
+ }
152
+ }
134
153
  ],
135
154
  "snapshotAfter": true
136
155
  }
137
156
  ```
138
157
 
158
+ Locator fields are `css`, `testId`, `role` + `name`, `label`, `placeholder`, `text`, `match`, and zero-based `nth`. Matching is exact by default. Ghost Bridge refuses ambiguous action targets and returns compact candidates instead of silently choosing the first element.
159
+
160
+ `waitFor` supports:
161
+
162
+ - `element`: `visible`, `hidden`, `attached`, `detached`, or `enabled`
163
+ - `url`: `contains` or `equals`
164
+ - `networkIdle`: optional `idleMs`
165
+ - `expression`: a truthy JavaScript expression, including a returned Promise
166
+
167
+ Polling happens inside the extension at a short interval, so it does not create repeated model/tool turns. Each condition defaults to 10 seconds and is capped at 30 seconds. The whole batch has a hard deadline of at most 60 seconds; once reached, remaining actions are not executed. Fixed `waitMs` remains for compatibility but defaults to zero.
168
+
139
169
  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
170
 
141
171
  ```javascript
@@ -150,6 +180,8 @@ Notes:
150
180
 
151
181
  - Use `bind_tab` when a workflow spans multiple pages. For example, bind a checklist page as `cases` and a business page as `app`, then call tools with `target: "cases"` or `target: "app"`.
152
182
  - All browser tools accept an optional `target` parameter. When named targets are bound, `dispatch_action` requires `target` so refs from one page are not accidentally used on another page.
183
+ - Semantic locators traverse open Shadow DOM and readable same-origin iframes. Cross-origin iframe DOM is not accessible and is skipped explicitly.
184
+ - `get_page_content` reports iframe counters and uses contiguous `offset`/`maxLength` slices, so pagination does not duplicate or skip the hidden middle of a head/tail truncation.
153
185
  - Use `pin_current_tab` when you are debugging a page and need to switch to other tabs without changing the AI target. Use `unpin_tab` to restore the original follow-focused-tab behavior.
154
186
  - `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
155
187
 
package/dist/cli.js CHANGED
File without changes
package/dist/server.js CHANGED
@@ -28536,6 +28536,178 @@ var packageJsonPath = path.resolve(__dirname, "../package.json");
28536
28536
  var packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
28537
28537
  var GHOST_BRIDGE_VERSION = packageJson.version;
28538
28538
 
28539
+ // src/bridge-utils.js
28540
+ var DEFAULT_DISPATCH_TIMEOUT_MS = 3e4;
28541
+ var MAX_DISPATCH_TIMEOUT_MS = 6e4;
28542
+ function jsonText(data) {
28543
+ return typeof data === "string" ? data : JSON.stringify(data);
28544
+ }
28545
+ function compact(value) {
28546
+ if (Array.isArray(value)) {
28547
+ const cleaned = value.map(compact).filter((item) => item !== void 0);
28548
+ return cleaned.length ? cleaned : void 0;
28549
+ }
28550
+ if (value && typeof value === "object") {
28551
+ const cleaned = {};
28552
+ for (const [key, item] of Object.entries(value)) {
28553
+ const compacted = compact(item);
28554
+ if (compacted !== void 0) cleaned[key] = compacted;
28555
+ }
28556
+ return Object.keys(cleaned).length ? cleaned : void 0;
28557
+ }
28558
+ if (value === null || value === "") return void 0;
28559
+ return value;
28560
+ }
28561
+ function out(data) {
28562
+ return jsonText(compact(data));
28563
+ }
28564
+ function clampNumber(value, fallback, min, max) {
28565
+ const number3 = Number(value);
28566
+ if (!Number.isFinite(number3)) return fallback;
28567
+ return Math.min(max, Math.max(min, Math.round(number3)));
28568
+ }
28569
+ function buildTruncatedOutput(text, retainedChars) {
28570
+ const headLength = Math.ceil(retainedChars * 0.8);
28571
+ const tailLength = Math.max(0, retainedChars - headLength);
28572
+ const omitted = Math.max(0, text.length - headLength - tailLength);
28573
+ return out({
28574
+ truncated: true,
28575
+ originalLength: text.length,
28576
+ content: `${text.slice(0, headLength)}
28577
+ ... [\u5DF2\u7701\u7565 ${omitted} \u4E2A\u5B57\u7B26] ...
28578
+ ${tailLength ? text.slice(-tailLength) : ""}`,
28579
+ 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"
28580
+ });
28581
+ }
28582
+ function boundedOut(data, maxLength, { defaultLength = 8e3, maxAllowed = 5e4 } = {}) {
28583
+ const text = out(data) ?? "undefined";
28584
+ const limit = clampNumber(maxLength, defaultLength, 200, maxAllowed);
28585
+ if (text.length <= limit) return text;
28586
+ let low = 0;
28587
+ let high = text.length;
28588
+ let best = buildTruncatedOutput(text, 0);
28589
+ if (best.length > limit) {
28590
+ return JSON.stringify({ truncated: true });
28591
+ }
28592
+ while (low <= high) {
28593
+ const middle = Math.floor((low + high) / 2);
28594
+ const candidate = buildTruncatedOutput(text, middle);
28595
+ if (candidate.length <= limit) {
28596
+ best = candidate;
28597
+ low = middle + 1;
28598
+ } else {
28599
+ high = middle - 1;
28600
+ }
28601
+ }
28602
+ return best;
28603
+ }
28604
+ function calculateDispatchBudget(args = {}) {
28605
+ const steps = Array.isArray(args.actions) ? args.actions : [args];
28606
+ if (!steps.length || steps.length > 20) throw new Error("actions \u6570\u91CF\u5FC5\u987B\u5728 1-20 \u4E4B\u95F4");
28607
+ const totalLegacyWaitMs = steps.reduce(
28608
+ (sum, step) => sum + clampNumber(step.waitMs, 0, 0, 3e3),
28609
+ 0
28610
+ );
28611
+ const totalConditionWaitMs = steps.reduce(
28612
+ (sum, step) => sum + (step.waitFor ? clampNumber(step.waitFor.timeoutMs, 1e4, 100, 3e4) : 0),
28613
+ 0
28614
+ );
28615
+ const estimatedActionMs = steps.length * 750;
28616
+ const snapshotMs = args.snapshotAfter ? 2e3 : 0;
28617
+ const estimatedExecutionMs = 2e3 + estimatedActionMs + totalLegacyWaitMs + totalConditionWaitMs + snapshotMs;
28618
+ const explicitTimeout = args.timeoutMs === void 0 ? null : clampNumber(args.timeoutMs, DEFAULT_DISPATCH_TIMEOUT_MS, 1e3, MAX_DISPATCH_TIMEOUT_MS);
28619
+ if (explicitTimeout === null && estimatedExecutionMs > MAX_DISPATCH_TIMEOUT_MS) {
28620
+ throw new Error(
28621
+ `\u6279\u5904\u7406\u6700\u574F\u6267\u884C\u9884\u7B97 ${estimatedExecutionMs}ms \u8D85\u8FC7 ${MAX_DISPATCH_TIMEOUT_MS}ms\uFF1B\u8BF7\u51CF\u5C11\u52A8\u4F5C/\u7B49\u5F85\u6570\u91CF\u3001\u7F29\u77ED waitFor.timeoutMs\uFF0C\u6216\u663E\u5F0F\u8BBE\u7F6E\u8F83\u5C0F\u7684\u6574\u4F53 timeoutMs \u8BA9\u6279\u5904\u7406\u6309\u622A\u6B62\u65F6\u95F4\u505C\u6B62`
28622
+ );
28623
+ }
28624
+ const executionTimeoutMs = explicitTimeout ?? Math.max(5e3, Math.min(MAX_DISPATCH_TIMEOUT_MS, estimatedExecutionMs));
28625
+ return {
28626
+ steps,
28627
+ estimatedExecutionMs,
28628
+ executionTimeoutMs,
28629
+ serverTimeoutMs: executionTimeoutMs + 5e3
28630
+ };
28631
+ }
28632
+
28633
+ // src/tool-schemas.js
28634
+ var LOCATOR_REF = { $ref: "#/$defs/locator" };
28635
+ var WAIT_FOR_REF = { $ref: "#/$defs/wait" };
28636
+ var LOCATOR_DEFINITION = {
28637
+ type: "object",
28638
+ description: "\u591A\u5B57\u6BB5\u540C\u65F6\u6EE1\u8DB3\uFF1B\u9ED8\u8BA4 exact\uFF0C\u6B67\u4E49\u65F6\u7528 nth \u6216\u6536\u7A84\u6761\u4EF6\u3002",
28639
+ properties: {
28640
+ css: { type: "string", description: "CSS \u9009\u62E9\u5668" },
28641
+ testId: { type: "string", description: "data-testid" },
28642
+ role: { type: "string", description: "ARIA/\u9690\u5F0F role" },
28643
+ name: { type: "string", description: "\u53EF\u8BBF\u95EE\u540D\u79F0" },
28644
+ label: { type: "string", description: "\u8868\u5355 label/aria-label" },
28645
+ placeholder: { type: "string" },
28646
+ text: { type: "string", description: "\u5143\u7D20\u6587\u672C" },
28647
+ nth: { type: "integer", minimum: 0, description: "\u4ECE 0 \u5F00\u59CB" },
28648
+ match: { enum: ["exact", "contains"], description: "\u9ED8\u8BA4 exact" }
28649
+ }
28650
+ };
28651
+ var WAIT_FOR_DEFINITION = {
28652
+ type: "object",
28653
+ description: "\u52A8\u4F5C\u540E\u5728\u6269\u5C55\u5185\u90E8\u7B49\u5F85\uFF0C\u4E0D\u589E\u52A0\u6A21\u578B\u5F80\u8FD4\u3002",
28654
+ properties: {
28655
+ type: { enum: ["element", "url", "networkIdle", "expression"] },
28656
+ locator: { ...LOCATOR_REF, description: "element \u6761\u4EF6\u7684\u5B9A\u4F4D\u5668\uFF1B\u7701\u7565\u65F6\u590D\u7528\u52A8\u4F5C locator" },
28657
+ state: { enum: ["visible", "hidden", "attached", "detached", "enabled"], description: "element \u9ED8\u8BA4 visible" },
28658
+ contains: { type: "string", description: "URL \u5305\u542B" },
28659
+ equals: { type: "string", description: "URL \u7B49\u4E8E" },
28660
+ idleMs: { type: "number", minimum: 100, maximum: 1e4, description: "networkIdle \u7A7A\u95F2\u65F6\u95F4" },
28661
+ expression: { type: "string", description: "JS \u771F\u503C\u8868\u8FBE\u5F0F\uFF0C\u53EF\u8FD4\u56DE Promise" },
28662
+ timeoutMs: { type: "number", minimum: 100, maximum: 3e4, description: "\u9ED8\u8BA4 10000" }
28663
+ },
28664
+ required: ["type"]
28665
+ };
28666
+ var ACTION_PROPERTIES = {
28667
+ ref: { type: "string" },
28668
+ selector: { type: "string" },
28669
+ locator: LOCATOR_REF,
28670
+ action: { enum: ["click", "fill", "press", "scroll", "select", "hover", "focus"] },
28671
+ value: { type: "string" },
28672
+ key: { type: "string" },
28673
+ deltaX: { type: "number" },
28674
+ deltaY: { type: "number" },
28675
+ waitMs: { type: "number", minimum: 0, maximum: 3e3 },
28676
+ waitFor: WAIT_FOR_REF
28677
+ };
28678
+ var ACTION_DEFINITION = {
28679
+ type: "object",
28680
+ properties: ACTION_PROPERTIES,
28681
+ required: ["action"]
28682
+ };
28683
+ var DISPATCH_ACTION_TOOL = {
28684
+ name: "dispatch_action",
28685
+ description: "\u5B9A\u4F4D\u2192\u64CD\u4F5C\u2192\u7B49\u5F85\u2192\u5FEB\u7167\u4E00\u6B21\u5B8C\u6210\u3002\u5355\u52A8\u4F20 ref/selector/locator+action\uFF0C\u6279\u91CF\u4F20 actions\uFF1Bfill/select \u7528 value\uFF0Cpress \u7528 key\u3002",
28686
+ inputSchema: {
28687
+ type: "object",
28688
+ $defs: {
28689
+ locator: LOCATOR_DEFINITION,
28690
+ wait: WAIT_FOR_DEFINITION,
28691
+ action: ACTION_DEFINITION
28692
+ },
28693
+ properties: {
28694
+ target: { type: "string" },
28695
+ ...ACTION_PROPERTIES,
28696
+ timeoutMs: { type: "number", minimum: 1e3, maximum: 6e4 },
28697
+ actions: {
28698
+ type: "array",
28699
+ minItems: 1,
28700
+ maxItems: 20,
28701
+ items: { $ref: "#/$defs/action" }
28702
+ },
28703
+ stopOnError: { type: "boolean" },
28704
+ snapshotAfter: { type: "boolean" },
28705
+ snapshotSelector: { type: "string" },
28706
+ snapshotMaxElements: { type: "number" }
28707
+ }
28708
+ }
28709
+ };
28710
+
28539
28711
  // src/server.js
28540
28712
  var BASE_PORT = Number(process.env.GHOST_BRIDGE_PORT || 33333);
28541
28713
  var DEFAULT_WS_TOKEN = "ghost-bridge-local";
@@ -29130,50 +29302,6 @@ async function askChrome(command, params = {}, options = {}) {
29130
29302
  });
29131
29303
  });
29132
29304
  }
29133
- function jsonText(data) {
29134
- return typeof data === "string" ? data : JSON.stringify(data);
29135
- }
29136
- function compact(value) {
29137
- if (Array.isArray(value)) {
29138
- const cleaned = value.map(compact).filter((v) => v !== void 0);
29139
- return cleaned.length ? cleaned : void 0;
29140
- }
29141
- if (value && typeof value === "object") {
29142
- const cleaned = {};
29143
- for (const [k, v] of Object.entries(value)) {
29144
- const c = compact(v);
29145
- if (c !== void 0) cleaned[k] = c;
29146
- }
29147
- return Object.keys(cleaned).length ? cleaned : void 0;
29148
- }
29149
- if (value === null || value === "") return void 0;
29150
- return value;
29151
- }
29152
- function out(data) {
29153
- return jsonText(compact(data));
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
29305
  function truncateUrl(url2, maxLen = 200) {
29178
29306
  if (typeof url2 !== "string" || url2.length <= maxLen) return url2;
29179
29307
  return url2.slice(0, maxLen) + "\u2026";
@@ -29568,7 +29696,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
29568
29696
  },
29569
29697
  {
29570
29698
  name: "get_interactive_snapshot",
29571
- description: "\u626B\u63CF\u53EF\u89C1\u53EF\u4EA4\u4E92\u5143\u7D20\uFF0C\u8FD4\u56DE\u5E26 ref \u7684\u7CBE\u7B80\u5217\u8868\u4F9B dispatch_action \u4F7F\u7528\uFF08\u5148\u5FEB\u7167\u540E\u64CD\u4F5C\uFF09\u3002\u9ED8\u8BA4 30 \u4E2A\uFF0C\u9700\u8981\u66F4\u591A\u4F20 maxElements \u6216\u7528 selector \u6536\u7A84\u3002\u652F\u6301 Shadow DOM\u3002",
29699
+ description: "\u5F53\u65E0\u6CD5\u76F4\u63A5\u5199\u51FA\u8BED\u4E49 locator \u65F6\uFF0C\u626B\u63CF\u53EF\u89C1\u53EF\u4EA4\u4E92\u5143\u7D20\u5E76\u8FD4\u56DE ref\u3002\u9ED8\u8BA4 30 \u4E2A\uFF0C\u9700\u8981\u66F4\u591A\u4F20 maxElements \u6216\u7528 selector \u6536\u7A84\u3002\u652F\u6301 Shadow DOM\u3002",
29572
29700
  inputSchema: {
29573
29701
  type: "object",
29574
29702
  properties: {
@@ -29588,83 +29716,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
29588
29716
  }
29589
29717
  }
29590
29718
  },
29591
- {
29592
- name: "dispatch_action",
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",
29594
- inputSchema: {
29595
- type: "object",
29596
- properties: {
29597
- target: TARGET_ARG,
29598
- ref: {
29599
- type: "string",
29600
- description: "\u5143\u7D20 ref\uFF0C\u5982 'e1'\uFF08\u6765\u81EA get_interactive_snapshot\uFF09"
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
- },
29606
- action: {
29607
- type: "string",
29608
- enum: ["click", "fill", "press", "scroll", "select", "hover", "focus"]
29609
- },
29610
- value: {
29611
- type: "string",
29612
- description: "fill \u7684\u6587\u672C / select \u7684 option value"
29613
- },
29614
- key: {
29615
- type: "string",
29616
- description: "press \u6309\u952E\uFF0C\u9ED8\u8BA4 'Enter'"
29617
- },
29618
- deltaX: {
29619
- type: "number",
29620
- description: "\u6C34\u5E73\u6EDA\u52A8\u91CF\uFF0C\u9ED8\u8BA4 0"
29621
- },
29622
- deltaY: {
29623
- type: "number",
29624
- description: "\u5782\u76F4\u6EDA\u52A8\u91CF\uFF0C\u9ED8\u8BA4 300\uFF08\u6B63\u6570\u5411\u4E0B\uFF09"
29625
- },
29626
- waitMs: {
29627
- type: "number",
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"
29664
- }
29665
- }
29666
- }
29667
- }
29719
+ DISPATCH_ACTION_TOOL
29668
29720
  ]
29669
29721
  }));
29670
29722
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -29704,7 +29756,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
29704
29756
  summary,
29705
29757
  page,
29706
29758
  interactive,
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"
29759
+ nextStepHint: "\u89C6\u89C9\u7528 capture_screenshot\uFF1B\u70B9\u51FB/\u8F93\u5165\u4F18\u5148\u7528 dispatch_action \u8BED\u4E49 locator\uFF0C\u5F53\u524D refs \u4E5F\u53EF\u76F4\u63A5\u4F7F\u7528\uFF1B\u8BF7\u6C42/\u6027\u80FD\u7528 list_network_requests / perf_metrics\u3002"
29708
29760
  } : {
29709
29761
  summary,
29710
29762
  interactive: elements ? { viewport: interactive?.viewport, elements } : void 0,
@@ -29924,6 +29976,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
29924
29976
  }
29925
29977
  if (name === "get_page_content") {
29926
29978
  const { target, mode = "text", selector, maxLength = 8e3, offset = 0, includeMetadata = true } = args;
29979
+ const safeMaxLength = clampNumber(maxLength, 8e3, 1, 5e4);
29980
+ const safeOffset = clampNumber(offset, 0, 0, 1e8);
29927
29981
  const validModes = ["text", "html", "structured"];
29928
29982
  if (mode && !validModes.includes(mode)) {
29929
29983
  return {
@@ -29933,22 +29987,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
29933
29987
  }]
29934
29988
  };
29935
29989
  }
29936
- if (offset > 0 && mode === "text") {
29937
- const selectorExpr = selector ? `(document.querySelector(${JSON.stringify(selector)}) || document.body)` : "document.body";
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}}})()`;
29939
- const res2 = await askChrome("eval", { target, code });
29940
- return { content: [{ type: "text", text: out(res2) }] };
29941
- }
29942
- if (offset > 0 && mode === "html") {
29943
- const res2 = await askChrome("getPageContent", { target, mode, selector, maxLength: offset + maxLength, includeMetadata: false });
29944
- if (typeof res2?.content === "string") {
29945
- res2.content = res2.content.slice(offset, offset + maxLength);
29946
- res2.offset = offset;
29947
- res2.hasMore = (res2.contentLength || 0) > offset + maxLength;
29948
- }
29949
- return { content: [{ type: "text", text: out(res2) }] };
29950
- }
29951
- const res = await askChrome("getPageContent", { target, mode, selector, maxLength, includeMetadata });
29990
+ const res = await askChrome("getPageContent", {
29991
+ target,
29992
+ mode,
29993
+ selector,
29994
+ maxLength: safeMaxLength,
29995
+ offset: safeOffset,
29996
+ includeMetadata
29997
+ });
29952
29998
  if (typeof res?.metadata?.url === "string" && res.metadata.url.length > 200) {
29953
29999
  res.metadata.url = truncateUrl(res.metadata.url);
29954
30000
  res.metadata.urlTruncated = true;
@@ -29961,11 +30007,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
29961
30007
  return { content: [{ type: "text", text: out(res) }] };
29962
30008
  }
29963
30009
  if (name === "dispatch_action") {
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 });
30010
+ const budget = calculateDispatchBudget(args);
30011
+ const res = await askChrome(
30012
+ "dispatchAction",
30013
+ { ...args, timeoutMs: budget.executionTimeoutMs },
30014
+ { timeoutMs: budget.serverTimeoutMs }
30015
+ );
29969
30016
  return { content: [{ type: "text", text: out(res) }] };
29970
30017
  }
29971
30018
  return { content: [{ type: "text", text: `\u672A\u77E5\u5DE5\u5177\uFF1A${name}` }] };