Ver código fonte

后端联调

zjh 1 mês atrás
pai
commit
4bf0710357
6 arquivos alterados com 1408 adições e 59 exclusões
  1. 2 1
      .env
  2. 21 21
      package-lock.json
  3. 1 1
      package.json
  4. 1321 0
      src/public/buildDomTree.js
  5. 61 35
      src/utils/navigator.js
  6. 2 1
      wxt.config.ts

+ 2 - 1
.env

@@ -1,9 +1,10 @@
 VITE_OPENAI_API_KEY_TONG=sk-e9855234f47346049809ce23ed3ebe3f
 VITE_MAX_FILE_NUMBER=10
 VITE_APP_BASE_API='http://192.168.1.5:8000'
-
+#VITE_APP_BASE_API='http://192.168.1.166:5555'
 
 # 接口地址 (WebSocket) 
 VITE_API_WS_URL ='ws://192.168.1.166:7777'
 # 终端ID 
 VITE_CLIENT_ID = '7d489df3ed56b51fcbf4a4f7139885c2'
+#VITE_CLIENT_ID = '7a1958fafed3268a7515b083bd6f144f'

+ 21 - 21
package-lock.json

@@ -22,7 +22,7 @@
         "moment": "^2.30.1",
         "openai": "^4.85.4",
         "pinia": "^3.0.1",
-        "puppeteer-core": "^24.10.2",
+        "puppeteer-core": "^24.8.2",
         "sass": "^1.85.1",
         "uuid": "^11.1.0",
         "vant": "^4.9.17",
