ghost-bridge 1.0.2 → 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 +30 -3
- package/dist/server.js +140 -16
- package/extension/background.js +179 -34
- package/extension/bg-dom.js +18 -1
- package/extension/manifest.json +4 -3
- package/package.json +1 -1
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`
|
|
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/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\
|
|
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: "
|
|
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\
|
|
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: {
|
|
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\
|
|
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: "\
|
|
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 =
|
|
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
|
-
|
|
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
|
|
29762
|
-
|
|
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})
|
|
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
|
|
29844
|
-
|
|
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}` }] };
|
package/extension/background.js
CHANGED
|
@@ -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
|
|
993
|
-
|
|
994
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
1387
|
-
if (!el) return { error: '
|
|
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(`元素 ${
|
|
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 = `已点击 ${
|
|
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(
|
|
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(
|
|
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 = `已在 ${
|
|
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(
|
|
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 = `已在 ${
|
|
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 = `已在 ${
|
|
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(
|
|
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 = `已在 ${
|
|
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 = `已将鼠标悬停到 ${
|
|
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(
|
|
1637
|
+
const el = document.querySelector(${locatorExpression});
|
|
1511
1638
|
if (el) el.focus();
|
|
1512
1639
|
})()`,
|
|
1513
1640
|
})
|
|
1514
|
-
actionResult.detail = `已聚焦到 ${
|
|
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
|
-
|
|
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
|
-
|
|
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 秒周期心跳超时
|
package/extension/bg-dom.js
CHANGED
|
@@ -299,9 +299,26 @@
|
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
if (mode === 'text') {
|
|
302
|
-
|
|
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;
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Ghost Bridge",
|
|
4
|
-
"version": "1.0
|
|
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
|
+
}
|