bb-browser 0.2.2 → 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.
@@ -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
- if (!filter) return requests;
354
- const lowerFilter = filter.toLowerCase();
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
- headers: params.request.headers,
472
- postData: params.request.postData
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 tabSnapshotRefs$1 = /* @__PURE__ */ new Map();
604
- const tabActiveFrameId$2 = /* @__PURE__ */ new Map();
605
- function cleanupTab$1(tabId) {
606
- tabSnapshotRefs$1.delete(tabId);
607
- tabActiveFrameId$2.delete(tabId);
608
- }
609
- async function getSnapshot$1(tabId, options = {}) {
610
- const { interactive = false } = options;
611
- console.log("[DOMService] Getting snapshot for tab:", tabId, { interactive });
612
- await injectBuildDomTreeScript(tabId);
613
- const domTreeResult = await executeBuildDomTree(tabId);
614
- const snapshotResult = interactive ? convertToAccessibilityTree(domTreeResult) : convertToFullTree(domTreeResult);
615
- tabSnapshotRefs$1.set(tabId, snapshotResult.refs);
616
- console.log("[DOMService] Snapshot complete:", {
617
- mode: interactive ? "interactive" : "full",
618
- linesCount: snapshotResult.snapshot.split("\n").length,
619
- refsCount: Object.keys(snapshotResult.refs).length
620
- });
621
- return snapshotResult;
622
- }
623
- function getFrameTarget(tabId) {
624
- const frameId = tabActiveFrameId$2.get(tabId) ?? null;
625
- if (frameId !== null) {
626
- return { tabId, frameIds: [frameId] };
627
- }
628
- return { tabId };
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
- async function injectBuildDomTreeScript(tabId) {
631
- try {
632
- const target = getFrameTarget(tabId);
633
- const checkResults = await chrome.scripting.executeScript({
634
- target,
635
- func: () => Object.prototype.hasOwnProperty.call(window, "buildDomTree")
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
- async function executeBuildDomTree(tabId) {
651
- const target = getFrameTarget(tabId);
652
- const results = await chrome.scripting.executeScript({
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 getRole(node) {
674
- const tagName = node.tagName.toLowerCase();
675
- const role = node.attributes?.role;
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 truncateText(text, maxLength = 50) {
763
- if (text.length <= maxLength) return text;
764
- return text.slice(0, maxLength - 3) + "...";
838
+ function indent(depth) {
839
+ return " ".repeat(depth);
765
840
  }
766
- function isAncestor(ancestorId, descendantId, nodeMap) {
767
- const descendant = nodeMap[descendantId];
768
- if (!descendant || "type" in descendant) return false;
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 convertToAccessibilityTree(result) {
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 { map } = result;
780
- const interactiveNodes = [];
781
- for (const [id, node] of Object.entries(map)) {
782
- if (!node) continue;
783
- if ("type" in node && node.type === "TEXT_NODE") continue;
784
- const elementNode = node;
785
- if (elementNode.highlightIndex !== void 0 && elementNode.highlightIndex !== null) {
786
- interactiveNodes.push({ id, node: elementNode });
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
- const nonSemanticTags = /* @__PURE__ */ new Set(["span", "div", "i", "b", "em", "strong", "small", "svg", "path", "g"]);
798
- const filterableTags = /* @__PURE__ */ new Set(["label"]);
799
- const filteredNodes = [];
800
- for (const item of interactiveNodes) {
801
- const { id, node } = item;
802
- const info = nodeIdToInfo.get(id);
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 (isChildOfInteractive) continue;
816
- let isDuplicate = false;
817
- for (const otherItem of interactiveNodes) {
818
- if (otherItem.id === id) continue;
819
- const otherInfo = nodeIdToInfo.get(otherItem.id);
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
- if (!isDuplicate) filteredNodes.push(item);
826
- }
827
- for (const { id, node } of filteredNodes) {
828
- const refId = String(node.highlightIndex);
829
- const info = nodeIdToInfo.get(id);
830
- let line = `- ${info.role}`;
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
- const elementNode = node;
877
- const tagName = elementNode.tagName.toLowerCase();
878
- if (elementNode.isVisible === false) return;
879
- if (skipTags.has(tagName)) return;
880
- const role = getRole(elementNode);
881
- const name = getAccessibleName(elementNode, map);
882
- const hasRef = elementNode.highlightIndex !== void 0 && elementNode.highlightIndex !== null;
883
- let line = `${indent}- ${role}`;
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
- const displayName = name.length > 50 ? name.slice(0, 47) + "..." : name;
886
- line += ` "${displayName}"`;
887
- }
888
- if (hasRef) {
889
- const refId = String(elementNode.highlightIndex);
890
- line += ` [ref=${refId}]`;
891
- refs[refId] = {
892
- xpath: elementNode.xpath || "",
893
- role,
894
- name: name ? name.length > 100 ? name.slice(0, 97) + "..." : name : void 0,
895
- tagName: elementNode.tagName
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
- for (const childId of elementNode.children || []) {
940
+ if (options.interactive) return;
941
+ for (const childId of node.childIds || []) {
900
942
  traverse(childId, depth + 1);
901
943
  }
902
944
  }
903
- const rootNode = map[rootId];
904
- if (rootNode && !("type" in rootNode)) {
905
- for (const childId of rootNode.children || []) {
906
- traverse(childId, 0);
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 { snapshot: lines.join("\n"), refs };
1003
+ return result.join("\n");
910
1004
  }
911
1005
 
912
- const tabSnapshotRefs = /* @__PURE__ */ new Map();
913
- const tabActiveFrameId$1 = /* @__PURE__ */ new Map();
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 legacy method for tab:", tabId);
942
- const result = await getSnapshot$1(tabId, options);
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, refInfo] of Object.entries(result.refs)) {
1086
+ for (const [refId, axRef] of Object.entries(result.refs)) {
945
1087
  convertedRefs[refId] = {
946
- xpath: refInfo.xpath,
947
- role: refInfo.role,
948
- name: refInfo.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
- snapshot: result.snapshot,
960
- refs: convertedRefs
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 getRefInfo(tabId, ref) {
964
- const refId = ref.startsWith("@") ? ref.slice(1) : ref;
965
- const refs = tabSnapshotRefs.get(tabId);
966
- if (refs?.[refId]) {
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 cleanupTab(tabId) {
979
- tabSnapshotRefs.delete(tabId);
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
- throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
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
- throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
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
- throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
1036
- }
1037
- const { xpath, role, name } = refInfo;
1038
- await evaluate(tabId, `
1039
- (function() {
1040
- const result = document.evaluate(
1041
- ${JSON.stringify(xpath)},
1042
- document,
1043
- null,
1044
- XPathResult.FIRST_ORDERED_NODE_TYPE,
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
- throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
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
- throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
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
- const { xpath } = refInfo;
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
- throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
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
- const { xpath, role, name } = refInfo;
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
- throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
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
- const { xpath, role, name } = refInfo;
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
- throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
1182
- }
1183
- const { xpath, role, name } = refInfo;
1184
- const result = await evaluate(tabId, `
1185
- (function() {
1186
- const selectValue = ${JSON.stringify(value)};
1187
- const result = document.evaluate(
1188
- ${JSON.stringify(xpath)},
1189
- document,
1190
- null,
1191
- XPathResult.FIRST_ORDERED_NODE_TYPE,
1192
- null
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
- element.value = matched.value;
1226
- element.dispatchEvent(new Event('change', { bubbles: true }));
1227
-
1228
- return { selectedValue: matched.value, selectedLabel: matched.textContent.trim() };
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
- throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
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
- const found = await evaluate(tabId, `
1244
- (function() {
1245
- const result = document.evaluate(
1246
- ${JSON.stringify(xpath)},
1247
- document,
1248
- null,
1249
- XPathResult.FIRST_ORDERED_NODE_TYPE,
1250
- null
1251
- );
1252
- return result.singleNodeValue !== null;
1253
- })()
1254
- `);
1255
- if (found) {
1256
- console.log("[CDPDOMService] Element found:", { ref, elapsed });
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$1.set(tabId, frameId);
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
- console.log("[CDPDOMService] Scrolled:", { direction, pixels });
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
- console.log("[CommandHandler] Taking snapshot of tab:", activeTab.id, activeTab.url, { interactive });
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 requests = getNetworkRequests(tabId, filter);
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,