@@ -1180,16 +1180,16 @@
       }
     },
     "node_modules/@puppeteer/browsers": {
-      "version": "2.10.5",
-      "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz",
-      "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==",
+      "version": "2.10.4",
+      "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.4.tgz",
+      "integrity": "sha512-9DxbZx+XGMNdjBynIs4BRSz+M3iRDeB7qRcAr6UORFLphCIM2x3DXgOucvADiifcqCE4XePFUKcnaAMyGbrDlQ==",
       "license": "Apache-2.0",
       "dependencies": {
-        "debug": "^4.4.1",
+        "debug": "^4.4.0",
         "extract-zip": "^2.0.1",
         "progress": "^2.0.3",
         "proxy-agent": "^6.5.0",
-        "semver": "^7.7.2",
+        "semver": "^7.7.1",
         "tar-fs": "^3.0.8",
         "yargs": "^17.7.2"
       },
@@ -4179,9 +4179,9 @@
       }
     },
     "node_modules/devtools-protocol": {
-      "version": "0.0.1452169",
-      "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz",
-      "integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==",
+      "version": "0.0.1439962",
+      "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1439962.tgz",
+      "integrity": "sha512-jJF48UdryzKiWhJ1bLKr7BFWUQCEIT5uCNbDLqkQJBtkFxYzILJH44WN0PDKMIlGDN7Utb8vyUY85C3w4R/t2g==",
       "license": "BSD-3-Clause"
     },
     "node_modules/didyoumean": {
@@ -9580,15 +9580,15 @@
       }
     },
     "node_modules/puppeteer-core": {
-      "version": "24.10.2",
-      "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.10.2.tgz",
-      "integrity": "sha512-CnzhOgrZj8DvkDqI+Yx+9or33i3Y9uUYbKyYpP4C13jWwXx/keQ38RMTMmxuLCWQlxjZrOH0Foq7P2fGP7adDQ==",
+      "version": "24.8.2",
+      "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.8.2.tgz",
+      "integrity": "sha512-wNw5cRZOHiFibWc0vdYCYO92QuKTbJ8frXiUfOq/UGJWMqhPoBThTKkV+dJ99YyWfzJ2CfQQ4T1nhhR0h8FlVw==",
       "license": "Apache-2.0",
       "dependencies": {
-        "@puppeteer/browsers": "2.10.5",
+        "@puppeteer/browsers": "2.10.4",
         "chromium-bidi": "5.1.0",
-        "debug": "^4.4.1",
-        "devtools-protocol": "0.0.1452169",
+        "debug": "^4.4.0",
+        "devtools-protocol": "0.0.1439962",
         "typed-query-selector": "^2.12.0",
         "ws": "^8.18.2"
       },
@@ -9597,9 +9597,9 @@
       }
     },
     "node_modules/puppeteer-core/node_modules/ws": {
-      "version": "8.18.2",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
-      "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
+      "version": "8.18.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+      "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
       "license": "MIT",
       "engines": {
         "node": ">=10.0.0"
@@ -11782,9 +11782,9 @@
       }
     },
     "node_modules/tar-fs": {
-      "version": "3.0.10",
-      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.10.tgz",
-      "integrity": "sha512-C1SwlQGNLe/jPNqapK8epDsXME7CAJR5RL3GcE6KWx1d9OUByzoHVcbu1VPI8tevg9H8Alae0AApHHFGzrD5zA==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz",
+      "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==",
       "license": "MIT",
       "dependencies": {
         "pump": "^3.0.0",

+ 1 - 1
package.json

@@ -29,7 +29,7 @@
     "moment": "^2.30.1",
     "openai": "^4.85.4",
     "pinia": "^3.0.1",
-    "puppeteer-core": "^24.10.2",
+    "puppeteer-core": "^24.8.2",
     "sass": "^1.85.1",
     "uuid": "^11.1.0",
     "vant": "^4.9.17",

+ 1321 - 0
src/public/buildDomTree.js

@@ -0,0 +1,1321 @@
+
+window.buildDomTree = (
+  args = {
+    doHighlightElements: true,
+    focusHighlightIndex: -1,
+    viewportExpansion: 0,
+    debugMode: false,
+  },
+) => {
+  const { doHighlightElements, focusHighlightIndex, viewportExpansion, debugMode } = args;
+  let highlightIndex = 0; // Reset highlight index
+
+  // Add timing stack to handle recursion
+  const TIMING_STACK = {
+    nodeProcessing: [],
+    treeTraversal: [],
+    highlighting: [],
+    current: null,
+  };
+
+  function pushTiming(type) {
+    TIMING_STACK[type] = TIMING_STACK[type] || [];
+    TIMING_STACK[type].push(performance.now());
+  }
+
+  function popTiming(type) {
+    const start = TIMING_STACK[type].pop();
+    const duration = performance.now() - start;
+    return duration;
+  }
+
+  // Only initialize performance tracking if in debug mode
+  const PERF_METRICS = debugMode
+    ? {
+        buildDomTreeCalls: 0,
+        timings: {
+          buildDomTree: 0,
+          highlightElement: 0,
+          isInteractiveElement: 0,
+          isElementVisible: 0,
+          isTopElement: 0,
+          isInExpandedViewport: 0,
+          isTextNodeVisible: 0,
+          getEffectiveScroll: 0,
+        },
+        cacheMetrics: {
+          boundingRectCacheHits: 0,
+          boundingRectCacheMisses: 0,
+          computedStyleCacheHits: 0,
+          computedStyleCacheMisses: 0,
+          getBoundingClientRectTime: 0,
+          getComputedStyleTime: 0,
+          boundingRectHitRate: 0,
+          computedStyleHitRate: 0,
+          overallHitRate: 0,
+        },
+        nodeMetrics: {
+          totalNodes: 0,
+          processedNodes: 0,
+          skippedNodes: 0,
+        },
+        buildDomTreeBreakdown: {
+          totalTime: 0,
+          totalSelfTime: 0,
+          buildDomTreeCalls: 0,
+          domOperations: {
+            getBoundingClientRect: 0,
+            getComputedStyle: 0,
+          },
+          domOperationCounts: {
+            getBoundingClientRect: 0,
+            getComputedStyle: 0,
+          },
+        },
+      }
+    : null;
+
+  // Simple timing helper that only runs in debug mode
+  function measureTime(fn) {
+    if (!debugMode) return fn;
+    return function (...args) {
+      const start = performance.now();
+      const result = fn.apply(this, args);
+      const duration = performance.now() - start;
+      return result;
+    };
+  }
+
+  // Helper to measure DOM operations
+  function measureDomOperation(operation, name) {
+    if (!debugMode) return operation();
+
+    const start = performance.now();
+    const result = operation();
+    const duration = performance.now() - start;
+
+    if (PERF_METRICS && name in PERF_METRICS.buildDomTreeBreakdown.domOperations) {
+      PERF_METRICS.buildDomTreeBreakdown.domOperations[name] += duration;
+      PERF_METRICS.buildDomTreeBreakdown.domOperationCounts[name]++;
+    }
+
+    return result;
+  }
+
+  // Add caching mechanisms at the top level
+  const DOM_CACHE = {
+    boundingRects: new WeakMap(),
+    computedStyles: new WeakMap(),
+    clearCache: () => {
+      DOM_CACHE.boundingRects = new WeakMap();
+      DOM_CACHE.computedStyles = new WeakMap();
+    },
+  };
+
+  // Cache helper functions
+  function getCachedBoundingRect(element) {
+    if (!element) return null;
+
+    if (DOM_CACHE.boundingRects.has(element)) {
+      if (debugMode && PERF_METRICS) {
+        PERF_METRICS.cacheMetrics.boundingRectCacheHits++;
+      }
+      return DOM_CACHE.boundingRects.get(element);
+    }
+
+    if (debugMode && PERF_METRICS) {
+      PERF_METRICS.cacheMetrics.boundingRectCacheMisses++;
+    }
+
+    let rect;
+    if (debugMode) {
+      const start = performance.now();
+      rect = element.getBoundingClientRect();
+      const duration = performance.now() - start;
+      if (PERF_METRICS) {
+        PERF_METRICS.buildDomTreeBreakdown.domOperations.getBoundingClientRect += duration;
+        PERF_METRICS.buildDomTreeBreakdown.domOperationCounts.getBoundingClientRect++;
+      }
+    } else {
+      rect = element.getBoundingClientRect();
+    }
+
+    if (rect) {
+      DOM_CACHE.boundingRects.set(element, rect);
+    }
+    return rect;
+  }
+
+  function getCachedComputedStyle(element) {
+    if (!element) return null;
+
+    if (DOM_CACHE.computedStyles.has(element)) {
+      if (debugMode && PERF_METRICS) {
+        PERF_METRICS.cacheMetrics.computedStyleCacheHits++;
+      }
+      return DOM_CACHE.computedStyles.get(element);
+    }
+
+    if (debugMode && PERF_METRICS) {
+      PERF_METRICS.cacheMetrics.computedStyleCacheMisses++;
+    }
+
+    let style;
+    if (debugMode) {
+      const start = performance.now();
+      style = window.getComputedStyle(element);
+      const duration = performance.now() - start;
+      if (PERF_METRICS) {
+        PERF_METRICS.buildDomTreeBreakdown.domOperations.getComputedStyle += duration;
+        PERF_METRICS.buildDomTreeBreakdown.domOperationCounts.getComputedStyle++;
+      }
+    } else {
+      style = window.getComputedStyle(element);
+    }
+
+    if (style) {
+      DOM_CACHE.computedStyles.set(element, style);
+    }
+    return style;
+  }
+
+  /**
+   * Hash map of DOM nodes indexed by their highlight index.
+   *
+   * @type {Object<string, any>}
+   */
+  const DOM_HASH_MAP = {};
+
+  const ID = { current: 0 };
+
+  const HIGHLIGHT_CONTAINER_ID = 'playwright-highlight-container';
+
+  /**
+   * Highlights an element in the DOM and returns the index of the next element.
+   */
+  function highlightElement(element, index, parentIframe = null) {
+    if (!element) return index;
+
+    // Store overlays and the single label for updating
+    const overlays = [];
+    let label = null;
+    let labelWidth = 20; // Approximate label width
+    let labelHeight = 16; // Approximate label height
+
+    try {
+      // Create or get highlight container
+      let container = document.getElementById(HIGHLIGHT_CONTAINER_ID);
+      if (!container) {
+        container = document.createElement('div');
+        container.id = HIGHLIGHT_CONTAINER_ID;
+        container.style.position = 'fixed';
+        container.style.pointerEvents = 'none';
+        container.style.top = '0';
+        container.style.left = '0';
+        container.style.width = '100%';
+        container.style.height = '100%';
+        container.style.zIndex = '2147483647';
+        document.body.appendChild(container);
+      }
+
+      // Get element client rects
+      const rects = element.getClientRects(); // Use getClientRects()
+
+      if (!rects || rects.length === 0) return index; // Exit if no rects
+
+      // Generate a color based on the index
+      const colors = [
+        '#FF0000',
+        '#00FF00',
+        '#0000FF',
+        '#FFA500',
+        '#800080',
+        '#008080',
+        '#FF69B4',
+        '#4B0082',
+        '#FF4500',
+        '#2E8B57',
+        '#DC143C',
+        '#4682B4',
+      ];
+      const colorIndex = index % colors.length;
+      const baseColor = colors[colorIndex];
+      const backgroundColor = baseColor + '1A'; // 10% opacity version of the color
+
+      // Get iframe offset if necessary
+      let iframeOffset = { x: 0, y: 0 };
+      if (parentIframe) {
+        const iframeRect = parentIframe.getBoundingClientRect(); // Keep getBoundingClientRect for iframe offset
+        iframeOffset.x = iframeRect.left;
+        iframeOffset.y = iframeRect.top;
+      }
+
+      // Create highlight overlays for each client rect
+      for (const rect of rects) {
+        if (rect.width === 0 || rect.height === 0) continue; // Skip empty rects
+
+        const overlay = document.createElement('div');
+        overlay.style.position = 'fixed';
+        overlay.style.border = `2px solid ${baseColor}`;
+        overlay.style.backgroundColor = backgroundColor;
+        overlay.style.pointerEvents = 'none';
+        overlay.style.boxSizing = 'border-box';
+
+        const top = rect.top + iframeOffset.y;
+        const left = rect.left + iframeOffset.x;
+
+        overlay.style.top = `${top}px`;
+        overlay.style.left = `${left}px`;
+        overlay.style.width = `${rect.width}px`;
+        overlay.style.height = `${rect.height}px`;
+
+        container.appendChild(overlay);
+        overlays.push({ element: overlay, initialRect: rect }); // Store overlay and its rect
+      }
+
+      // Create and position a single label relative to the first rect
+      const firstRect = rects[0];
+      label = document.createElement('div');
+      label.className = 'playwright-highlight-label';
+      label.style.position = 'fixed';
+      label.style.background = baseColor;
+      label.style.color = 'white';
+      label.style.padding = '1px 4px';
+      label.style.borderRadius = '4px';
+      label.style.fontSize = `${Math.min(12, Math.max(8, firstRect.height / 2))}px`;
+      label.textContent = index;
+
+      labelWidth = label.offsetWidth > 0 ? label.offsetWidth : labelWidth; // Update actual width if possible
+      labelHeight = label.offsetHeight > 0 ? label.offsetHeight : labelHeight; // Update actual height if possible
+
+      const firstRectTop = firstRect.top + iframeOffset.y;
+      const firstRectLeft = firstRect.left + iframeOffset.x;
+
+      let labelTop = firstRectTop + 2;
+      let labelLeft = firstRectLeft + firstRect.width - labelWidth - 2;
+
+      // Adjust label position if first rect is too small
+      if (firstRect.width < labelWidth + 4 || firstRect.height < labelHeight + 4) {
+        labelTop = firstRectTop - labelHeight - 2;
+        labelLeft = firstRectLeft + firstRect.width - labelWidth; // Align with right edge
+        if (labelLeft < iframeOffset.x) labelLeft = firstRectLeft; // Prevent going off-left
+      }
+
+      // Ensure label stays within viewport bounds slightly better
+      labelTop = Math.max(0, Math.min(labelTop, window.innerHeight - labelHeight));
+      labelLeft = Math.max(0, Math.min(labelLeft, window.innerWidth - labelWidth));
+
+      label.style.top = `${labelTop}px`;
+      label.style.left = `${labelLeft}px`;
+
+      container.appendChild(label);
+
+      // Update positions on scroll/resize
+      const updatePositions = () => {
+        const newRects = element.getClientRects(); // Get fresh rects
+        let newIframeOffset = { x: 0, y: 0 };
+
+        if (parentIframe) {
+          const iframeRect = parentIframe.getBoundingClientRect(); // Keep getBoundingClientRect for iframe
+          newIframeOffset.x = iframeRect.left;
+          newIframeOffset.y = iframeRect.top;
+        }
+
+        // Update each overlay
+        overlays.forEach((overlayData, i) => {
+          if (i < newRects.length) {
+            // Check if rect still exists
+            const newRect = newRects[i];
+            const newTop = newRect.top + newIframeOffset.y;
+            const newLeft = newRect.left + newIframeOffset.x;
+
+            overlayData.element.style.top = `${newTop}px`;
+            overlayData.element.style.left = `${newLeft}px`;
+            overlayData.element.style.width = `${newRect.width}px`;
+            overlayData.element.style.height = `${newRect.height}px`;
+            overlayData.element.style.display = newRect.width === 0 || newRect.height === 0 ? 'none' : 'block';
+          } else {
+            // If fewer rects now, hide extra overlays
+            overlayData.element.style.display = 'none';
+          }
+        });
+
+        // If there are fewer new rects than overlays, hide the extras
+        if (newRects.length < overlays.length) {
+          for (let i = newRects.length; i < overlays.length; i++) {
+            overlays[i].element.style.display = 'none';
+          }
+        }
+
+        // Update label position based on the first new rect
+        if (label && newRects.length > 0) {
+          const firstNewRect = newRects[0];
+          const firstNewRectTop = firstNewRect.top + newIframeOffset.y;
+          const firstNewRectLeft = firstNewRect.left + newIframeOffset.x;
+
+          let newLabelTop = firstNewRectTop + 2;
+          let newLabelLeft = firstNewRectLeft + firstNewRect.width - labelWidth - 2;
+
+          if (firstNewRect.width < labelWidth + 4 || firstNewRect.height < labelHeight + 4) {
+            newLabelTop = firstNewRectTop - labelHeight - 2;
+            newLabelLeft = firstNewRectLeft + firstNewRect.width - labelWidth;
+            if (newLabelLeft < newIframeOffset.x) newLabelLeft = firstNewRectLeft;
+          }
+
+          // Ensure label stays within viewport bounds
+          newLabelTop = Math.max(0, Math.min(newLabelTop, window.innerHeight - labelHeight));
+          newLabelLeft = Math.max(0, Math.min(newLabelLeft, window.innerWidth - labelWidth));
+
+          label.style.top = `${newLabelTop}px`;
+          label.style.left = `${newLabelLeft}px`;
+          label.style.display = 'block';
+        } else if (label) {
+          // Hide label if element has no rects anymore
+          label.style.display = 'none';
+        }
+      };
+
+      window.addEventListener('scroll', updatePositions, true); // Use capture phase
+      window.addEventListener('resize', updatePositions);
+
+      // TODO: Add cleanup logic to remove listeners and elements when done.
+
+      return index + 1;
+    } finally {
+      // popTiming('highlighting'); // Assuming this was a typo and should be removed or corrected
+    }
+  }
+
+  /**
+   * Returns an XPath tree string for an element.
+   */
+  function getXPathTree(element, stopAtBoundary = true) {
+    const segments = [];
+    let currentElement = element;
+
+    while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {
+      // Stop if we hit a shadow root or iframe
+      if (
+        stopAtBoundary &&
+        (currentElement.parentNode instanceof ShadowRoot || currentElement.parentNode instanceof HTMLIFrameElement)
+      ) {
+        break;
+      }
+
+      let index = 0;
+      let sibling = currentElement.previousSibling;
+      while (sibling) {
+        if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName === currentElement.nodeName) {
+          index++;
+        }
+        sibling = sibling.previousSibling;
+      }
+
+      const tagName = currentElement.nodeName.toLowerCase();
+      const xpathIndex = index > 0 ? `[${index + 1}]` : '';
+      segments.unshift(`${tagName}${xpathIndex}`);
+
+      currentElement = currentElement.parentNode;
+    }
+
+    return segments.join('/');
+  }
+
+  /**
+   * Checks if a text node is visible.
+   */
+  function isTextNodeVisible(textNode) {
+    try {
+      const range = document.createRange();
+      range.selectNodeContents(textNode);
+      const rects = range.getClientRects(); // Use getClientRects for Range
+
+      if (!rects || rects.length === 0) {
+        return false;
+      }
+
+      let isAnyRectVisible = false;
+      let isAnyRectInViewport = false;
+
+      for (const rect of rects) {
+        // Check size
+        if (rect.width > 0 && rect.height > 0) {
+          isAnyRectVisible = true;
+
+          // Viewport check for this rect
+          if (
+            !(
+              rect.bottom < -viewportExpansion ||
+              rect.top > window.innerHeight + viewportExpansion ||
+              rect.right < -viewportExpansion ||
+              rect.left > window.innerWidth + viewportExpansion
+            ) ||
+            viewportExpansion === -1
+          ) {
+            isAnyRectInViewport = true;
+            break; // Found a visible rect in viewport, no need to check others
+          }
+        }
+      }
+
+      if (!isAnyRectVisible || !isAnyRectInViewport) {
+        return false;
+      }
+
+      // Check parent visibility
+      const parentElement = textNode.parentElement;
+      if (!parentElement) return false;
+
+      try {
+        return (
+          isInViewport &&
+          parentElement.checkVisibility({
+            checkOpacity: true,
+            checkVisibilityCSS: true,
+          })
+        );
+      } catch (e) {
+        // Fallback if checkVisibility is not supported
+        const style = window.getComputedStyle(parentElement);
+        return isInViewport && style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
+      }
+    } catch (e) {
+      console.warn('Error checking text node visibility:', e);
+      return false;
+    }
+  }
+
+  // Helper function to check if element is accepted
+  function isElementAccepted(element) {
+    if (!element || !element.tagName) return false;
+
+    // Always accept body and common container elements
+    const alwaysAccept = new Set(['body', 'div', 'main', 'article', 'section', 'nav', 'header', 'footer']);
+    const tagName = element.tagName.toLowerCase();
+
+    if (alwaysAccept.has(tagName)) return true;
+
+    const leafElementDenyList = new Set(['svg', 'script', 'style', 'link', 'meta', 'noscript', 'template']);
+
+    return !leafElementDenyList.has(tagName);
+  }
+
+  /**
+   * Checks if an element is visible.
+   */
+  function isElementVisible(element) {
+    const style = getCachedComputedStyle(element);
+    return (
+      element.offsetWidth > 0 && element.offsetHeight > 0 && style.visibility !== 'hidden' && style.display !== 'none'
+    );
+  }
+
+  /**
+   * Checks if an element is interactive.
+   *
+   * lots of comments, and uncommented code - to show the logic of what we already tried
+   *
+   * One of the things we tried at the beginning was also to use event listeners, and other fancy class, style stuff -> what actually worked best was just combining most things with computed cursor style :)
+   */
+  function isInteractiveElement(element) {
+    if (!element || element.nodeType !== Node.ELEMENT_NODE) {
+      return false;
+    }
+
+    // Define interactive cursors
+    const interactiveCursors = new Set([
+      'pointer', // Link/clickable elements
+      'move', // Movable elements
+      'text', // Text selection
+      'grab', // Grabbable elements
+      'grabbing', // Currently grabbing
+      'cell', // Table cell selection
+      'copy', // Copy operation
+      'alias', // Alias creation
+      'all-scroll', // Scrollable content
+      'col-resize', // Column resize
+      'context-menu', // Context menu available
+      'crosshair', // Precise selection
+      'e-resize', // East resize
+      'ew-resize', // East-west resize
+      'help', // Help available
+      'n-resize', // North resize
+      'ne-resize', // Northeast resize
+      'nesw-resize', // Northeast-southwest resize
+      'ns-resize', // North-south resize
+      'nw-resize', // Northwest resize
+      'nwse-resize', // Northwest-southeast resize
+      'row-resize', // Row resize
+      's-resize', // South resize
+      'se-resize', // Southeast resize
+      'sw-resize', // Southwest resize
+      'vertical-text', // Vertical text selection
+      'w-resize', // West resize
+      'zoom-in', // Zoom in
+      'zoom-out', // Zoom out
+    ]);
+
+    // Define non-interactive cursors
+    const nonInteractiveCursors = new Set([
+      'not-allowed', // Action not allowed
+      'no-drop', // Drop not allowed
+      'wait', // Processing
+      'progress', // In progress
+      'initial', // Initial value
+      'inherit', // Inherited value
+      //? Let's just include all potentially clickable elements that are not specifically blocked
+      // 'none',        // No cursor
+      // 'default',     // Default cursor
+      // 'auto',        // Browser default
+    ]);
+
+    function doesElementHaveInteractivePointer(element) {
+      if (element.tagName.toLowerCase() === 'html') return false;
+      const style = getCachedComputedStyle(element);
+
+      if (interactiveCursors.has(style.cursor)) return true;
+
+      return false;
+    }
+
+    let isInteractiveCursor = doesElementHaveInteractivePointer(element);
+
+    // Genius fix for almost all interactive elements
+    if (isInteractiveCursor) {
+      return true;
+    }
+
+    const interactiveElements = new Set([
+      'a', // Links
+      'button', // Buttons
+      'input', // All input types (text, checkbox, radio, etc.)
+      'select', // Dropdown menus
+      'textarea', // Text areas
+      'details', // Expandable details
+      'summary', // Summary element (clickable part of details)
+      'label', // Form labels (often clickable)
+      'option', // Select options
+      'optgroup', // Option groups
+      'fieldset', // Form fieldsets (can be interactive with legend)
+      'legend', // Fieldset legends
+    ]);
+
+    // Define explicit disable attributes and properties
+    const explicitDisableTags = new Set([
+      'disabled', // Standard disabled attribute
+      // 'aria-disabled',      // ARIA disabled state
+      'readonly', // Read-only state
+      // 'aria-readonly',     // ARIA read-only state
+      // 'aria-hidden',       // Hidden from accessibility
+      // 'hidden',            // Hidden attribute
+      // 'inert',             // Inert attribute
+      // 'aria-inert',        // ARIA inert state
+      // 'tabindex="-1"',     // Removed from tab order
+      // 'aria-hidden="true"' // Hidden from screen readers
+    ]);
+
+    // handle inputs, select, checkbox, radio, textarea, button and make sure they are not cursor style disabled/not-allowed
+    if (interactiveElements.has(element.tagName.toLowerCase())) {
+      const style = getCachedComputedStyle(element);
+
+      // Check for non-interactive cursor
+      if (nonInteractiveCursors.has(style.cursor)) {
+        return false;
+      }
+
+      // Check for explicit disable attributes
+      for (const disableTag of explicitDisableTags) {
+        if (
+          element.hasAttribute(disableTag) ||
+          element.getAttribute(disableTag) === 'true' ||
+          element.getAttribute(disableTag) === ''
+        ) {
+          return false;
+        }
+      }
+
+      // Check for disabled property on form elements
+      if (element.disabled) {
+        return false;
+      }
+
+      // Check for readonly property on form elements
+      if (element.readOnly) {
+        return false;
+      }
+
+      // Check for inert property
+      if (element.inert) {
+        return false;
+      }
+
+      return true;
+    }
+
+    const tagName = element.tagName.toLowerCase();
+    const role = element.getAttribute('role');
+    const ariaRole = element.getAttribute('aria-role');
+
+    // Added enhancement to capture dropdown interactive elements
+    if (
+      element.classList &&
+      (element.classList.contains('button') ||
+        element.classList.contains('dropdown-toggle') ||
+        element.getAttribute('data-index') ||
+        element.getAttribute('data-toggle') === 'dropdown' ||
+        element.getAttribute('aria-haspopup') === 'true')
+    ) {
+      return true;
+    }
+
+    const interactiveRoles = new Set([
+      'button', // Directly clickable element
+      // 'link',            // Clickable link
+      // 'menuitem',        // Clickable menu item
+      'menuitemradio', // Radio-style menu item (selectable)
+      'menuitemcheckbox', // Checkbox-style menu item (toggleable)
+      'radio', // Radio button (selectable)
+      'checkbox', // Checkbox (toggleable)
+      'tab', // Tab (clickable to switch content)
+      'switch', // Toggle switch (clickable to change state)
+      'slider', // Slider control (draggable)
+      'spinbutton', // Number input with up/down controls
+      'combobox', // Dropdown with text input
+      'searchbox', // Search input field
+      'textbox', // Text input field
+      // 'listbox',         // Selectable list
+      'option', // Selectable option in a list
+      'scrollbar', // Scrollable control
+    ]);
+
+    // Basic role/attribute checks
+    const hasInteractiveRole =
+      interactiveElements.has(tagName) || interactiveRoles.has(role) || interactiveRoles.has(ariaRole);
+
+    if (hasInteractiveRole) return true;
+
+    // check whether element has event listeners
+    try {
+      if (typeof getEventListeners === 'function') {
+        const listeners = getEventListeners(element);
+        const mouseEvents = ['click', 'mousedown', 'mouseup', 'dblclick'];
+        for (const eventType of mouseEvents) {
+          if (listeners[eventType] && listeners[eventType].length > 0) {
+            return true; // Found a mouse interaction listener
+          }
+        }
+      } else {
+        // Fallback: Check common event attributes if getEventListeners is not available
+        const commonMouseAttrs = ['onclick', 'onmousedown', 'onmouseup', 'ondblclick'];
+        if (commonMouseAttrs.some(attr => element.hasAttribute(attr))) {
+          return true;
+        }
+      }
+    } catch (e) {
+      // console.warn(`Could not check event listeners for ${element.tagName}:`, e);
+      // If checking listeners fails, rely on other checks
+    }
+
+    return false;
+  }
+
+  /**
+   * Checks if an element is the topmost element at its position.
+   */
+  function isTopElement(element) {
+    const rects = element.getClientRects(); // Use getClientRects
+
+    if (!rects || rects.length === 0) {
+      return false; // No geometry, cannot be top
+    }
+
+    let isAnyRectInViewport = false;
+    for (const rect of rects) {
+      // Use the same logic as isInExpandedViewport check
+      if (
+        (rect.width > 0 &&
+          rect.height > 0 &&
+          !(
+            // Only check non-empty rects
+            (
+              rect.bottom < -viewportExpansion ||
+              rect.top > window.innerHeight + viewportExpansion ||
+              rect.right < -viewportExpansion ||
+              rect.left > window.innerWidth + viewportExpansion
+            )
+          )) ||
+        viewportExpansion === -1
+      ) {
+        isAnyRectInViewport = true;
+        break;
+      }
+    }
+
+    if (!isAnyRectInViewport) {
+      return false; // All rects are outside the viewport area
+    }
+
+    // Find the correct document context and root element
+    let doc = element.ownerDocument;
+
+    // If we're in an iframe, elements are considered top by default
+    if (doc !== window.document) {
+      return true;
+    }
+
+    // For shadow DOM, we need to check within its own root context
+    const shadowRoot = element.getRootNode();
+    if (shadowRoot instanceof ShadowRoot) {
+      const centerX = rects[Math.floor(rects.length / 2)].left + rects[Math.floor(rects.length / 2)].width / 2;
+      const centerY = rects[Math.floor(rects.length / 2)].top + rects[Math.floor(rects.length / 2)].height / 2;
+
+      try {
+        const topEl = measureDomOperation(() => shadowRoot.elementFromPoint(centerX, centerY), 'elementFromPoint');
+        if (!topEl) return false;
+
+        let current = topEl;
+        while (current && current !== shadowRoot) {
+          if (current === element) return true;
+          current = current.parentElement;
+        }
+        return false;
+      } catch (e) {
+        return true;
+      }
+    }
+
+    // For elements in viewport, check if they're topmost
+    const centerX = rects[Math.floor(rects.length / 2)].left + rects[Math.floor(rects.length / 2)].width / 2;
+    const centerY = rects[Math.floor(rects.length / 2)].top + rects[Math.floor(rects.length / 2)].height / 2;
+
+    try {
+      const topEl = document.elementFromPoint(centerX, centerY);
+      if (!topEl) return false;
+
+      let current = topEl;
+      while (current && current !== document.documentElement) {
+        if (current === element) return true;
+        current = current.parentElement;
+      }
+      return false;
+    } catch (e) {
+      return true;
+    }
+  }
+
+  /**
+   * Checks if an element is within the expanded viewport.
+   */
+  function isInExpandedViewport(element, viewportExpansion) {
+    return true;
+
+    if (viewportExpansion === -1) {
+      return true;
+    }
+
+    const rects = element.getClientRects(); // Use getClientRects
+
+    if (!rects || rects.length === 0) {
+      // Fallback to getBoundingClientRect if getClientRects is empty,
+      // useful for elements like <svg> that might not have client rects but have a bounding box.
+      const boundingRect = getCachedBoundingRect(element);
+      if (!boundingRect || boundingRect.width === 0 || boundingRect.height === 0) {
+        return false;
+      }
+      return !(
+        boundingRect.bottom < -viewportExpansion ||
+        boundingRect.top > window.innerHeight + viewportExpansion ||
+        boundingRect.right < -viewportExpansion ||
+        boundingRect.left > window.innerWidth + viewportExpansion
+      );
+    }
+
+    // Check if *any* client rect is within the viewport
+    for (const rect of rects) {
+      if (rect.width === 0 || rect.height === 0) continue; // Skip empty rects
+
+      if (
+        !(
+          rect.bottom < -viewportExpansion ||
+          rect.top > window.innerHeight + viewportExpansion ||
+          rect.right < -viewportExpansion ||
+          rect.left > window.innerWidth + viewportExpansion
+        )
+      ) {
+        return true; // Found at least one rect in the viewport
+      }
+    }
+
+    return false; // No rects were found in the viewport
+  }
+
+  // Add this new helper function
+  function getEffectiveScroll(element) {
+    let currentEl = element;
+    let scrollX = 0;
+    let scrollY = 0;
+
+    return measureDomOperation(() => {
+      while (currentEl && currentEl !== document.documentElement) {
+        if (currentEl.scrollLeft || currentEl.scrollTop) {
+          scrollX += currentEl.scrollLeft;
+          scrollY += currentEl.scrollTop;
+        }
+        currentEl = currentEl.parentElement;
+      }
+
+      scrollX += window.scrollX;
+      scrollY += window.scrollY;
+
+      return { scrollX, scrollY };
+    }, 'scrollOperations');
+  }
+
+  // Add these helper functions at the top level
+  function isInteractiveCandidate(element) {
+    if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
+
+    const tagName = element.tagName.toLowerCase();
+
+    // Fast-path for common interactive elements
+    const interactiveElements = new Set(['a', 'button', 'input', 'select', 'textarea', 'details', 'summary']);
+
+    if (interactiveElements.has(tagName)) return true;
+
+    // Quick attribute checks without getting full lists
+    const hasQuickInteractiveAttr =
+      element.hasAttribute('onclick') ||
+      element.hasAttribute('role') ||
+      element.hasAttribute('tabindex') ||
+      element.hasAttribute('aria-') ||
+      element.hasAttribute('data-action') ||
+      element.getAttribute('contenteditable') == 'true';
+
+    return hasQuickInteractiveAttr;
+  }
+
+  // --- Define constants for distinct interaction check ---
+  const DISTINCT_INTERACTIVE_TAGS = new Set([
+    'a',
+    'button',
+    'input',
+    'select',
+    'textarea',
+    'summary',
+    'details',
+    'label',
+    'option',
+  ]);
+  const INTERACTIVE_ROLES = new Set([
+    'button',
+    'link',
+    'menuitem',
+    'menuitemradio',
+    'menuitemcheckbox',
+    'radio',
+    'checkbox',
+    'tab',
+    'switch',
+    'slider',
+    'spinbutton',
+    'combobox',
+    'searchbox',
+    'textbox',
+    'listbox',
+    'option',
+    'scrollbar',
+  ]);
+
+  /**
+   * Checks if an element likely represents a distinct interaction
+   * separate from its parent (if the parent is also interactive).
+   */
+  function isElementDistinctInteraction(element) {
+    if (!element || element.nodeType !== Node.ELEMENT_NODE) {
+      return false;
+    }
+
+    const tagName = element.tagName.toLowerCase();
+    const role = element.getAttribute('role');
+
+    // Check if it's an iframe - always distinct boundary
+    if (tagName === 'iframe') {
+      return true;
+    }
+
+    // Check tag name
+    if (DISTINCT_INTERACTIVE_TAGS.has(tagName)) {
+      return true;
+    }
+    // Check interactive roles
+    if (role && INTERACTIVE_ROLES.has(role)) {
+      return true;
+    }
+    // Check contenteditable
+    if (element.isContentEditable || element.getAttribute('contenteditable') === 'true') {
+      return true;
+    }
+    // Check for common testing/automation attributes
+    if (element.hasAttribute('data-testid') || element.hasAttribute('data-cy') || element.hasAttribute('data-test')) {
+      return true;
+    }
+    // Check for explicit onclick handler (attribute or property)
+    if (element.hasAttribute('onclick') || typeof element.onclick === 'function') {
+      return true;
+    }
+    // Check for other common interaction event listeners
+    try {
+      if (typeof getEventListeners === 'function') {
+        const listeners = getEventListeners(element);
+        const interactionEvents = [
+          'mousedown',
+          'mouseup',
+          'keydown',
+          'keyup',
+          'submit',
+          'change',
+          'input',
+          'focus',
+          'blur',
+        ];
+        for (const eventType of interactionEvents) {
+          if (listeners[eventType] && listeners[eventType].length > 0) {
+            return true; // Found a common interaction listener
+          }
+        }
+      } else {
+        // Fallback: Check common event attributes if getEventListeners is not available
+        const commonEventAttrs = [
+          'onmousedown',
+          'onmouseup',
+          'onkeydown',
+          'onkeyup',
+          'onsubmit',
+          'onchange',
+          'oninput',
+          'onfocus',
+          'onblur',
+        ];
+        if (commonEventAttrs.some(attr => element.hasAttribute(attr))) {
+          return true;
+        }
+      }
+    } catch (e) {
+      // console.warn(`Could not check event listeners for ${element.tagName}:`, e);
+      // If checking listeners fails, rely on other checks
+    }
+
+    // Default to false: if it's interactive but doesn't match above,
+    // assume it triggers the same action as the parent.
+    return false;
+  }
+  // --- End distinct interaction check ---
+
+  /**
+   * Handles the logic for deciding whether to highlight an element and performing the highlight.
+   */
+  function handleHighlighting(nodeData, node, parentIframe, isParentHighlighted) {
+    if (!nodeData.isInteractive) return false; // Not interactive, definitely don't highlight
+
+    let shouldHighlight = false;
+    if (!isParentHighlighted) {
+      // Parent wasn't highlighted, this interactive node can be highlighted.
+      shouldHighlight = true;
+    } else {
+      // Parent *was* highlighted. Only highlight this node if it represents a distinct interaction.
+      if (isElementDistinctInteraction(node)) {
+        shouldHighlight = true;
+      } else {
+        // console.log(`Skipping highlight for ${nodeData.tagName} (parent highlighted)`);
+        shouldHighlight = false;
+      }
+    }
+
+    if (shouldHighlight) {
+      // Check viewport status before assigning index and highlighting
+      nodeData.isInViewport = isInExpandedViewport(node, viewportExpansion);
+      if (nodeData.isInViewport) {
+        nodeData.highlightIndex = highlightIndex++;
+
+        if (doHighlightElements) {
+          if (focusHighlightIndex >= 0) {
+            if (focusHighlightIndex === nodeData.highlightIndex) {
+              highlightElement(node, nodeData.highlightIndex, parentIframe);
+            }
+          } else {
+            highlightElement(node, nodeData.highlightIndex, parentIframe);
+          }
+          return true; // Successfully highlighted
+        }
+      } else {
+        // console.log(`Skipping highlight for ${nodeData.tagName} (outside viewport)`);
+      }
+    }
+
+    return false; // Did not highlight
+  }
+
+  /**
+   * Creates a node data object for a given node and its descendants.
+   */
+  function buildDomTree(node, parentIframe = null, isParentHighlighted = false) {
+    if (debugMode) PERF_METRICS.nodeMetrics.totalNodes++;
+
+    if (!node || node.id === HIGHLIGHT_CONTAINER_ID) {
+      if (debugMode) PERF_METRICS.nodeMetrics.skippedNodes++;
+      return null;
+    }
+
+    // Special handling for root node (body)
+    if (node === document.body) {
+      const nodeData = {
+        tagName: 'body',
+        attributes: {},
+        xpath: '/body',
+        children: [],
+      };
+
+      // Process children of body
+      for (const child of node.childNodes) {
+        const domElement = buildDomTree(child, parentIframe, false); // Body's children have no highlighted parent initially
+        if (domElement) nodeData.children.push(domElement);
+      }
+
+      const id = `${ID.current++}`;
+      DOM_HASH_MAP[id] = nodeData;
+      if (debugMode) PERF_METRICS.nodeMetrics.processedNodes++;
+      return id;
+    }
+
+    // Early bailout for non-element nodes except text
+    if (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) {
+      if (debugMode) PERF_METRICS.nodeMetrics.skippedNodes++;
+      return null;
+    }
+
+    // Process text nodes
+    if (node.nodeType === Node.TEXT_NODE) {
+      const textContent = node.textContent.trim();
+      if (!textContent) {
+        if (debugMode) PERF_METRICS.nodeMetrics.skippedNodes++;
+        return null;
+      }
+
+      // Only check visibility for text nodes that might be visible
+      const parentElement = node.parentElement;
+      if (!parentElement || parentElement.tagName.toLowerCase() === 'script') {
+        if (debugMode) PERF_METRICS.nodeMetrics.skippedNodes++;
+        return null;
+      }
+
+      const id = `${ID.current++}`;
+      DOM_HASH_MAP[id] = {
+        type: 'TEXT_NODE',
+        text: textContent,
+        isVisible: isTextNodeVisible(node),
+      };
+      if (debugMode) PERF_METRICS.nodeMetrics.processedNodes++;
+      return id;
+    }
+
+    // Quick checks for element nodes
+    if (node.nodeType === Node.ELEMENT_NODE && !isElementAccepted(node)) {
+      if (debugMode) PERF_METRICS.nodeMetrics.skippedNodes++;
+      return null;
+    }
+
+    // Early viewport check - only filter out elements clearly outside viewport
+    if (viewportExpansion !== -1) {
+      const rect = getCachedBoundingRect(node); // Keep for initial quick check
+      const style = getCachedComputedStyle(node);
+
+      // Skip viewport check for fixed/sticky elements as they may appear anywhere
+      const isFixedOrSticky = style && (style.position === 'fixed' || style.position === 'sticky');
+
+      // Check if element has actual dimensions using offsetWidth/Height (quick check)
+      const hasSize = node.offsetWidth > 0 || node.offsetHeight > 0;
+
+      // Use getBoundingClientRect for the quick OUTSIDE check.
+      // isInExpandedViewport will do the more accurate check later if needed.
+      if (
+        !rect ||
+        (!isFixedOrSticky &&
+          !hasSize &&
+          (rect.bottom < -viewportExpansion ||
+            rect.top > window.innerHeight + viewportExpansion ||
+            rect.right < -viewportExpansion ||
+            rect.left > window.innerWidth + viewportExpansion))
+      ) {
+        // console.log("Skipping node outside viewport (quick check):", node.tagName, rect);
+        if (debugMode) PERF_METRICS.nodeMetrics.skippedNodes++;
+        return null;
+      }
+    }
+
+    // Process element node
+    const nodeData = {
+      tagName: node.tagName.toLowerCase(),
+      attributes: {},
+      xpath: getXPathTree(node, true),
+      children: [],
+    };
+
+    // Get attributes for interactive elements or potential text containers
+    if (
+      isInteractiveCandidate(node) ||
+      node.tagName.toLowerCase() === 'iframe' ||
+      node.tagName.toLowerCase() === 'body'
+    ) {
+      const attributeNames = node.getAttributeNames?.() || [];
+      for (const name of attributeNames) {
+        nodeData.attributes[name] = node.getAttribute(name);
+      }
+    }
+
+    let nodeWasHighlighted = false;
+    // Perform visibility, interactivity, and highlighting checks
+    if (node.nodeType === Node.ELEMENT_NODE) {
+      nodeData.isVisible = isElementVisible(node); // isElementVisible uses offsetWidth/Height, which is fine
+      if (nodeData.isVisible) {
+        nodeData.isTopElement = isTopElement(node);
+        if (nodeData.isTopElement) {
+          nodeData.isInteractive = isInteractiveElement(node);
+          // Call the dedicated highlighting function
+          nodeWasHighlighted = handleHighlighting(nodeData, node, parentIframe, isParentHighlighted);
+        }
+      }
+    }
+
+    // Process children, with special handling for iframes and rich text editors
+    if (node.tagName) {
+      const tagName = node.tagName.toLowerCase();
+
+      // Handle iframes
+      if (tagName === 'iframe') {
+        try {
+          const iframeDoc = node.contentDocument || node.contentWindow?.document;
+          if (iframeDoc) {
+            for (const child of iframeDoc.childNodes) {
+              const domElement = buildDomTree(child, node, false);
+              if (domElement) nodeData.children.push(domElement);
+            }
+          }
+        } catch (e) {
+          console.warn('Unable to access iframe:', e);
+        }
+      }
+      // Handle rich text editors and contenteditable elements
+      else if (
+        node.isContentEditable ||
+        node.getAttribute('contenteditable') === 'true' ||
+        node.id === 'tinymce' ||
+        node.classList.contains('mce-content-body') ||
+        (tagName === 'body' && node.getAttribute('data-id')?.startsWith('mce_'))
+      ) {
+        // Process all child nodes to capture formatted text
+        for (const child of node.childNodes) {
+          const domElement = buildDomTree(child, parentIframe, nodeWasHighlighted);
+          if (domElement) nodeData.children.push(domElement);
+        }
+      } else {
+        // Handle shadow DOM
+        if (node.shadowRoot) {
+          nodeData.shadowRoot = true;
+          for (const child of node.shadowRoot.childNodes) {
+            const domElement = buildDomTree(child, parentIframe, nodeWasHighlighted);
+            if (domElement) nodeData.children.push(domElement);
+          }
+        }
+        // Handle regular elements
+        for (const child of node.childNodes) {
+          // Pass the highlighted status of the *current* node to its children
+          const passHighlightStatusToChild = nodeWasHighlighted || isParentHighlighted;
+          const domElement = buildDomTree(child, parentIframe, passHighlightStatusToChild);
+          if (domElement) nodeData.children.push(domElement);
+        }
+      }
+    }
+
+    // Skip empty anchor tags
+    if (nodeData.tagName === 'a' && nodeData.children.length === 0 && !nodeData.attributes.href) {
+      if (debugMode) PERF_METRICS.nodeMetrics.skippedNodes++;
+      return null;
+    }
+
+    const id = `${ID.current++}`;
+    DOM_HASH_MAP[id] = nodeData;
+    if (debugMode) PERF_METRICS.nodeMetrics.processedNodes++;
+    return id;
+  }
+
+  // After all functions are defined, wrap them with performance measurement
+  // Remove buildDomTree from here as we measure it separately
+  highlightElement = measureTime(highlightElement);
+  isInteractiveElement = measureTime(isInteractiveElement);
+  isElementVisible = measureTime(isElementVisible);
+  isTopElement = measureTime(isTopElement);
+  isInExpandedViewport = measureTime(isInExpandedViewport);
+  isTextNodeVisible = measureTime(isTextNodeVisible);
+  getEffectiveScroll = measureTime(getEffectiveScroll);
+
+  const rootId = buildDomTree(document.body);
+
+  // Clear the cache before starting
+  DOM_CACHE.clearCache();
+
+  // Only process metrics in debug mode
+  if (debugMode && PERF_METRICS) {
+    // Convert timings to seconds and add useful derived metrics
+    Object.keys(PERF_METRICS.timings).forEach(key => {
+      PERF_METRICS.timings[key] = PERF_METRICS.timings[key] / 1000;
+    });
+
+    Object.keys(PERF_METRICS.buildDomTreeBreakdown).forEach(key => {
+      if (typeof PERF_METRICS.buildDomTreeBreakdown[key] === 'number') {
+        PERF_METRICS.buildDomTreeBreakdown[key] = PERF_METRICS.buildDomTreeBreakdown[key] / 1000;
+      }
+    });
+
+    // Add some useful derived metrics
+    if (PERF_METRICS.buildDomTreeBreakdown.buildDomTreeCalls > 0) {
+      PERF_METRICS.buildDomTreeBreakdown.averageTimePerNode =
+        PERF_METRICS.buildDomTreeBreakdown.totalTime / PERF_METRICS.buildDomTreeBreakdown.buildDomTreeCalls;
+    }
+
+    PERF_METRICS.buildDomTreeBreakdown.timeInChildCalls =
+      PERF_METRICS.buildDomTreeBreakdown.totalTime - PERF_METRICS.buildDomTreeBreakdown.totalSelfTime;
+
+    // Add average time per operation to the metrics
+    Object.keys(PERF_METRICS.buildDomTreeBreakdown.domOperations).forEach(op => {
+      const time = PERF_METRICS.buildDomTreeBreakdown.domOperations[op];
+      const count = PERF_METRICS.buildDomTreeBreakdown.domOperationCounts[op];
+      if (count > 0) {
+        PERF_METRICS.buildDomTreeBreakdown.domOperations[`${op}Average`] = time / count;
+      }
+    });
+
+    // Calculate cache hit rates
+    const boundingRectTotal =
+      PERF_METRICS.cacheMetrics.boundingRectCacheHits + PERF_METRICS.cacheMetrics.boundingRectCacheMisses;
+    const computedStyleTotal =
+      PERF_METRICS.cacheMetrics.computedStyleCacheHits + PERF_METRICS.cacheMetrics.computedStyleCacheMisses;
+
+    if (boundingRectTotal > 0) {
+      PERF_METRICS.cacheMetrics.boundingRectHitRate =
+        PERF_METRICS.cacheMetrics.boundingRectCacheHits / boundingRectTotal;
+    }
+
+    if (computedStyleTotal > 0) {
+      PERF_METRICS.cacheMetrics.computedStyleHitRate =
+        PERF_METRICS.cacheMetrics.computedStyleCacheHits / computedStyleTotal;
+    }
+
+    if (boundingRectTotal + computedStyleTotal > 0) {
+      PERF_METRICS.cacheMetrics.overallHitRate =
+        (PERF_METRICS.cacheMetrics.boundingRectCacheHits + PERF_METRICS.cacheMetrics.computedStyleCacheHits) /
+        (boundingRectTotal + computedStyleTotal);
+    }
+  }
+
+  return debugMode ? { rootId, map: DOM_HASH_MAP, perfMetrics: PERF_METRICS } : { rootId, map: DOM_HASH_MAP };
+};

+ 61 - 35
src/utils/navigator.js

@@ -8,15 +8,15 @@ export const navigator=()=> {
     let browser = null;
     async function browsercomm() {
         const tabitem = await getActiveTabId();
-        let tabId=tabitem.id
         console.log(tabitem)
-        return
+        let tabId=tabitem.id
+        console.log(tabId)
         const connectOptions = {
             transport: await ExtensionTransport.connectTab(tabId), // 替换为实际标签ID
             defaultViewport: null,
             protocol: 'cdp'
         };
-
+        console.log(connectOptions)
         let browser;
 
         try {
@@ -50,17 +50,18 @@ export const navigator=()=> {
                     .filter(n => typeof n === "object" && n.highlightIndex !== undefined)
                     .map(n => findDomByXpath(n.xpath))
                     .filter(x => !!x)
-                    .map((item, index) => `[${index}] ${item.outerHTML}}`)
+                    .map((item, index) => `[${index}] ${item.outerHTML}`)
+                    .join("\n")
                 console.log(domList)
                 return {buildDomTree:buildDomTree,domList:domList}
             });
             return {domState:domState, page:page,tabitem:tabitem};
         } finally {
-            if (browser) await browser.disconnect();
+            // if (browser) await browser.disconnect();
         }
     }
     async function browserautomate(domState,page,type,targetIndex) {
-
+console.log(domState,page,type,targetIndex,browser)
         // 查找目标元素
         const targetElement = findElementByIndex(domState.map, targetIndex);
         if (!targetElement) throw new Error('Element not found');
@@ -68,7 +69,7 @@ export const navigator=()=> {
         // 生成增强选择器
         const selector = generateSelector(targetElement);
         console.log(domState, targetElement, selector)
-
+        // const [page] = await browser.pages()
         try {
             await page.waitForSelector(selector, {visible: true, timeout: 10000});
             switch (type) {
@@ -125,9 +126,12 @@ export const navigator=()=> {
 
     function getActiveTabId() {
         return new Promise((resolve) => {
-            chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
-                resolve(tabs[0]);
-            });
+            setTimeout(() => {
+                chrome.tabs.query({active: true}, (tabs) => {
+                    console.log('tabs', tabs)
+                    resolve(tabs[0]);
+                });
+            },500)
         });
     }
 // 辅助函数:通过索引查找元素
@@ -161,41 +165,21 @@ export const navigator=()=> {
             // 查询是否存在指定的标签页
             chrome.tabs.query({windowId: chrome.windows.WINDOW_ID_CURRENT}, async (tabs) => {
                 let tabId = tabs.find(tabs => tabs.url === url);
-                console.log(tabs, tabId)
+                console.log(tabs, tabId,url)
                 if (tabId) {
                     // 如果存在,则激活该标签页
                     await chrome.tabs.update(tabId.id, {active: true});
                     resolve()
                 } else {
                     // 如果不存在,则新打开一个标签页
-                    await chrome.tabs.create({url: url, active: true});
+                    await chrome.tabs.create({url: url});
                     resolve()
                 }
             });
         })
     }
 
-    function getexecuteapi(payload) {
-        console.log(payload)
-        let params = {
-            conversationId: payload.conversationId,
-                messageId: payload.messageId,
-            messageContent: payload.messageContent,
-            planId: payload.id,
-            currentPlanStepId: payload.steps[0].id,
-            envData: {
-            osName: "Google Chrome",
-                osVersion: "137.0.7151.69(正式版本)",
-                osArch: "arm64",
-                url: "",
-                title: "",
-                tabs: [],
-                interactiveElements: ""
-        },
-            resultSummary: null,
-                needSummary: true,
-            success: false
-        }
+    function getexecuteapi(params) {
        return  getexecute(params).then((res)=>{
            return res;
         })
@@ -205,7 +189,26 @@ export const navigator=()=> {
         for(let item of payload.steps){
             console.log(item)
             if(item.stepIndex === 0){
-                let add = await getexecuteapi(payload)
+                let params = {
+                    conversationId: payload.conversationId,
+                    messageId: payload.messageId,
+                    messageContent: payload.messageContent,
+                    planId: payload.id,
+                    currentPlanStepId: item.id,
+                    envData: {
+                        osName: "Google Chrome",
+                        osVersion: "137.0.7151.69(正式版本)",
+                        osArch: "arm64",
+                        url: "",
+                        title:"",
+                        tabs: [],
+                        interactiveElements: ""
+                    },
+                    resultSummary: null,
+                    needSummary: true,
+                    success: false
+                }
+                let add = await getexecuteapi(params)
                 console.log(add)
                 let toolParameters=add.data.plan.steps[0].toolParameters
                 console.log(JSON.parse(toolParameters).action)
@@ -213,8 +216,31 @@ export const navigator=()=> {
                     await switchTabOrOpenNew(JSON.parse(toolParameters).url)
                 }
             }else {
+
                 let browdata =  await  browsercomm()
-                console.log(browdata)
+        console.log(browdata)
+        let params = {
+            conversationId: payload.conversationId,
+            messageId: payload.messageId,
+            messageContent: payload.messageContent,
+            planId: payload.id,
+            currentPlanStepId: item.id,
+            envData: {
+                osName: "Google Chrome",
+                osVersion: "137.0.7151.69(正式版本)",
+                osArch: "arm64",
+                url: browdata.tabitem.url,
+                title: browdata.tabitem.title,
+                tabs: [],
+                interactiveElements: browdata.domState.domList
+            },
+            resultSummary: null,
+            needSummary: true,
+            success: false
+        }
+        let add = await getexecuteapi(params)
+                console.log(add)
+                await  browserautomate(browdata.domState.buildDomTree,browdata.page,'click',1)
             }
         }
     }

+ 2 - 1
wxt.config.ts

@@ -31,7 +31,8 @@ export default defineConfig({
       'activeTab',
       'webNavigation',
       'webRequest',
-      'sidePanel'
+      'sidePanel',
+      "debugger",
     ],
     content_security_policy: {
       extension_pages: "script-src 'self'; object-src 'self';"