bb-browser 0.3.0 → 0.4.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 +106 -130
- package/README.zh-CN.md +169 -0
- package/dist/chunk-CWLDHQGR.js +60 -0
- package/dist/chunk-CWLDHQGR.js.map +1 -0
- package/dist/cli.js +4205 -22
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +1 -1
- package/dist/mcp.js +21240 -0
- package/dist/mcp.js.map +1 -0
- package/extension/dist/background.js +700 -543
- package/extension/dist/background.js.map +1 -1
- package/extension/dist/manifest.json +1 -1
- package/package.json +12 -7
- package/dist/chunk-TUO443YI.js +0 -21
- package/dist/chunk-TUO443YI.js.map +0 -1
|
@@ -216,7 +216,11 @@ const consoleMessages = /* @__PURE__ */ new Map();
|
|
|
216
216
|
const jsErrors = /* @__PURE__ */ new Map();
|
|
217
217
|
const networkRoutes = /* @__PURE__ */ new Map();
|
|
218
218
|
const networkEnabledTabs = /* @__PURE__ */ new Set();
|
|
219
|
+
const networkBodyBytes = /* @__PURE__ */ new Map();
|
|
219
220
|
const MAX_REQUESTS = 500;
|
|
221
|
+
const MAX_REQUEST_BODY_BYTES = 64 * 1024;
|
|
222
|
+
const MAX_RESPONSE_BODY_BYTES = 256 * 1024;
|
|
223
|
+
const MAX_TAB_BODY_BYTES = 8 * 1024 * 1024;
|
|
220
224
|
const MAX_CONSOLE_MESSAGES = 500;
|
|
221
225
|
const MAX_ERRORS = 100;
|
|
222
226
|
async function ensureAttached(tabId) {
|
|
@@ -256,6 +260,41 @@ async function evaluate(tabId, expression, options = {}) {
|
|
|
256
260
|
}
|
|
257
261
|
return result.result?.value;
|
|
258
262
|
}
|
|
263
|
+
async function callFunctionOn(tabId, objectId, functionDeclaration, args = []) {
|
|
264
|
+
const result = await sendCommand(tabId, "Runtime.callFunctionOn", {
|
|
265
|
+
objectId,
|
|
266
|
+
functionDeclaration,
|
|
267
|
+
arguments: args.map((arg) => ({ value: arg })),
|
|
268
|
+
returnByValue: true,
|
|
269
|
+
awaitPromise: true
|
|
270
|
+
});
|
|
271
|
+
if (result.exceptionDetails) {
|
|
272
|
+
throw new Error(result.exceptionDetails.exception?.description || "Call failed");
|
|
273
|
+
}
|
|
274
|
+
return result.result?.value;
|
|
275
|
+
}
|
|
276
|
+
async function getDocument(tabId, options = {}) {
|
|
277
|
+
const result = await sendCommand(tabId, "DOM.getDocument", {
|
|
278
|
+
depth: options.depth ?? -1,
|
|
279
|
+
// -1 表示获取整个树
|
|
280
|
+
pierce: options.pierce ?? true
|
|
281
|
+
// 穿透 shadow DOM 和 iframe
|
|
282
|
+
});
|
|
283
|
+
return result.root;
|
|
284
|
+
}
|
|
285
|
+
async function querySelector(tabId, nodeId, selector) {
|
|
286
|
+
const result = await sendCommand(tabId, "DOM.querySelector", {
|
|
287
|
+
nodeId,
|
|
288
|
+
selector
|
|
289
|
+
});
|
|
290
|
+
return result.nodeId;
|
|
291
|
+
}
|
|
292
|
+
async function resolveNodeByBackendId(tabId, backendNodeId) {
|
|
293
|
+
const result = await sendCommand(tabId, "DOM.resolveNode", {
|
|
294
|
+
backendNodeId
|
|
295
|
+
});
|
|
296
|
+
return result.object.objectId;
|
|
297
|
+
}
|
|
259
298
|
async function dispatchMouseEvent(tabId, type, x, y, options = {}) {
|
|
260
299
|
await sendCommand(tabId, "Input.dispatchMouseEvent", {
|
|
261
300
|
type,
|
|
@@ -335,6 +374,32 @@ async function handleJavaScriptDialog(tabId, accept, promptText) {
|
|
|
335
374
|
function getPendingDialog(tabId) {
|
|
336
375
|
return pendingDialogs.get(tabId);
|
|
337
376
|
}
|
|
377
|
+
async function getFullAccessibilityTree(tabId, options = {}) {
|
|
378
|
+
await sendCommand(tabId, "Accessibility.enable");
|
|
379
|
+
const result = await sendCommand(
|
|
380
|
+
tabId,
|
|
381
|
+
"Accessibility.getFullAXTree",
|
|
382
|
+
{
|
|
383
|
+
depth: options.depth,
|
|
384
|
+
frameId: options.frameId
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
return result.nodes;
|
|
388
|
+
}
|
|
389
|
+
async function getPartialAccessibilityTree(tabId, nodeId, backendNodeId, options = {}) {
|
|
390
|
+
await sendCommand(tabId, "Accessibility.enable");
|
|
391
|
+
const result = await sendCommand(
|
|
392
|
+
tabId,
|
|
393
|
+
"Accessibility.getPartialAXTree",
|
|
394
|
+
{
|
|
395
|
+
nodeId,
|
|
396
|
+
backendNodeId,
|
|
397
|
+
fetchRelatives: options.fetchRelatives ?? true,
|
|
398
|
+
depth: options.depth
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
return result.nodes;
|
|
402
|
+
}
|
|
338
403
|
async function enableNetwork(tabId) {
|
|
339
404
|
if (networkEnabledTabs.has(tabId)) return;
|
|
340
405
|
await ensureAttached(tabId);
|
|
@@ -346,18 +411,34 @@ async function enableNetwork(tabId) {
|
|
|
346
411
|
if (!networkRequests.has(tabId)) {
|
|
347
412
|
networkRequests.set(tabId, []);
|
|
348
413
|
}
|
|
414
|
+
if (!networkBodyBytes.has(tabId)) {
|
|
415
|
+
networkBodyBytes.set(tabId, 0);
|
|
416
|
+
}
|
|
349
417
|
console.log("[CDPService] Network enabled for tab:", tabId);
|
|
350
418
|
}
|
|
351
|
-
function getNetworkRequests(tabId, filter) {
|
|
419
|
+
function getNetworkRequests(tabId, filter, withBody = false) {
|
|
352
420
|
const requests = networkRequests.get(tabId) || [];
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
return requests.filter(
|
|
356
|
-
(r) => r.url.toLowerCase().includes(lowerFilter) || r.method.toLowerCase().includes(lowerFilter) || r.type.toLowerCase().includes(lowerFilter)
|
|
421
|
+
const filtered = !filter ? requests : requests.filter(
|
|
422
|
+
(r) => r.url.toLowerCase().includes(filter.toLowerCase()) || r.method.toLowerCase().includes(filter.toLowerCase()) || r.type.toLowerCase().includes(filter.toLowerCase())
|
|
357
423
|
);
|
|
424
|
+
if (withBody) return filtered;
|
|
425
|
+
return filtered.map((r) => ({
|
|
426
|
+
requestId: r.requestId,
|
|
427
|
+
url: r.url,
|
|
428
|
+
method: r.method,
|
|
429
|
+
type: r.type,
|
|
430
|
+
timestamp: r.timestamp,
|
|
431
|
+
response: r.response ? {
|
|
432
|
+
status: r.response.status,
|
|
433
|
+
statusText: r.response.statusText
|
|
434
|
+
} : void 0,
|
|
435
|
+
failed: r.failed,
|
|
436
|
+
failureReason: r.failureReason
|
|
437
|
+
}));
|
|
358
438
|
}
|
|
359
439
|
function clearNetworkRequests(tabId) {
|
|
360
440
|
networkRequests.set(tabId, []);
|
|
441
|
+
networkBodyBytes.set(tabId, 0);
|
|
361
442
|
}
|
|
362
443
|
async function addNetworkRoute(tabId, urlPattern, options = {}) {
|
|
363
444
|
await enableNetwork(tabId);
|
|
@@ -428,6 +509,8 @@ function initEventListeners() {
|
|
|
428
509
|
handleNetworkResponse(tabId, params);
|
|
429
510
|
} else if (method === "Network.loadingFailed") {
|
|
430
511
|
handleNetworkFailed(tabId, params);
|
|
512
|
+
} else if (method === "Network.loadingFinished") {
|
|
513
|
+
void handleNetworkLoadingFinished(tabId, params);
|
|
431
514
|
} else if (method === "Fetch.requestPaused") {
|
|
432
515
|
handleFetchPaused(tabId, params);
|
|
433
516
|
} else if (method === "Runtime.consoleAPICalled") {
|
|
@@ -454,24 +537,69 @@ function cleanupTab$2(tabId) {
|
|
|
454
537
|
networkRequests.delete(tabId);
|
|
455
538
|
networkRoutes.delete(tabId);
|
|
456
539
|
networkEnabledTabs.delete(tabId);
|
|
540
|
+
networkBodyBytes.delete(tabId);
|
|
457
541
|
consoleMessages.delete(tabId);
|
|
458
542
|
jsErrors.delete(tabId);
|
|
459
543
|
}
|
|
544
|
+
function estimateBodyBytes(value) {
|
|
545
|
+
return value ? value.length * 2 : 0;
|
|
546
|
+
}
|
|
547
|
+
function truncateBody(value, maxBytes) {
|
|
548
|
+
const maxChars = Math.max(0, Math.floor(maxBytes / 2));
|
|
549
|
+
if (value.length <= maxChars) {
|
|
550
|
+
return { body: value, truncated: false };
|
|
551
|
+
}
|
|
552
|
+
return { body: value.slice(0, maxChars), truncated: true };
|
|
553
|
+
}
|
|
554
|
+
function getStoredBodyBytes(request) {
|
|
555
|
+
return estimateBodyBytes(request.requestBody) + estimateBodyBytes(request.response?.body);
|
|
556
|
+
}
|
|
557
|
+
function updateTabBodyBytes(tabId) {
|
|
558
|
+
const requests = networkRequests.get(tabId) || [];
|
|
559
|
+
let total = 0;
|
|
560
|
+
for (const request of requests) {
|
|
561
|
+
total += getStoredBodyBytes(request);
|
|
562
|
+
}
|
|
563
|
+
networkBodyBytes.set(tabId, total);
|
|
564
|
+
}
|
|
565
|
+
function enforceBodyBudget(tabId) {
|
|
566
|
+
const requests = networkRequests.get(tabId) || [];
|
|
567
|
+
let total = networkBodyBytes.get(tabId) || 0;
|
|
568
|
+
for (const request of requests) {
|
|
569
|
+
if (total <= MAX_TAB_BODY_BYTES) break;
|
|
570
|
+
if (request.requestBody) {
|
|
571
|
+
total -= estimateBodyBytes(request.requestBody);
|
|
572
|
+
delete request.requestBody;
|
|
573
|
+
request.requestBodyTruncated = true;
|
|
574
|
+
}
|
|
575
|
+
if (total <= MAX_TAB_BODY_BYTES) break;
|
|
576
|
+
if (request.response?.body) {
|
|
577
|
+
total -= estimateBodyBytes(request.response.body);
|
|
578
|
+
delete request.response.body;
|
|
579
|
+
request.response.bodyTruncated = true;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
networkBodyBytes.set(tabId, Math.max(0, total));
|
|
583
|
+
}
|
|
460
584
|
function handleNetworkRequest(tabId, params) {
|
|
461
585
|
const requests = networkRequests.get(tabId) || [];
|
|
462
586
|
if (requests.length >= MAX_REQUESTS) {
|
|
463
587
|
requests.shift();
|
|
464
588
|
}
|
|
589
|
+
const truncatedRequestBody = params.request.postData ? truncateBody(params.request.postData, MAX_REQUEST_BODY_BYTES) : void 0;
|
|
465
590
|
requests.push({
|
|
466
591
|
requestId: params.requestId,
|
|
467
592
|
url: params.request.url,
|
|
468
593
|
method: params.request.method,
|
|
469
594
|
type: params.type,
|
|
470
595
|
timestamp: params.timestamp * 1e3,
|
|
471
|
-
|
|
472
|
-
|
|
596
|
+
requestHeaders: params.request.headers,
|
|
597
|
+
requestBody: truncatedRequestBody?.body,
|
|
598
|
+
requestBodyTruncated: truncatedRequestBody?.truncated
|
|
473
599
|
});
|
|
474
600
|
networkRequests.set(tabId, requests);
|
|
601
|
+
updateTabBodyBytes(tabId);
|
|
602
|
+
enforceBodyBudget(tabId);
|
|
475
603
|
}
|
|
476
604
|
function handleNetworkResponse(tabId, params) {
|
|
477
605
|
const requests = networkRequests.get(tabId) || [];
|
|
@@ -481,10 +609,38 @@ function handleNetworkResponse(tabId, params) {
|
|
|
481
609
|
status: params.response.status,
|
|
482
610
|
statusText: params.response.statusText,
|
|
483
611
|
headers: params.response.headers,
|
|
484
|
-
mimeType: params.response.mimeType
|
|
612
|
+
mimeType: params.response.mimeType,
|
|
613
|
+
body: request.response?.body,
|
|
614
|
+
bodyBase64: request.response?.bodyBase64,
|
|
615
|
+
bodyTruncated: request.response?.bodyTruncated
|
|
485
616
|
};
|
|
486
617
|
}
|
|
487
618
|
}
|
|
619
|
+
async function handleNetworkLoadingFinished(tabId, params) {
|
|
620
|
+
const requests = networkRequests.get(tabId) || [];
|
|
621
|
+
const request = requests.find((r) => r.requestId === params.requestId);
|
|
622
|
+
if (!request || request.failed) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
const result = await sendCommand(tabId, "Network.getResponseBody", { requestId: params.requestId });
|
|
627
|
+
const truncatedResponseBody = truncateBody(result.body, MAX_RESPONSE_BODY_BYTES);
|
|
628
|
+
request.response = {
|
|
629
|
+
status: request.response?.status ?? 0,
|
|
630
|
+
statusText: request.response?.statusText ?? "",
|
|
631
|
+
headers: request.response?.headers,
|
|
632
|
+
mimeType: request.response?.mimeType,
|
|
633
|
+
body: truncatedResponseBody.body,
|
|
634
|
+
bodyBase64: result.base64Encoded,
|
|
635
|
+
bodyTruncated: truncatedResponseBody.truncated
|
|
636
|
+
};
|
|
637
|
+
request.bodyError = void 0;
|
|
638
|
+
updateTabBodyBytes(tabId);
|
|
639
|
+
enforceBodyBudget(tabId);
|
|
640
|
+
} catch (error) {
|
|
641
|
+
request.bodyError = error instanceof Error ? error.message : String(error);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
488
644
|
function handleNetworkFailed(tabId, params) {
|
|
489
645
|
const requests = networkRequests.get(tabId) || [];
|
|
490
646
|
const request = requests.find((r) => r.requestId === params.requestId);
|
|
@@ -600,326 +756,264 @@ function handleException(tabId, params) {
|
|
|
600
756
|
jsErrors.set(tabId, errors);
|
|
601
757
|
}
|
|
602
758
|
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
759
|
+
const INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
760
|
+
"button",
|
|
761
|
+
"link",
|
|
762
|
+
"textbox",
|
|
763
|
+
"searchbox",
|
|
764
|
+
"combobox",
|
|
765
|
+
"listbox",
|
|
766
|
+
"checkbox",
|
|
767
|
+
"radio",
|
|
768
|
+
"slider",
|
|
769
|
+
"spinbutton",
|
|
770
|
+
"switch",
|
|
771
|
+
"tab",
|
|
772
|
+
"menuitem",
|
|
773
|
+
"menuitemcheckbox",
|
|
774
|
+
"menuitemradio",
|
|
775
|
+
"option",
|
|
776
|
+
"treeitem"
|
|
777
|
+
]);
|
|
778
|
+
const SKIP_ROLES = /* @__PURE__ */ new Set([
|
|
779
|
+
"none",
|
|
780
|
+
"InlineTextBox",
|
|
781
|
+
"LineBreak",
|
|
782
|
+
"Ignored"
|
|
783
|
+
]);
|
|
784
|
+
const CONTENT_ROLES_WITH_REF = /* @__PURE__ */ new Set([
|
|
785
|
+
"heading",
|
|
786
|
+
"img",
|
|
787
|
+
"cell",
|
|
788
|
+
"columnheader",
|
|
789
|
+
"rowheader"
|
|
790
|
+
]);
|
|
791
|
+
function createRoleNameTracker() {
|
|
792
|
+
const counts = /* @__PURE__ */ new Map();
|
|
793
|
+
const refsByKey = /* @__PURE__ */ new Map();
|
|
794
|
+
return {
|
|
795
|
+
counts,
|
|
796
|
+
refsByKey,
|
|
797
|
+
getKey(role, name) {
|
|
798
|
+
return `${role}:${name ?? ""}`;
|
|
799
|
+
},
|
|
800
|
+
getNextIndex(role, name) {
|
|
801
|
+
const key = this.getKey(role, name);
|
|
802
|
+
const current = counts.get(key) ?? 0;
|
|
803
|
+
counts.set(key, current + 1);
|
|
804
|
+
return current;
|
|
805
|
+
},
|
|
806
|
+
trackRef(role, name, ref) {
|
|
807
|
+
const key = this.getKey(role, name);
|
|
808
|
+
const refs = refsByKey.get(key) ?? [];
|
|
809
|
+
refs.push(ref);
|
|
810
|
+
refsByKey.set(key, refs);
|
|
811
|
+
},
|
|
812
|
+
getDuplicateKeys() {
|
|
813
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
814
|
+
for (const [key, refs] of refsByKey) {
|
|
815
|
+
if (refs.length > 1) duplicates.add(key);
|
|
816
|
+
}
|
|
817
|
+
return duplicates;
|
|
818
|
+
}
|
|
819
|
+
};
|
|
629
820
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
});
|
|
637
|
-
const isInjected = checkResults[0]?.result;
|
|
638
|
-
if (isInjected) {
|
|
639
|
-
return;
|
|
821
|
+
function removeNthFromNonDuplicates(refs, tracker) {
|
|
822
|
+
const duplicateKeys = tracker.getDuplicateKeys();
|
|
823
|
+
for (const refInfo of Object.values(refs)) {
|
|
824
|
+
const key = tracker.getKey(refInfo.role, refInfo.name);
|
|
825
|
+
if (!duplicateKeys.has(key)) {
|
|
826
|
+
delete refInfo.nth;
|
|
640
827
|
}
|
|
641
|
-
await chrome.scripting.executeScript({
|
|
642
|
-
target,
|
|
643
|
-
files: ["buildDomTree.js"]
|
|
644
|
-
});
|
|
645
|
-
} catch (error) {
|
|
646
|
-
console.error("[DOMService] Failed to inject buildDomTree script:", error);
|
|
647
|
-
throw new Error(`Failed to inject script: ${error instanceof Error ? error.message : String(error)}`);
|
|
648
828
|
}
|
|
649
829
|
}
|
|
650
|
-
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
target,
|
|
654
|
-
func: (args) => {
|
|
655
|
-
return window.buildDomTree(args);
|
|
656
|
-
},
|
|
657
|
-
args: [{
|
|
658
|
-
showHighlightElements: true,
|
|
659
|
-
focusHighlightIndex: -1,
|
|
660
|
-
viewportExpansion: -1,
|
|
661
|
-
// -1 = 全页面模式,不限制视口
|
|
662
|
-
debugMode: false,
|
|
663
|
-
startId: 0,
|
|
664
|
-
startHighlightIndex: 0
|
|
665
|
-
}]
|
|
666
|
-
});
|
|
667
|
-
const result = results[0]?.result;
|
|
668
|
-
if (!result || !result.map || !result.rootId) {
|
|
669
|
-
throw new Error("Failed to build DOM tree: invalid result structure");
|
|
670
|
-
}
|
|
671
|
-
return result;
|
|
830
|
+
function getProperty(node, propName) {
|
|
831
|
+
const prop = node.properties?.find((p) => p.name === propName);
|
|
832
|
+
return prop?.value?.value;
|
|
672
833
|
}
|
|
673
|
-
function
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if (role) return role;
|
|
677
|
-
const roleMap = {
|
|
678
|
-
a: "link",
|
|
679
|
-
button: "button",
|
|
680
|
-
input: getInputRole(node),
|
|
681
|
-
select: "combobox",
|
|
682
|
-
textarea: "textbox",
|
|
683
|
-
img: "image",
|
|
684
|
-
nav: "navigation",
|
|
685
|
-
main: "main",
|
|
686
|
-
header: "banner",
|
|
687
|
-
footer: "contentinfo",
|
|
688
|
-
aside: "complementary",
|
|
689
|
-
form: "form",
|
|
690
|
-
table: "table",
|
|
691
|
-
ul: "list",
|
|
692
|
-
ol: "list",
|
|
693
|
-
li: "listitem",
|
|
694
|
-
h1: "heading",
|
|
695
|
-
h2: "heading",
|
|
696
|
-
h3: "heading",
|
|
697
|
-
h4: "heading",
|
|
698
|
-
h5: "heading",
|
|
699
|
-
h6: "heading",
|
|
700
|
-
dialog: "dialog",
|
|
701
|
-
article: "article",
|
|
702
|
-
section: "region",
|
|
703
|
-
label: "label",
|
|
704
|
-
details: "group",
|
|
705
|
-
summary: "button"
|
|
706
|
-
};
|
|
707
|
-
return roleMap[tagName] || tagName;
|
|
708
|
-
}
|
|
709
|
-
function getInputRole(node) {
|
|
710
|
-
const type = node.attributes?.type?.toLowerCase() || "text";
|
|
711
|
-
const inputRoleMap = {
|
|
712
|
-
text: "textbox",
|
|
713
|
-
password: "textbox",
|
|
714
|
-
email: "textbox",
|
|
715
|
-
url: "textbox",
|
|
716
|
-
tel: "textbox",
|
|
717
|
-
search: "searchbox",
|
|
718
|
-
number: "spinbutton",
|
|
719
|
-
range: "slider",
|
|
720
|
-
checkbox: "checkbox",
|
|
721
|
-
radio: "radio",
|
|
722
|
-
button: "button",
|
|
723
|
-
submit: "button",
|
|
724
|
-
reset: "button",
|
|
725
|
-
file: "button"
|
|
726
|
-
};
|
|
727
|
-
return inputRoleMap[type] || "textbox";
|
|
728
|
-
}
|
|
729
|
-
function getAccessibleName(node, nodeMap) {
|
|
730
|
-
const attrs = node.attributes || {};
|
|
731
|
-
if (attrs["aria-label"]) return attrs["aria-label"];
|
|
732
|
-
if (attrs.title) return attrs.title;
|
|
733
|
-
if (attrs.placeholder) return attrs.placeholder;
|
|
734
|
-
if (attrs.alt) return attrs.alt;
|
|
735
|
-
if (attrs.value) return attrs.value;
|
|
736
|
-
const textContent = collectTextContent(node, nodeMap);
|
|
737
|
-
if (textContent) return textContent;
|
|
738
|
-
if (attrs.name) return attrs.name;
|
|
739
|
-
return void 0;
|
|
740
|
-
}
|
|
741
|
-
function collectTextContent(node, nodeMap, maxDepth = 5) {
|
|
742
|
-
const texts = [];
|
|
743
|
-
function collect(nodeId, depth) {
|
|
744
|
-
if (depth > maxDepth) return;
|
|
745
|
-
const currentNode = nodeMap[nodeId];
|
|
746
|
-
if (!currentNode) return;
|
|
747
|
-
if ("type" in currentNode && currentNode.type === "TEXT_NODE") {
|
|
748
|
-
const text = currentNode.text.trim();
|
|
749
|
-
if (text) texts.push(text);
|
|
750
|
-
return;
|
|
751
|
-
}
|
|
752
|
-
const elementNode = currentNode;
|
|
753
|
-
for (const childId of elementNode.children || []) {
|
|
754
|
-
collect(childId, depth + 1);
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
for (const childId of node.children || []) {
|
|
758
|
-
collect(childId, 0);
|
|
759
|
-
}
|
|
760
|
-
return texts.join(" ").trim();
|
|
834
|
+
function truncate(text, max = 80) {
|
|
835
|
+
if (text.length <= max) return text;
|
|
836
|
+
return text.slice(0, max - 3) + "...";
|
|
761
837
|
}
|
|
762
|
-
function
|
|
763
|
-
|
|
764
|
-
return text.slice(0, maxLength - 3) + "...";
|
|
838
|
+
function indent(depth) {
|
|
839
|
+
return " ".repeat(depth);
|
|
765
840
|
}
|
|
766
|
-
function
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
const ancestor = nodeMap[ancestorId];
|
|
770
|
-
if (!ancestor || "type" in ancestor) return false;
|
|
771
|
-
const ancestorElement = ancestor;
|
|
772
|
-
const elementNode = descendant;
|
|
773
|
-
if (!ancestorElement.xpath || !elementNode.xpath) return false;
|
|
774
|
-
return elementNode.xpath.startsWith(ancestorElement.xpath + "/");
|
|
841
|
+
function getIndentLevel(line) {
|
|
842
|
+
const match = line.match(/^(\s*)/);
|
|
843
|
+
return match ? Math.floor(match[1].length / 2) : 0;
|
|
775
844
|
}
|
|
776
|
-
function
|
|
845
|
+
function formatAXTree(nodes, urlMap, options = {}) {
|
|
846
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
847
|
+
for (const node of nodes) {
|
|
848
|
+
nodeMap.set(node.nodeId, node);
|
|
849
|
+
}
|
|
850
|
+
const rootNode = nodes[0];
|
|
851
|
+
if (!rootNode) {
|
|
852
|
+
return { snapshot: "(empty)", refs: {} };
|
|
853
|
+
}
|
|
777
854
|
const lines = [];
|
|
778
855
|
const refs = {};
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
if (
|
|
786
|
-
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
interactiveNodes.sort((a, b) => (a.node.highlightIndex ?? 0) - (b.node.highlightIndex ?? 0));
|
|
790
|
-
const nodeIdToInfo = /* @__PURE__ */ new Map();
|
|
791
|
-
for (const { id, node } of interactiveNodes) {
|
|
792
|
-
nodeIdToInfo.set(id, {
|
|
793
|
-
name: getAccessibleName(node, map),
|
|
794
|
-
role: getRole(node)
|
|
795
|
-
});
|
|
856
|
+
const tracker = createRoleNameTracker();
|
|
857
|
+
let refCounter = 0;
|
|
858
|
+
function nextRef() {
|
|
859
|
+
return String(refCounter++);
|
|
860
|
+
}
|
|
861
|
+
function shouldAssignRef(role) {
|
|
862
|
+
if (options.interactive) {
|
|
863
|
+
return INTERACTIVE_ROLES.has(role);
|
|
864
|
+
}
|
|
865
|
+
return INTERACTIVE_ROLES.has(role) || CONTENT_ROLES_WITH_REF.has(role);
|
|
796
866
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
const tagName = node.tagName.toLowerCase();
|
|
804
|
-
if ((nonSemanticTags.has(tagName) || tagName === "a") && !info.name) continue;
|
|
805
|
-
if (filterableTags.has(tagName)) continue;
|
|
806
|
-
let isChildOfInteractive = false;
|
|
807
|
-
for (const otherItem of interactiveNodes) {
|
|
808
|
-
if (otherItem.id === id) continue;
|
|
809
|
-
const otherTagName = otherItem.node.tagName.toLowerCase();
|
|
810
|
-
if (["a", "button"].includes(otherTagName) && nonSemanticTags.has(tagName) && isAncestor(otherItem.id, id, map)) {
|
|
811
|
-
isChildOfInteractive = true;
|
|
812
|
-
break;
|
|
867
|
+
function traverse(nodeId, depth) {
|
|
868
|
+
const node = nodeMap.get(nodeId);
|
|
869
|
+
if (!node) return;
|
|
870
|
+
if (node.ignored) {
|
|
871
|
+
for (const childId of node.childIds || []) {
|
|
872
|
+
traverse(childId, depth);
|
|
813
873
|
}
|
|
874
|
+
return;
|
|
814
875
|
}
|
|
815
|
-
if (
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
if (isAncestor(otherItem.id, id, map) && info.name && otherInfo.name && info.name === otherInfo.name) {
|
|
821
|
-
isDuplicate = true;
|
|
822
|
-
break;
|
|
876
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) return;
|
|
877
|
+
const role = node.role?.value || "";
|
|
878
|
+
if (SKIP_ROLES.has(role)) {
|
|
879
|
+
for (const childId of node.childIds || []) {
|
|
880
|
+
traverse(childId, depth);
|
|
823
881
|
}
|
|
882
|
+
return;
|
|
824
883
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
if (info.name) line += ` "${truncateText(info.name)}"`;
|
|
832
|
-
line += ` [ref=${refId}]`;
|
|
833
|
-
lines.push(line);
|
|
834
|
-
refs[refId] = {
|
|
835
|
-
xpath: node.xpath || "",
|
|
836
|
-
role: info.role,
|
|
837
|
-
name: info.name ? truncateText(info.name, 100) : void 0,
|
|
838
|
-
tagName: node.tagName
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
|
-
return { snapshot: lines.join("\n"), refs };
|
|
842
|
-
}
|
|
843
|
-
function convertToFullTree(result) {
|
|
844
|
-
const lines = [];
|
|
845
|
-
const refs = {};
|
|
846
|
-
const { rootId, map } = result;
|
|
847
|
-
const skipTags = /* @__PURE__ */ new Set([
|
|
848
|
-
"script",
|
|
849
|
-
"style",
|
|
850
|
-
"noscript",
|
|
851
|
-
"svg",
|
|
852
|
-
"path",
|
|
853
|
-
"g",
|
|
854
|
-
"defs",
|
|
855
|
-
"clippath",
|
|
856
|
-
"lineargradient",
|
|
857
|
-
"stop",
|
|
858
|
-
"symbol",
|
|
859
|
-
"use",
|
|
860
|
-
"meta",
|
|
861
|
-
"link",
|
|
862
|
-
"head"
|
|
863
|
-
]);
|
|
864
|
-
function traverse(nodeId, depth) {
|
|
865
|
-
const node = map[nodeId];
|
|
866
|
-
if (!node) return;
|
|
867
|
-
const indent = " ".repeat(depth);
|
|
868
|
-
if ("type" in node && node.type === "TEXT_NODE") {
|
|
869
|
-
if (!node.isVisible) return;
|
|
870
|
-
const text = node.text.trim();
|
|
871
|
-
if (!text) return;
|
|
872
|
-
const displayText = text.length > 100 ? text.slice(0, 97) + "..." : text;
|
|
873
|
-
lines.push(`${indent}- text: ${displayText}`);
|
|
884
|
+
const name = node.name?.value?.trim() || "";
|
|
885
|
+
const isInteractive = INTERACTIVE_ROLES.has(role);
|
|
886
|
+
if (options.interactive && !isInteractive) {
|
|
887
|
+
for (const childId of node.childIds || []) {
|
|
888
|
+
traverse(childId, depth);
|
|
889
|
+
}
|
|
874
890
|
return;
|
|
875
891
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
892
|
+
if (role === "StaticText") {
|
|
893
|
+
if (name) {
|
|
894
|
+
const displayText = truncate(name, 100);
|
|
895
|
+
lines.push(`${indent(depth)}- text: ${displayText}`);
|
|
896
|
+
}
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
if ((role === "GenericContainer" || role === "generic") && !name) {
|
|
900
|
+
for (const childId of node.childIds || []) {
|
|
901
|
+
traverse(childId, depth);
|
|
902
|
+
}
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
const displayRole = role.charAt(0).toLowerCase() + role.slice(1);
|
|
906
|
+
let line = `${indent(depth)}- ${displayRole}`;
|
|
884
907
|
if (name) {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
if (
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
908
|
+
line += ` "${truncate(name, 50)}"`;
|
|
909
|
+
}
|
|
910
|
+
const level = getProperty(node, "level");
|
|
911
|
+
if (level !== void 0) {
|
|
912
|
+
line += ` [level=${level}]`;
|
|
913
|
+
}
|
|
914
|
+
const hasBackendId = node.backendDOMNodeId !== void 0;
|
|
915
|
+
if (shouldAssignRef(role) && hasBackendId) {
|
|
916
|
+
const ref = nextRef();
|
|
917
|
+
const nth = tracker.getNextIndex(role, name || void 0);
|
|
918
|
+
tracker.trackRef(role, name || void 0, ref);
|
|
919
|
+
line += ` [ref=${ref}]`;
|
|
920
|
+
if (nth > 0) line += ` [nth=${nth}]`;
|
|
921
|
+
refs[ref] = {
|
|
922
|
+
backendDOMNodeId: node.backendDOMNodeId,
|
|
923
|
+
role: displayRole,
|
|
924
|
+
name: name || void 0,
|
|
925
|
+
nth
|
|
896
926
|
};
|
|
897
927
|
}
|
|
928
|
+
if (!options.interactive && role === "link" && node.backendDOMNodeId !== void 0) {
|
|
929
|
+
const url = urlMap.get(node.backendDOMNodeId);
|
|
930
|
+
if (url) {
|
|
931
|
+
lines.push(line);
|
|
932
|
+
lines.push(`${indent(depth + 1)}- /url: ${url}`);
|
|
933
|
+
for (const childId of node.childIds || []) {
|
|
934
|
+
traverse(childId, depth + 1);
|
|
935
|
+
}
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
898
939
|
lines.push(line);
|
|
899
|
-
|
|
940
|
+
if (options.interactive) return;
|
|
941
|
+
for (const childId of node.childIds || []) {
|
|
900
942
|
traverse(childId, depth + 1);
|
|
901
943
|
}
|
|
902
944
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
945
|
+
traverse(rootNode.nodeId, 0);
|
|
946
|
+
removeNthFromNonDuplicates(refs, tracker);
|
|
947
|
+
const duplicateKeys = tracker.getDuplicateKeys();
|
|
948
|
+
const cleanedLines = lines.map((line) => {
|
|
949
|
+
const nthMatch = line.match(/\[nth=0\]/);
|
|
950
|
+
if (nthMatch) {
|
|
951
|
+
return line.replace(" [nth=0]", "");
|
|
952
|
+
}
|
|
953
|
+
const refMatch = line.match(/\[ref=(\d+)\].*\[nth=\d+\]/);
|
|
954
|
+
if (refMatch) {
|
|
955
|
+
const refId = refMatch[1];
|
|
956
|
+
const refInfo = refs[refId];
|
|
957
|
+
if (refInfo) {
|
|
958
|
+
const key = tracker.getKey(refInfo.role, refInfo.name);
|
|
959
|
+
if (!duplicateKeys.has(key)) {
|
|
960
|
+
return line.replace(/\s*\[nth=\d+\]/, "");
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return line;
|
|
965
|
+
});
|
|
966
|
+
let snapshot = cleanedLines.join("\n");
|
|
967
|
+
if (options.compact) {
|
|
968
|
+
snapshot = compactTree(snapshot);
|
|
969
|
+
}
|
|
970
|
+
return { snapshot: snapshot || "(empty)", refs };
|
|
971
|
+
}
|
|
972
|
+
function compactTree(tree) {
|
|
973
|
+
const lines = tree.split("\n");
|
|
974
|
+
const result = [];
|
|
975
|
+
for (let i = 0; i < lines.length; i++) {
|
|
976
|
+
const line = lines[i];
|
|
977
|
+
if (line.includes("[ref=")) {
|
|
978
|
+
result.push(line);
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
if (line.includes("- text:") || line.includes("- /url:")) {
|
|
982
|
+
result.push(line);
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
if (line.includes('"')) {
|
|
986
|
+
result.push(line);
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
const currentIndent = getIndentLevel(line);
|
|
990
|
+
let hasRelevantChildren = false;
|
|
991
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
992
|
+
const childIndent = getIndentLevel(lines[j]);
|
|
993
|
+
if (childIndent <= currentIndent) break;
|
|
994
|
+
if (lines[j].includes("[ref=") || lines[j].includes('"') || lines[j].includes("- text:")) {
|
|
995
|
+
hasRelevantChildren = true;
|
|
996
|
+
break;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if (hasRelevantChildren) {
|
|
1000
|
+
result.push(line);
|
|
907
1001
|
}
|
|
908
1002
|
}
|
|
909
|
-
return
|
|
1003
|
+
return result.join("\n");
|
|
910
1004
|
}
|
|
911
1005
|
|
|
912
|
-
const tabSnapshotRefs = /* @__PURE__ */ new Map();
|
|
913
|
-
const tabActiveFrameId$
|
|
1006
|
+
const tabSnapshotRefs$1 = /* @__PURE__ */ new Map();
|
|
1007
|
+
const tabActiveFrameId$2 = /* @__PURE__ */ new Map();
|
|
914
1008
|
async function loadRefsFromStorage() {
|
|
915
1009
|
try {
|
|
916
1010
|
const result = await chrome.storage.session.get("tabSnapshotRefs");
|
|
917
1011
|
if (result.tabSnapshotRefs) {
|
|
918
1012
|
const stored = result.tabSnapshotRefs;
|
|
919
1013
|
for (const [tabIdStr, refs] of Object.entries(stored)) {
|
|
920
|
-
tabSnapshotRefs.set(Number(tabIdStr), refs);
|
|
1014
|
+
tabSnapshotRefs$1.set(Number(tabIdStr), refs);
|
|
921
1015
|
}
|
|
922
|
-
console.log("[CDPDOMService] Loaded refs from storage:", tabSnapshotRefs.size, "tabs");
|
|
1016
|
+
console.log("[CDPDOMService] Loaded refs from storage:", tabSnapshotRefs$1.size, "tabs");
|
|
923
1017
|
}
|
|
924
1018
|
} catch (e) {
|
|
925
1019
|
console.warn("[CDPDOMService] Failed to load refs from storage:", e);
|
|
@@ -931,159 +1025,221 @@ async function saveRefsToStorage(tabId, refs) {
|
|
|
931
1025
|
const stored = result.tabSnapshotRefs || {};
|
|
932
1026
|
stored[String(tabId)] = refs;
|
|
933
1027
|
await chrome.storage.session.set({ tabSnapshotRefs: stored });
|
|
934
|
-
console.log("[CDPDOMService] Saved refs to storage for tab:", tabId, Object.keys(refs).length);
|
|
935
1028
|
} catch (e) {
|
|
936
1029
|
console.warn("[CDPDOMService] Failed to save refs to storage:", e);
|
|
937
1030
|
}
|
|
938
1031
|
}
|
|
939
1032
|
loadRefsFromStorage();
|
|
1033
|
+
async function buildURLMap(tabId, linkBackendIds) {
|
|
1034
|
+
if (linkBackendIds.size === 0) return /* @__PURE__ */ new Map();
|
|
1035
|
+
const urlMap = /* @__PURE__ */ new Map();
|
|
1036
|
+
try {
|
|
1037
|
+
let walk = function(node) {
|
|
1038
|
+
if (linkBackendIds.has(node.backendNodeId)) {
|
|
1039
|
+
const attrs = node.attributes || [];
|
|
1040
|
+
for (let i = 0; i < attrs.length; i += 2) {
|
|
1041
|
+
if (attrs[i] === "href") {
|
|
1042
|
+
urlMap.set(node.backendNodeId, attrs[i + 1]);
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
for (const child of node.children || []) walk(child);
|
|
1048
|
+
if (node.contentDocument) walk(node.contentDocument);
|
|
1049
|
+
for (const shadow of node.shadowRoots || []) walk(shadow);
|
|
1050
|
+
};
|
|
1051
|
+
const doc = await getDocument(tabId, { depth: -1, pierce: true });
|
|
1052
|
+
walk(doc);
|
|
1053
|
+
} catch (e) {
|
|
1054
|
+
console.warn("[CDPDOMService] Failed to build URL map:", e);
|
|
1055
|
+
}
|
|
1056
|
+
return urlMap;
|
|
1057
|
+
}
|
|
940
1058
|
async function getSnapshot(tabId, options = {}) {
|
|
941
|
-
console.log("[CDPDOMService] Getting snapshot via
|
|
942
|
-
|
|
1059
|
+
console.log("[CDPDOMService] Getting snapshot via AX tree for tab:", tabId, options);
|
|
1060
|
+
let axNodes;
|
|
1061
|
+
if (options.selector) {
|
|
1062
|
+
try {
|
|
1063
|
+
const doc = await getDocument(tabId, { depth: 0 });
|
|
1064
|
+
const nodeId = await querySelector(tabId, doc.nodeId, options.selector);
|
|
1065
|
+
if (!nodeId) throw new Error(`Selector "${options.selector}" not found`);
|
|
1066
|
+
axNodes = await getPartialAccessibilityTree(tabId, nodeId);
|
|
1067
|
+
} catch (e) {
|
|
1068
|
+
throw new Error(`Selector "${options.selector}" failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1069
|
+
}
|
|
1070
|
+
} else {
|
|
1071
|
+
axNodes = await getFullAccessibilityTree(tabId);
|
|
1072
|
+
}
|
|
1073
|
+
const linkBackendIds = /* @__PURE__ */ new Set();
|
|
1074
|
+
for (const node of axNodes) {
|
|
1075
|
+
if (node.role?.value === "link" && node.backendDOMNodeId !== void 0) {
|
|
1076
|
+
linkBackendIds.add(node.backendDOMNodeId);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
const urlMap = await buildURLMap(tabId, linkBackendIds);
|
|
1080
|
+
const result = formatAXTree(axNodes, urlMap, {
|
|
1081
|
+
interactive: options.interactive,
|
|
1082
|
+
compact: options.compact,
|
|
1083
|
+
maxDepth: options.maxDepth
|
|
1084
|
+
});
|
|
943
1085
|
const convertedRefs = {};
|
|
944
|
-
for (const [refId,
|
|
1086
|
+
for (const [refId, axRef] of Object.entries(result.refs)) {
|
|
945
1087
|
convertedRefs[refId] = {
|
|
946
|
-
|
|
947
|
-
role:
|
|
948
|
-
name:
|
|
949
|
-
tagName: refInfo.tagName
|
|
1088
|
+
backendDOMNodeId: axRef.backendDOMNodeId,
|
|
1089
|
+
role: axRef.role,
|
|
1090
|
+
name: axRef.name
|
|
950
1091
|
};
|
|
951
1092
|
}
|
|
952
|
-
tabSnapshotRefs.set(tabId, convertedRefs);
|
|
1093
|
+
tabSnapshotRefs$1.set(tabId, convertedRefs);
|
|
953
1094
|
await saveRefsToStorage(tabId, convertedRefs);
|
|
954
1095
|
console.log("[CDPDOMService] Snapshot complete:", {
|
|
955
1096
|
linesCount: result.snapshot.split("\n").length,
|
|
956
1097
|
refsCount: Object.keys(convertedRefs).length
|
|
957
1098
|
});
|
|
958
|
-
return {
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1099
|
+
return { snapshot: result.snapshot, refs: convertedRefs };
|
|
1100
|
+
}
|
|
1101
|
+
async function getElementCenter(tabId, backendNodeId) {
|
|
1102
|
+
const objectId = await resolveNodeByBackendId(tabId, backendNodeId);
|
|
1103
|
+
if (!objectId) throw new Error("Failed to resolve node");
|
|
1104
|
+
const result = await callFunctionOn(tabId, objectId, `function() {
|
|
1105
|
+
this.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
|
|
1106
|
+
const rect = this.getBoundingClientRect();
|
|
1107
|
+
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
1108
|
+
}`);
|
|
1109
|
+
if (!result || typeof result !== "object") throw new Error("Failed to get element center");
|
|
1110
|
+
return result;
|
|
962
1111
|
}
|
|
963
|
-
async function
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
return refs[refId];
|
|
968
|
-
}
|
|
969
|
-
if (!tabSnapshotRefs.has(tabId)) {
|
|
970
|
-
await loadRefsFromStorage();
|
|
971
|
-
const loaded = tabSnapshotRefs.get(tabId);
|
|
972
|
-
if (loaded?.[refId]) {
|
|
973
|
-
return loaded[refId];
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
return null;
|
|
1112
|
+
async function evaluateOnElement(tabId, backendNodeId, fn, args = []) {
|
|
1113
|
+
const objectId = await resolveNodeByBackendId(tabId, backendNodeId);
|
|
1114
|
+
if (!objectId) throw new Error("Failed to resolve node");
|
|
1115
|
+
return callFunctionOn(tabId, objectId, fn, args);
|
|
977
1116
|
}
|
|
978
|
-
function
|
|
979
|
-
|
|
980
|
-
tabActiveFrameId$1.delete(tabId);
|
|
1117
|
+
function getBackendNodeId(refInfo) {
|
|
1118
|
+
return refInfo.backendDOMNodeId ?? null;
|
|
981
1119
|
}
|
|
982
1120
|
async function getElementCenterByXPath(tabId, xpath) {
|
|
983
1121
|
const result = await evaluate(tabId, `
|
|
984
1122
|
(function() {
|
|
985
1123
|
const result = document.evaluate(
|
|
986
1124
|
${JSON.stringify(xpath)},
|
|
987
|
-
document,
|
|
988
|
-
null,
|
|
989
|
-
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
990
|
-
null
|
|
1125
|
+
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null
|
|
991
1126
|
);
|
|
992
1127
|
const element = result.singleNodeValue;
|
|
993
1128
|
if (!element) return null;
|
|
994
|
-
|
|
995
|
-
// 滚动到可见
|
|
996
1129
|
element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
|
|
997
|
-
|
|
998
1130
|
const rect = element.getBoundingClientRect();
|
|
999
|
-
return {
|
|
1000
|
-
x: rect.left + rect.width / 2,
|
|
1001
|
-
y: rect.top + rect.height / 2,
|
|
1002
|
-
};
|
|
1131
|
+
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
1003
1132
|
})()
|
|
1004
1133
|
`, { returnByValue: true });
|
|
1005
|
-
if (!result) {
|
|
1006
|
-
throw new Error(`Element not found by xpath: ${xpath}`);
|
|
1007
|
-
}
|
|
1134
|
+
if (!result) throw new Error(`Element not found by xpath: ${xpath}`);
|
|
1008
1135
|
return result;
|
|
1009
1136
|
}
|
|
1137
|
+
async function getRefInfo(tabId, ref) {
|
|
1138
|
+
const refId = ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1139
|
+
const refs = tabSnapshotRefs$1.get(tabId);
|
|
1140
|
+
if (refs?.[refId]) return refs[refId];
|
|
1141
|
+
if (!tabSnapshotRefs$1.has(tabId)) {
|
|
1142
|
+
await loadRefsFromStorage();
|
|
1143
|
+
const loaded = tabSnapshotRefs$1.get(tabId);
|
|
1144
|
+
if (loaded?.[refId]) return loaded[refId];
|
|
1145
|
+
}
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
function cleanupTab$1(tabId) {
|
|
1149
|
+
tabSnapshotRefs$1.delete(tabId);
|
|
1150
|
+
tabActiveFrameId$2.delete(tabId);
|
|
1151
|
+
}
|
|
1010
1152
|
async function clickElement(tabId, ref) {
|
|
1011
1153
|
const refInfo = await getRefInfo(tabId, ref);
|
|
1012
|
-
if (!refInfo) {
|
|
1013
|
-
|
|
1154
|
+
if (!refInfo) throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
1155
|
+
const { role, name } = refInfo;
|
|
1156
|
+
const backendNodeId = getBackendNodeId(refInfo);
|
|
1157
|
+
let x, y;
|
|
1158
|
+
if (backendNodeId !== null) {
|
|
1159
|
+
({ x, y } = await getElementCenter(tabId, backendNodeId));
|
|
1160
|
+
} else if (refInfo.xpath) {
|
|
1161
|
+
({ x, y } = await getElementCenterByXPath(tabId, refInfo.xpath));
|
|
1162
|
+
} else {
|
|
1163
|
+
throw new Error(`No locator for ref "${ref}"`);
|
|
1014
1164
|
}
|
|
1015
|
-
const { xpath, role, name } = refInfo;
|
|
1016
|
-
const { x, y } = await getElementCenterByXPath(tabId, xpath);
|
|
1017
1165
|
await click(tabId, x, y);
|
|
1018
1166
|
console.log("[CDPDOMService] Clicked element:", { ref, role, name, x, y });
|
|
1019
1167
|
return { role, name };
|
|
1020
1168
|
}
|
|
1021
1169
|
async function hoverElement(tabId, ref) {
|
|
1022
1170
|
const refInfo = await getRefInfo(tabId, ref);
|
|
1023
|
-
if (!refInfo) {
|
|
1024
|
-
|
|
1171
|
+
if (!refInfo) throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
1172
|
+
const { role, name } = refInfo;
|
|
1173
|
+
const backendNodeId = getBackendNodeId(refInfo);
|
|
1174
|
+
let x, y;
|
|
1175
|
+
if (backendNodeId !== null) {
|
|
1176
|
+
({ x, y } = await getElementCenter(tabId, backendNodeId));
|
|
1177
|
+
} else if (refInfo.xpath) {
|
|
1178
|
+
({ x, y } = await getElementCenterByXPath(tabId, refInfo.xpath));
|
|
1179
|
+
} else {
|
|
1180
|
+
throw new Error(`No locator for ref "${ref}"`);
|
|
1025
1181
|
}
|
|
1026
|
-
const { xpath, role, name } = refInfo;
|
|
1027
|
-
const { x, y } = await getElementCenterByXPath(tabId, xpath);
|
|
1028
1182
|
await moveMouse(tabId, x, y);
|
|
1029
1183
|
console.log("[CDPDOMService] Hovered element:", { ref, role, name, x, y });
|
|
1030
1184
|
return { role, name };
|
|
1031
1185
|
}
|
|
1032
1186
|
async function fillElement(tabId, ref, text) {
|
|
1033
1187
|
const refInfo = await getRefInfo(tabId, ref);
|
|
1034
|
-
if (!refInfo) {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
null
|
|
1046
|
-
);
|
|
1047
|
-
const element = result.singleNodeValue;
|
|
1048
|
-
if (!element) throw new Error('Element not found');
|
|
1049
|
-
|
|
1050
|
-
element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
|
|
1051
|
-
element.focus();
|
|
1052
|
-
|
|
1053
|
-
// 清空内容
|
|
1054
|
-
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
|
|
1055
|
-
element.value = '';
|
|
1056
|
-
} else if (element.isContentEditable) {
|
|
1057
|
-
element.textContent = '';
|
|
1188
|
+
if (!refInfo) throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
1189
|
+
const { role, name } = refInfo;
|
|
1190
|
+
const backendNodeId = getBackendNodeId(refInfo);
|
|
1191
|
+
if (backendNodeId !== null) {
|
|
1192
|
+
await evaluateOnElement(tabId, backendNodeId, `function() {
|
|
1193
|
+
this.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
|
|
1194
|
+
this.focus();
|
|
1195
|
+
if (this.tagName === 'INPUT' || this.tagName === 'TEXTAREA') {
|
|
1196
|
+
this.value = '';
|
|
1197
|
+
} else if (this.isContentEditable) {
|
|
1198
|
+
this.textContent = '';
|
|
1058
1199
|
}
|
|
1059
|
-
})
|
|
1060
|
-
|
|
1200
|
+
}`);
|
|
1201
|
+
} else if (refInfo.xpath) {
|
|
1202
|
+
await evaluate(tabId, `
|
|
1203
|
+
(function() {
|
|
1204
|
+
const result = document.evaluate(${JSON.stringify(refInfo.xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
1205
|
+
const element = result.singleNodeValue;
|
|
1206
|
+
if (!element) throw new Error('Element not found');
|
|
1207
|
+
element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
|
|
1208
|
+
element.focus();
|
|
1209
|
+
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') { element.value = ''; }
|
|
1210
|
+
else if (element.isContentEditable) { element.textContent = ''; }
|
|
1211
|
+
})()
|
|
1212
|
+
`);
|
|
1213
|
+
} else {
|
|
1214
|
+
throw new Error(`No locator for ref "${ref}"`);
|
|
1215
|
+
}
|
|
1061
1216
|
await insertText(tabId, text);
|
|
1062
1217
|
console.log("[CDPDOMService] Filled element:", { ref, role, name, textLength: text.length });
|
|
1063
1218
|
return { role, name };
|
|
1064
1219
|
}
|
|
1065
1220
|
async function typeElement(tabId, ref, text) {
|
|
1066
1221
|
const refInfo = await getRefInfo(tabId, ref);
|
|
1067
|
-
if (!refInfo) {
|
|
1068
|
-
|
|
1222
|
+
if (!refInfo) throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
1223
|
+
const { role, name } = refInfo;
|
|
1224
|
+
const backendNodeId = getBackendNodeId(refInfo);
|
|
1225
|
+
if (backendNodeId !== null) {
|
|
1226
|
+
await evaluateOnElement(tabId, backendNodeId, `function() {
|
|
1227
|
+
this.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
|
|
1228
|
+
this.focus();
|
|
1229
|
+
}`);
|
|
1230
|
+
} else if (refInfo.xpath) {
|
|
1231
|
+
await evaluate(tabId, `
|
|
1232
|
+
(function() {
|
|
1233
|
+
const result = document.evaluate(${JSON.stringify(refInfo.xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
1234
|
+
const element = result.singleNodeValue;
|
|
1235
|
+
if (!element) throw new Error('Element not found');
|
|
1236
|
+
element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
|
|
1237
|
+
element.focus();
|
|
1238
|
+
})()
|
|
1239
|
+
`);
|
|
1240
|
+
} else {
|
|
1241
|
+
throw new Error(`No locator for ref "${ref}"`);
|
|
1069
1242
|
}
|
|
1070
|
-
const { xpath, role, name } = refInfo;
|
|
1071
|
-
await evaluate(tabId, `
|
|
1072
|
-
(function() {
|
|
1073
|
-
const result = document.evaluate(
|
|
1074
|
-
${JSON.stringify(xpath)},
|
|
1075
|
-
document,
|
|
1076
|
-
null,
|
|
1077
|
-
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
1078
|
-
null
|
|
1079
|
-
);
|
|
1080
|
-
const element = result.singleNodeValue;
|
|
1081
|
-
if (!element) throw new Error('Element not found');
|
|
1082
|
-
|
|
1083
|
-
element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
|
|
1084
|
-
element.focus();
|
|
1085
|
-
})()
|
|
1086
|
-
`);
|
|
1087
1243
|
for (const char of text) {
|
|
1088
1244
|
await pressKey$1(tabId, char);
|
|
1089
1245
|
}
|
|
@@ -1092,169 +1248,151 @@ async function typeElement(tabId, ref, text) {
|
|
|
1092
1248
|
}
|
|
1093
1249
|
async function getElementText(tabId, ref) {
|
|
1094
1250
|
const refInfo = await getRefInfo(tabId, ref);
|
|
1095
|
-
if (!refInfo) {
|
|
1096
|
-
|
|
1251
|
+
if (!refInfo) throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
1252
|
+
const backendNodeId = getBackendNodeId(refInfo);
|
|
1253
|
+
let text;
|
|
1254
|
+
if (backendNodeId !== null) {
|
|
1255
|
+
text = await evaluateOnElement(tabId, backendNodeId, `function() {
|
|
1256
|
+
return (this.textContent || '').trim();
|
|
1257
|
+
}`);
|
|
1258
|
+
} else if (refInfo.xpath) {
|
|
1259
|
+
text = await evaluate(tabId, `
|
|
1260
|
+
(function() {
|
|
1261
|
+
const result = document.evaluate(${JSON.stringify(refInfo.xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
1262
|
+
const element = result.singleNodeValue;
|
|
1263
|
+
if (!element) return '';
|
|
1264
|
+
return (element.textContent || '').trim();
|
|
1265
|
+
})()
|
|
1266
|
+
`);
|
|
1267
|
+
} else {
|
|
1268
|
+
throw new Error(`No locator for ref "${ref}"`);
|
|
1097
1269
|
}
|
|
1098
|
-
|
|
1099
|
-
const text = await evaluate(tabId, `
|
|
1100
|
-
(function() {
|
|
1101
|
-
const result = document.evaluate(
|
|
1102
|
-
${JSON.stringify(xpath)},
|
|
1103
|
-
document,
|
|
1104
|
-
null,
|
|
1105
|
-
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
1106
|
-
null
|
|
1107
|
-
);
|
|
1108
|
-
const element = result.singleNodeValue;
|
|
1109
|
-
if (!element) return '';
|
|
1110
|
-
return (element.textContent || '').trim();
|
|
1111
|
-
})()
|
|
1112
|
-
`);
|
|
1113
|
-
console.log("[CDPDOMService] Got element text:", { ref, textLength: text.length });
|
|
1114
|
-
return text;
|
|
1270
|
+
return text || "";
|
|
1115
1271
|
}
|
|
1116
1272
|
async function checkElement(tabId, ref) {
|
|
1117
1273
|
const refInfo = await getRefInfo(tabId, ref);
|
|
1118
|
-
if (!refInfo) {
|
|
1119
|
-
|
|
1274
|
+
if (!refInfo) throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
1275
|
+
const { role, name } = refInfo;
|
|
1276
|
+
const backendNodeId = getBackendNodeId(refInfo);
|
|
1277
|
+
let wasChecked;
|
|
1278
|
+
if (backendNodeId !== null) {
|
|
1279
|
+
wasChecked = await evaluateOnElement(tabId, backendNodeId, `function() {
|
|
1280
|
+
if (this.type !== 'checkbox' && this.type !== 'radio') throw new Error('Element is not a checkbox or radio');
|
|
1281
|
+
const was = this.checked;
|
|
1282
|
+
if (!was) { this.checked = true; this.dispatchEvent(new Event('change', { bubbles: true })); }
|
|
1283
|
+
return was;
|
|
1284
|
+
}`);
|
|
1285
|
+
} else if (refInfo.xpath) {
|
|
1286
|
+
wasChecked = await evaluate(tabId, `
|
|
1287
|
+
(function() {
|
|
1288
|
+
const result = document.evaluate(${JSON.stringify(refInfo.xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
1289
|
+
const element = result.singleNodeValue;
|
|
1290
|
+
if (!element) throw new Error('Element not found');
|
|
1291
|
+
if (element.type !== 'checkbox' && element.type !== 'radio') throw new Error('Element is not a checkbox or radio');
|
|
1292
|
+
const was = element.checked;
|
|
1293
|
+
if (!was) { element.checked = true; element.dispatchEvent(new Event('change', { bubbles: true })); }
|
|
1294
|
+
return was;
|
|
1295
|
+
})()
|
|
1296
|
+
`);
|
|
1297
|
+
} else {
|
|
1298
|
+
throw new Error(`No locator for ref "${ref}"`);
|
|
1120
1299
|
}
|
|
1121
|
-
|
|
1122
|
-
const result = await evaluate(tabId, `
|
|
1123
|
-
(function() {
|
|
1124
|
-
const result = document.evaluate(
|
|
1125
|
-
${JSON.stringify(xpath)},
|
|
1126
|
-
document,
|
|
1127
|
-
null,
|
|
1128
|
-
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
1129
|
-
null
|
|
1130
|
-
);
|
|
1131
|
-
const element = result.singleNodeValue;
|
|
1132
|
-
if (!element) throw new Error('Element not found');
|
|
1133
|
-
if (element.type !== 'checkbox' && element.type !== 'radio') {
|
|
1134
|
-
throw new Error('Element is not a checkbox or radio');
|
|
1135
|
-
}
|
|
1136
|
-
const wasChecked = element.checked;
|
|
1137
|
-
if (!wasChecked) {
|
|
1138
|
-
element.checked = true;
|
|
1139
|
-
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1140
|
-
}
|
|
1141
|
-
return wasChecked;
|
|
1142
|
-
})()
|
|
1143
|
-
`);
|
|
1144
|
-
console.log("[CDPDOMService] Checked element:", { ref, role, name, wasAlreadyChecked: result });
|
|
1145
|
-
return { role, name, wasAlreadyChecked: result };
|
|
1300
|
+
return { role, name, wasAlreadyChecked: wasChecked };
|
|
1146
1301
|
}
|
|
1147
1302
|
async function uncheckElement(tabId, ref) {
|
|
1148
1303
|
const refInfo = await getRefInfo(tabId, ref);
|
|
1149
|
-
if (!refInfo) {
|
|
1150
|
-
|
|
1304
|
+
if (!refInfo) throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
1305
|
+
const { role, name } = refInfo;
|
|
1306
|
+
const backendNodeId = getBackendNodeId(refInfo);
|
|
1307
|
+
let wasUnchecked;
|
|
1308
|
+
if (backendNodeId !== null) {
|
|
1309
|
+
wasUnchecked = await evaluateOnElement(tabId, backendNodeId, `function() {
|
|
1310
|
+
if (this.type !== 'checkbox' && this.type !== 'radio') throw new Error('Element is not a checkbox or radio');
|
|
1311
|
+
const was = !this.checked;
|
|
1312
|
+
if (!was) { this.checked = false; this.dispatchEvent(new Event('change', { bubbles: true })); }
|
|
1313
|
+
return was;
|
|
1314
|
+
}`);
|
|
1315
|
+
} else if (refInfo.xpath) {
|
|
1316
|
+
wasUnchecked = await evaluate(tabId, `
|
|
1317
|
+
(function() {
|
|
1318
|
+
const result = document.evaluate(${JSON.stringify(refInfo.xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
1319
|
+
const element = result.singleNodeValue;
|
|
1320
|
+
if (!element) throw new Error('Element not found');
|
|
1321
|
+
if (element.type !== 'checkbox' && element.type !== 'radio') throw new Error('Element is not a checkbox or radio');
|
|
1322
|
+
const was = !element.checked;
|
|
1323
|
+
if (!was) { element.checked = false; element.dispatchEvent(new Event('change', { bubbles: true })); }
|
|
1324
|
+
return was;
|
|
1325
|
+
})()
|
|
1326
|
+
`);
|
|
1327
|
+
} else {
|
|
1328
|
+
throw new Error(`No locator for ref "${ref}"`);
|
|
1151
1329
|
}
|
|
1152
|
-
|
|
1153
|
-
const result = await evaluate(tabId, `
|
|
1154
|
-
(function() {
|
|
1155
|
-
const result = document.evaluate(
|
|
1156
|
-
${JSON.stringify(xpath)},
|
|
1157
|
-
document,
|
|
1158
|
-
null,
|
|
1159
|
-
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
1160
|
-
null
|
|
1161
|
-
);
|
|
1162
|
-
const element = result.singleNodeValue;
|
|
1163
|
-
if (!element) throw new Error('Element not found');
|
|
1164
|
-
if (element.type !== 'checkbox' && element.type !== 'radio') {
|
|
1165
|
-
throw new Error('Element is not a checkbox or radio');
|
|
1166
|
-
}
|
|
1167
|
-
const wasUnchecked = !element.checked;
|
|
1168
|
-
if (!wasUnchecked) {
|
|
1169
|
-
element.checked = false;
|
|
1170
|
-
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1171
|
-
}
|
|
1172
|
-
return wasUnchecked;
|
|
1173
|
-
})()
|
|
1174
|
-
`);
|
|
1175
|
-
console.log("[CDPDOMService] Unchecked element:", { ref, role, name, wasAlreadyUnchecked: result });
|
|
1176
|
-
return { role, name, wasAlreadyUnchecked: result };
|
|
1330
|
+
return { role, name, wasAlreadyUnchecked: wasUnchecked };
|
|
1177
1331
|
}
|
|
1178
1332
|
async function selectOption(tabId, ref, value) {
|
|
1179
1333
|
const refInfo = await getRefInfo(tabId, ref);
|
|
1180
|
-
if (!refInfo) {
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
);
|
|
1194
|
-
const element = result.singleNodeValue;
|
|
1195
|
-
if (!element) throw new Error('Element not found');
|
|
1196
|
-
if (element.tagName !== 'SELECT') {
|
|
1197
|
-
throw new Error('Element is not a <select> element');
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
// 尝试通过 value 或 text 匹配
|
|
1201
|
-
let matched = null;
|
|
1202
|
-
for (const opt of element.options) {
|
|
1203
|
-
if (opt.value === selectValue || opt.textContent.trim() === selectValue) {
|
|
1204
|
-
matched = opt;
|
|
1205
|
-
break;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
// 不区分大小写匹配
|
|
1210
|
-
if (!matched) {
|
|
1211
|
-
const lower = selectValue.toLowerCase();
|
|
1212
|
-
for (const opt of element.options) {
|
|
1213
|
-
if (opt.value.toLowerCase() === lower || opt.textContent.trim().toLowerCase() === lower) {
|
|
1214
|
-
matched = opt;
|
|
1215
|
-
break;
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
if (!matched) {
|
|
1221
|
-
const available = Array.from(element.options).map(o => ({ value: o.value, label: o.textContent.trim() }));
|
|
1222
|
-
throw new Error('Option not found: ' + selectValue + '. Available: ' + JSON.stringify(available));
|
|
1334
|
+
if (!refInfo) throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
1335
|
+
const { role, name } = refInfo;
|
|
1336
|
+
const backendNodeId = getBackendNodeId(refInfo);
|
|
1337
|
+
const selectFn = `function(selectValue) {
|
|
1338
|
+
if (this.tagName !== 'SELECT') throw new Error('Element is not a <select> element');
|
|
1339
|
+
let matched = null;
|
|
1340
|
+
for (const opt of this.options) {
|
|
1341
|
+
if (opt.value === selectValue || opt.textContent.trim() === selectValue) { matched = opt; break; }
|
|
1342
|
+
}
|
|
1343
|
+
if (!matched) {
|
|
1344
|
+
const lower = selectValue.toLowerCase();
|
|
1345
|
+
for (const opt of this.options) {
|
|
1346
|
+
if (opt.value.toLowerCase() === lower || opt.textContent.trim().toLowerCase() === lower) { matched = opt; break; }
|
|
1223
1347
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1348
|
+
}
|
|
1349
|
+
if (!matched) {
|
|
1350
|
+
const available = Array.from(this.options).map(o => ({ value: o.value, label: o.textContent.trim() }));
|
|
1351
|
+
throw new Error('Option not found: ' + selectValue + '. Available: ' + JSON.stringify(available));
|
|
1352
|
+
}
|
|
1353
|
+
this.value = matched.value;
|
|
1354
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1355
|
+
return { selectedValue: matched.value, selectedLabel: matched.textContent.trim() };
|
|
1356
|
+
}`;
|
|
1357
|
+
let result;
|
|
1358
|
+
if (backendNodeId !== null) {
|
|
1359
|
+
result = await evaluateOnElement(tabId, backendNodeId, selectFn, [value]);
|
|
1360
|
+
} else if (refInfo.xpath) {
|
|
1361
|
+
result = await evaluate(tabId, `
|
|
1362
|
+
(function() {
|
|
1363
|
+
const selectValue = ${JSON.stringify(value)};
|
|
1364
|
+
const xpathResult = document.evaluate(${JSON.stringify(refInfo.xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
1365
|
+
const element = xpathResult.singleNodeValue;
|
|
1366
|
+
if (!element) throw new Error('Element not found');
|
|
1367
|
+
return (${selectFn}).call(element, selectValue);
|
|
1368
|
+
})()
|
|
1369
|
+
`);
|
|
1370
|
+
} else {
|
|
1371
|
+
throw new Error(`No locator for ref "${ref}"`);
|
|
1372
|
+
}
|
|
1231
1373
|
const { selectedValue, selectedLabel } = result;
|
|
1232
|
-
console.log("[CDPDOMService] Selected option:", { ref, role, name, selectedValue });
|
|
1233
1374
|
return { role, name, selectedValue, selectedLabel };
|
|
1234
1375
|
}
|
|
1235
1376
|
async function waitForElement(tabId, ref, maxWait = 1e4, interval = 200) {
|
|
1236
1377
|
const refInfo = await getRefInfo(tabId, ref);
|
|
1237
|
-
if (!refInfo) {
|
|
1238
|
-
|
|
1239
|
-
}
|
|
1240
|
-
const { xpath } = refInfo;
|
|
1378
|
+
if (!refInfo) throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
1379
|
+
const backendNodeId = getBackendNodeId(refInfo);
|
|
1241
1380
|
let elapsed = 0;
|
|
1242
1381
|
while (elapsed < maxWait) {
|
|
1243
|
-
|
|
1244
|
-
(
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
return;
|
|
1382
|
+
try {
|
|
1383
|
+
if (backendNodeId !== null) {
|
|
1384
|
+
const objectId = await resolveNodeByBackendId(tabId, backendNodeId);
|
|
1385
|
+
if (objectId) return;
|
|
1386
|
+
} else if (refInfo.xpath) {
|
|
1387
|
+
const found = await evaluate(tabId, `
|
|
1388
|
+
(function() {
|
|
1389
|
+
const result = document.evaluate(${JSON.stringify(refInfo.xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
1390
|
+
return result.singleNodeValue !== null;
|
|
1391
|
+
})()
|
|
1392
|
+
`);
|
|
1393
|
+
if (found) return;
|
|
1394
|
+
}
|
|
1395
|
+
} catch {
|
|
1258
1396
|
}
|
|
1259
1397
|
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
1260
1398
|
elapsed += interval;
|
|
@@ -1262,8 +1400,7 @@ async function waitForElement(tabId, ref, maxWait = 1e4, interval = 200) {
|
|
|
1262
1400
|
throw new Error(`Timeout waiting for element @${ref} after ${maxWait}ms`);
|
|
1263
1401
|
}
|
|
1264
1402
|
function setActiveFrameId(tabId, frameId) {
|
|
1265
|
-
tabActiveFrameId$
|
|
1266
|
-
console.log("[CDPDOMService] Active frame changed:", { tabId, frameId: frameId ?? "main" });
|
|
1403
|
+
tabActiveFrameId$2.set(tabId, frameId);
|
|
1267
1404
|
}
|
|
1268
1405
|
async function pressKey(tabId, key, modifiers = []) {
|
|
1269
1406
|
let modifierFlags = 0;
|
|
@@ -1272,7 +1409,6 @@ async function pressKey(tabId, key, modifiers = []) {
|
|
|
1272
1409
|
if (modifiers.includes("Meta")) modifierFlags |= 4;
|
|
1273
1410
|
if (modifiers.includes("Shift")) modifierFlags |= 8;
|
|
1274
1411
|
await pressKey$1(tabId, key, { modifiers: modifierFlags });
|
|
1275
|
-
console.log("[CDPDOMService] Pressed key:", key, modifiers);
|
|
1276
1412
|
}
|
|
1277
1413
|
async function scrollPage(tabId, direction, pixels) {
|
|
1278
1414
|
const result = await evaluate(
|
|
@@ -1299,7 +1435,13 @@ async function scrollPage(tabId, direction, pixels) {
|
|
|
1299
1435
|
break;
|
|
1300
1436
|
}
|
|
1301
1437
|
await scroll(tabId, x, y, deltaX, deltaY);
|
|
1302
|
-
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const tabSnapshotRefs = /* @__PURE__ */ new Map();
|
|
1441
|
+
const tabActiveFrameId$1 = /* @__PURE__ */ new Map();
|
|
1442
|
+
function cleanupTab(tabId) {
|
|
1443
|
+
tabSnapshotRefs.delete(tabId);
|
|
1444
|
+
tabActiveFrameId$1.delete(tabId);
|
|
1303
1445
|
}
|
|
1304
1446
|
|
|
1305
1447
|
let isRecording = false;
|
|
@@ -1600,9 +1742,12 @@ async function handleSnapshot(command) {
|
|
|
1600
1742
|
};
|
|
1601
1743
|
}
|
|
1602
1744
|
const interactive = command.interactive;
|
|
1603
|
-
|
|
1745
|
+
const compact = command.compact;
|
|
1746
|
+
const maxDepth = command.maxDepth;
|
|
1747
|
+
const selector = command.selector;
|
|
1748
|
+
console.log("[CommandHandler] Taking snapshot of tab:", activeTab.id, activeTab.url, { interactive, compact, maxDepth, selector });
|
|
1604
1749
|
try {
|
|
1605
|
-
const snapshotResult = await getSnapshot(activeTab.id, { interactive });
|
|
1750
|
+
const snapshotResult = await getSnapshot(activeTab.id, { interactive, compact, maxDepth, selector });
|
|
1606
1751
|
return {
|
|
1607
1752
|
id: command.id,
|
|
1608
1753
|
success: true,
|
|
@@ -1925,8 +2070,8 @@ async function handleClose(command) {
|
|
|
1925
2070
|
console.log("[CommandHandler] Closing tab:", tabId, url);
|
|
1926
2071
|
try {
|
|
1927
2072
|
await chrome.tabs.remove(tabId);
|
|
1928
|
-
cleanupTab$1(tabId);
|
|
1929
2073
|
cleanupTab(tabId);
|
|
2074
|
+
cleanupTab$1(tabId);
|
|
1930
2075
|
tabActiveFrameId.delete(tabId);
|
|
1931
2076
|
return {
|
|
1932
2077
|
id: command.id,
|
|
@@ -2478,8 +2623,8 @@ async function handleTabClose(command) {
|
|
|
2478
2623
|
const title = targetTab.title || "";
|
|
2479
2624
|
const url = targetTab.url || "";
|
|
2480
2625
|
await chrome.tabs.remove(tabId);
|
|
2481
|
-
cleanupTab$1(tabId);
|
|
2482
2626
|
cleanupTab(tabId);
|
|
2627
|
+
cleanupTab$1(tabId);
|
|
2483
2628
|
tabActiveFrameId.delete(tabId);
|
|
2484
2629
|
return {
|
|
2485
2630
|
id: command.id,
|
|
@@ -2735,7 +2880,8 @@ async function handleNetwork(command) {
|
|
|
2735
2880
|
case "requests": {
|
|
2736
2881
|
await enableNetwork(tabId);
|
|
2737
2882
|
const filter = command.filter;
|
|
2738
|
-
const
|
|
2883
|
+
const withBody = command.withBody === true;
|
|
2884
|
+
const requests = getNetworkRequests(tabId, filter, withBody);
|
|
2739
2885
|
const networkRequests = requests.map((r) => ({
|
|
2740
2886
|
requestId: r.requestId,
|
|
2741
2887
|
url: r.url,
|
|
@@ -2745,7 +2891,18 @@ async function handleNetwork(command) {
|
|
|
2745
2891
|
status: r.response?.status,
|
|
2746
2892
|
statusText: r.response?.statusText,
|
|
2747
2893
|
failed: r.failed,
|
|
2748
|
-
failureReason: r.failureReason
|
|
2894
|
+
failureReason: r.failureReason,
|
|
2895
|
+
...withBody ? {
|
|
2896
|
+
requestHeaders: r.requestHeaders,
|
|
2897
|
+
requestBody: r.requestBody,
|
|
2898
|
+
requestBodyTruncated: r.requestBodyTruncated,
|
|
2899
|
+
responseHeaders: r.response?.headers,
|
|
2900
|
+
responseBody: r.response?.body,
|
|
2901
|
+
responseBodyBase64: r.response?.bodyBase64,
|
|
2902
|
+
responseBodyTruncated: r.response?.bodyTruncated,
|
|
2903
|
+
mimeType: r.response?.mimeType,
|
|
2904
|
+
bodyError: r.bodyError
|
|
2905
|
+
} : {}
|
|
2749
2906
|
}));
|
|
2750
2907
|
return {
|
|
2751
2908
|
id: command.id,
|