/* eslint-disable */ // accessibility-tree-helper.js // Injected script to generate an accessibility-like tree of the visible page // Elements receive stable refs (ref_*) via WeakRef mapping for later reference. (function () { if (window.__ACCESSIBILITY_TREE_HELPER_INITIALIZED__) return; window.__ACCESSIBILITY_TREE_HELPER_INITIALIZED__ = true; // Traversal and output limits to ensure stability on very large/complex pages const MAX_DEPTH = 30; // maximum DOM depth to traverse const MAX_NODES = 4000; // hard limit to avoid long blocking on huge DOMs const MAX_LINE_LABEL = 100; // max characters for a single label in output const REF_MAP_LIMIT = 1000; // limit size of the ref map to keep payload small // Keep a weak map from ref id to elements if (!window.__claudeElementMap) window.__claudeElementMap = {}; if (!window.__claudeRefCounter) window.__claudeRefCounter = 0; /** * Infer ARIA-like role from element * @param {Element} el * @returns {string} */ function inferRole(el) { const role = el.getAttribute('role'); if (role) return role; const tag = el.tagName.toLowerCase(); const type = el.getAttribute('type') || ''; const map = { a: 'link', button: 'button', input: type === 'submit' || type === 'button' ? 'button' : type === 'checkbox' ? 'checkbox' : type === 'radio' ? 'radio' : type === 'file' ? 'button' : 'textbox', select: 'combobox', textarea: 'textbox', h1: 'heading', h2: 'heading', h3: 'heading', h4: 'heading', h5: 'heading', h6: 'heading', img: 'image', nav: 'navigation', main: 'main', header: 'banner', footer: 'contentinfo', section: 'region', article: 'article', aside: 'complementary', form: 'form', table: 'table', ul: 'list', ol: 'list', li: 'listitem', label: 'label', }; return map[tag] || 'generic'; } /** * Derive readable label for element * @param {Element} el * @returns {string} */ function inferLabel(el) { const tag = el.tagName.toLowerCase(); if (tag === 'select') { const sel = /** @type {HTMLSelectElement} */ (el); const opt = sel.querySelector('option[selected]') || sel.options[sel.selectedIndex]; if (opt && opt.textContent) return opt.textContent.trim(); } const aria = el.getAttribute('aria-label'); if (aria && aria.trim()) return aria.trim(); const placeholder = el.getAttribute('placeholder'); if (placeholder && placeholder.trim()) return placeholder.trim(); const title = el.getAttribute('title'); if (title && title.trim()) return title.trim(); const alt = el.getAttribute('alt'); if (alt && alt.trim()) return alt.trim(); if (/** @type {HTMLElement} */ (el).id) { const lab = document.querySelector(`label[for="${/** @type {HTMLElement} */ (el).id}"]`); if (lab && lab.textContent && lab.textContent.trim()) return lab.textContent.trim(); } if (tag === 'input') { const input = /** @type {HTMLInputElement} */ (el); const type = input.getAttribute('type') || ''; const val = input.getAttribute('value'); if (type === 'submit' && val && val.trim()) return val.trim(); if (input.value && input.value.length < 50 && input.value.trim()) return input.value.trim(); } if (['button', 'a', 'summary'].includes(tag)) { let text = ''; for (let i = 0; i < el.childNodes.length; i++) { const n = el.childNodes[i]; if (n.nodeType === Node.TEXT_NODE) text += n.textContent || ''; } if (text.trim()) return text.trim(); } if (/^h[1-6]$/.test(tag)) { const t = el.textContent; if (t && t.trim()) return t.trim().substring(0, MAX_LINE_LABEL); } if (tag === 'img') { const src = el.getAttribute('src'); if (src) { const file = src.split('/').pop()?.split('?')[0]; return `Image: ${file}`; } } let agg = ''; for (let i = 0; i < el.childNodes.length; i++) { const n = el.childNodes[i]; if (n.nodeType === Node.TEXT_NODE) agg += n.textContent || ''; } if (agg && agg.trim() && agg.trim().length >= 3) { const v = agg.trim(); return v.length > 50 ? v.substring(0, 50) + '...' : v; } return ''; } /** * Check if element is visible in DOM * @param {Element} el */ function isVisible(el) { const cs = window.getComputedStyle(/** @type {HTMLElement} */ (el)); if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false; const he = /** @type {HTMLElement} */ (el); return he.offsetWidth > 0 && he.offsetHeight > 0; } /** * Whether the element is interactive * @param {Element} el */ function isInteractive(el) { // Native interactive tags const tag = el.tagName.toLowerCase(); if (['a', 'button', 'input', 'select', 'textarea', 'details', 'summary'].includes(tag)) return true; // Generic interactive hints if (el.getAttribute('onclick') != null) return true; if ( el.getAttribute('tabindex') != null && String(el.getAttribute('tabindex')).trim() !== '' && !String(el.getAttribute('tabindex')).trim().startsWith('-') ) return true; if (el.getAttribute('contenteditable') === 'true') return true; // ARIA roles commonly used by custom elements const role = (el.getAttribute && el.getAttribute('role')) || ''; const interactiveRoles = new Set([ 'button', 'link', 'checkbox', 'radio', 'switch', 'slider', 'option', 'menuitem', 'textbox', 'searchbox', 'combobox', 'spinbutton', 'tab', 'treeitem', ]); if (role && interactiveRoles.has(role.toLowerCase())) return true; // Shadow host case: treat host as interactive if its open shadow root contains // an interactive control (textarea/input/select/button/a or contenteditable). try { const anyEl = /** @type {any} */ (el); const sr = anyEl && anyEl.shadowRoot ? anyEl.shadowRoot : null; if (sr) { const inner = sr.querySelector( 'input, textarea, select, button, a[href], [contenteditable="true"], [role="button"], [role="link"], [role="textbox"], [role="combobox"], [role="searchbox"], [role="menuitem"], [role="option"], [role="switch"], [role="radio"], [role="checkbox"], [role="tab"], [role="slider"]', ); if (inner) return true; } } catch (_) { /* ignore */ } return false; } /** * Structural containers useful to include * @param {Element} el */ function isStructural(el) { const tag = el.tagName.toLowerCase(); if ( [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'nav', 'main', 'header', 'footer', 'section', 'article', 'aside', ].includes(tag) ) return true; return el.getAttribute('role') != null; } /** * Form-ish containers to keep * @param {Element} el */ function isFormishContainer(el) { const tag = el.tagName.toLowerCase(); const role = (el.getAttribute && el.getAttribute('role')) || ''; const id = /** @type {HTMLElement} */ (el).id || ''; // Normalize className for HTML/SVG elements let cls = ''; try { const attr = el.getAttribute && el.getAttribute('class'); if (typeof attr === 'string') cls = attr; else { const cn = /** @type {any} */ (el).className; if (typeof cn === 'string') cls = cn; else if (cn && typeof cn.baseVal === 'string') cls = cn.baseVal; } } catch (e) { /* ignore */ } return ( role === 'search' || role === 'form' || role === 'group' || role === 'toolbar' || role === 'navigation' || tag === 'form' || tag === 'fieldset' || tag === 'nav' || tag === 'legend' || id.includes('search') || cls.includes('search') || id.includes('form') || cls.includes('form') || id.includes('menu') || cls.includes('menu') || id.includes('nav') || cls.includes('nav') ); } // Utility: query CSS across open shadow roots (best-effort) function querySelectorDeepFirst(selector) { try { // Fast path const direct = document.querySelector(selector); if (direct) return direct; } catch (_) {} const visited = new Set(); const stack = [document.documentElement]; while (stack.length) { const node = stack.pop(); if (!node || visited.has(node)) continue; visited.add(node); try { const root = /** @type {any} */ (node).shadowRoot || (node.nodeType === 9 ? node : null); if (root) { try { const hit = root.querySelector(selector); if (hit) return hit; } catch (_) {} } } catch (_) {} // Traverse DOM and shadow roots try { const children = /** @type {Element} */ (node).children || []; for (let i = 0; i < children.length; i++) stack.push(children[i]); const sr = /** @type {any} */ (node).shadowRoot; if (sr && sr.children) { for (let i = 0; i < sr.children.length; i++) stack.push(sr.children[i]); } } catch (_) {} } return null; } /** * Query CSS selector and return match info including uniqueness check. * @param {string} selector - CSS selector to query * @param {boolean} allowMultiple - If true, skip uniqueness check and return first match * @returns {{element: Element | null, matchCount: number, error?: string}} * Note: matchCount is capped at 2 (where 2 means "2 or more") for performance */ function querySelectorWithUniquenessCheck(selector, allowMultiple = false) { const seen = new Set(); let firstMatch = null; let matchCount = 0; const recordMatch = (el) => { if (!(el instanceof Element) || seen.has(el)) return false; seen.add(el); matchCount++; if (!firstMatch) firstMatch = el; // Short-circuit if: // - allowMultiple is true and we found first match (no need to continue) // - allowMultiple is false and we found multiple matches if (allowMultiple && firstMatch) return true; if (!allowMultiple && matchCount >= 2) return true; return false; }; // Query in main document let selectorError = null; try { const directMatches = document.querySelectorAll(selector); for (let i = 0; i < directMatches.length; i++) { if (recordMatch(directMatches[i])) { // Early exit: either found first match (allowMultiple) or found multiple (not allowed) return { element: firstMatch, matchCount: allowMultiple ? 1 : 2 }; } } } catch (e) { selectorError = e; } if (selectorError) { return { element: null, matchCount: 0, error: `Invalid CSS selector "${selector}": ${selectorError.message || selectorError}`, }; } // If allowMultiple and we already have a match, return immediately if (allowMultiple && firstMatch) { return { element: firstMatch, matchCount: 1 }; } // Query in shadow DOMs const visited = new Set(); const stack = [document.documentElement]; while (stack.length) { const node = stack.pop(); if (!node || visited.has(node)) continue; visited.add(node); try { const shadowRoot = /** @type {any} */ (node).shadowRoot; if (shadowRoot) { try { const shadowMatches = shadowRoot.querySelectorAll(selector); for (let i = 0; i < shadowMatches.length; i++) { if (recordMatch(shadowMatches[i])) { // Early exit: either found first match (allowMultiple) or found multiple (not allowed) return { element: firstMatch, matchCount: allowMultiple ? 1 : 2 }; } } } catch (e) { return { element: null, matchCount: 0, error: `Invalid CSS selector "${selector}": ${e.message || e}`, }; } // Add shadow root children to stack try { const shadowChildren = shadowRoot.children || []; for (let i = 0; i < shadowChildren.length; i++) { stack.push(shadowChildren[i]); } } catch (_) {} } } catch (_) {} // Add regular children to stack try { const children = /** @type {Element} */ (node).children || []; for (let i = 0; i < children.length; i++) { stack.push(children[i]); } } catch (_) {} } return { element: firstMatch, matchCount: Math.min(matchCount, 2) }; } /** * Query XPath selector and return match info including uniqueness check. * @param {string} selector - XPath selector to query * @param {boolean} allowMultiple - If true, skip uniqueness check and return first match * @returns {{element: Element | null, matchCount: number, error?: string}} * Note: matchCount is capped at 2 (where 2 means "2 or more") for performance */ function queryXPathWithUniquenessCheck(selector, allowMultiple = false) { if (!selector) { return { element: null, matchCount: 0 }; } try { if (allowMultiple) { // When multiple matches are allowed, use ANY_UNORDERED_NODE_TYPE for performance // This returns just the first match without evaluating the entire result set const result = document.evaluate( selector, document, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null, ); const firstMatch = result.singleNodeValue instanceof Element ? /** @type {Element} */ (result.singleNodeValue) : null; return { element: firstMatch, matchCount: firstMatch ? 1 : 0 }; } else { // When uniqueness is required, use ORDERED_NODE_SNAPSHOT_TYPE to count matches const snapshot = document.evaluate( selector, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null, ); const totalMatches = snapshot.snapshotLength; // Cap at 2 for performance (2 means "2 or more") const matchCount = Math.min(totalMatches, 2); const firstMatch = totalMatches > 0 && snapshot.snapshotItem(0) instanceof Element ? /** @type {Element} */ (snapshot.snapshotItem(0)) : null; return { element: firstMatch, matchCount }; } } catch (e) { return { element: null, matchCount: 0, error: `Invalid XPath "${selector}": ${e.message || e}`, }; } } /** * Whether to include element in tree under config * @param {Element} el * @param {{filter?: 'all'|'interactive'}} cfg */ function shouldInclude(el, cfg) { const tag = el.tagName.toLowerCase(); if (['script', 'style', 'meta', 'link', 'title', 'noscript'].includes(tag)) return false; if (el.getAttribute('aria-hidden') === 'true') return false; if (!isVisible(el)) return false; if (cfg.filter !== 'all') { const r = /** @type {HTMLElement} */ (el).getBoundingClientRect(); if ( !(r.top < window.innerHeight && r.bottom > 0 && r.left < window.innerWidth && r.right > 0) ) return false; } if (cfg.filter === 'interactive') return isInteractive(el); if (isInteractive(el)) return true; if (isStructural(el)) return true; if (inferLabel(el).length > 0) return true; return isFormishContainer(el); } /** * Generate a fairly stable CSS selector * @param {Element} el * @returns {string} */ function generateSelector(el) { if (!(el instanceof Element)) return ''; if (/** @type {HTMLElement} */ (el).id) { const idSel = `#${CSS.escape(/** @type {HTMLElement} */ (el).id)}`; if (document.querySelectorAll(idSel).length === 1) return idSel; } for (const attr of ['data-testid', 'data-cy', 'name']) { const attrValue = el.getAttribute(attr); if (attrValue) { const s = `[${attr}="${CSS.escape(attrValue)}"]`; if (document.querySelectorAll(s).length === 1) return s; } } let path = ''; let current = el; while (current && current.nodeType === Node.ELEMENT_NODE && current.tagName !== 'BODY') { let selector = current.tagName.toLowerCase(); const parent = current.parentElement; if (parent) { const siblings = Array.from(parent.children).filter( (child) => child.tagName === current.tagName, ); if (siblings.length > 1) { const index = siblings.indexOf(current) + 1; selector += `:nth-of-type(${index})`; } } path = path ? `${selector} > ${path}` : selector; current = parent; } return path ? `body > ${path}` : 'body'; } /** * Traverse DOM and build pageContent lines; collect ref map for interactive nodes. * @param {Element} el * @param {number} depth * @param {{filter?: 'all'|'interactive', maxDepth?: number}} cfg * @param {string[]} out * @param {Array<{ref:string, selector:string, rect:{x:number,y:number,width:number,height:number}}>} refMap */ function traverse(el, depth, cfg, out, refMap, state) { const maxDepth = cfg && typeof cfg.maxDepth === 'number' ? cfg.maxDepth : MAX_DEPTH; if (depth > maxDepth || !el || !el.tagName) return; if (state.processed >= MAX_NODES) return; if (state.visited.has(el)) return; state.visited.add(el); const include = shouldInclude(el, cfg) || depth === 0; if (include) { const role = inferRole(el); let label = inferLabel(el); let refId = null; for (const k in window.__claudeElementMap) { if (window.__claudeElementMap[k].deref && window.__claudeElementMap[k].deref() === el) { refId = k; break; } } if (!refId) { refId = `ref_${++window.__claudeRefCounter}`; window.__claudeElementMap[refId] = new WeakRef(el); } const rect = /** @type {HTMLElement} */ (el).getBoundingClientRect(); const cx = Math.round(rect.left + rect.width / 2); const cy = Math.round(rect.top + rect.height / 2); let line = `${' '.repeat(depth)}- ${role}`; if (label) { label = label.replace(/\s+/g, ' ').substring(0, MAX_LINE_LABEL); line += ` "${label.replace(/"/g, '\\"')}"`; } line += ` [ref=${refId}] (x=${cx},y=${cy})`; if (/** @type {HTMLElement} */ (el).id) line += ` id="${/** @type {HTMLElement} */ (el).id}"`; const href = el.getAttribute('href'); if (href) line += ` href="${href}"`; const type = el.getAttribute('type'); if (type) line += ` type="${type}"`; const placeholder = el.getAttribute('placeholder'); if (placeholder) line += ` placeholder="${placeholder}"`; // Surface disabled/pointer-events for better agent judgement try { const disabled = el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true'; if (disabled) line += ` disabled`; const cs = window.getComputedStyle(/** @type {HTMLElement} */ (el)); if (cs && cs.pointerEvents === 'none') line += ` pe=none`; } catch (_) { /* ignore style issues */ } out.push(line); state.included++; state.processed++; // Only collect ref mapping for interactive elements to limit cost if (isInteractive(el) && refMap.length < REF_MAP_LIMIT) { refMap.push({ ref: /** @type {string} */ (refId), selector: generateSelector(el), rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height, }, }); } } if (state.processed >= MAX_NODES) return; // Traverse light DOM children if (/** @type {HTMLElement} */ (el).children && depth < maxDepth) { const children = /** @type {HTMLElement} */ (el).children; for (let i = 0; i < children.length; i++) { if (state.processed >= MAX_NODES) break; traverse(children[i], include ? depth + 1 : depth, cfg, out, refMap, state); } } // Traverse shadow DOM roots (limited by maxDepth and MAX_NODES) try { const anyEl = /** @type {any} */ (el); if (anyEl && anyEl.shadowRoot && depth < maxDepth) { const srChildren = anyEl.shadowRoot.children || []; for (let i = 0; i < srChildren.length; i++) { if (state.processed >= MAX_NODES) break; traverse(srChildren[i], include ? depth + 1 : depth, cfg, out, refMap, state); } } } catch (_) { /* ignore shadow errors */ } } /** * Generate tree and return * @param {'all'|'interactive'|null} filter * @param {{maxDepth?: number, refId?: string}|undefined} options */ function __generateAccessibilityTree(filter, options) { try { const start = performance && performance.now ? performance.now() : Date.now(); const out = []; const cfg = { filter: filter || undefined }; // Clamp maxDepth to MAX_DEPTH to keep costs bounded if (options && Number.isFinite(options.maxDepth)) { const d = Math.max(0, Math.floor(Number(options.maxDepth))); cfg.maxDepth = Math.min(d, MAX_DEPTH); } const refMap = []; const state = { processed: 0, included: 0, visited: new WeakSet() }; // Determine root element (body or refId-specified element) let focus = null; let root = document.body; if (options && options.refId) { const refIdStr = String(options.refId || '').trim(); if (refIdStr) { const el = resolveRef(refIdStr); if (!el || !(el instanceof Element)) { return { error: `ref "${refIdStr}" not found or expired` }; } root = el; focus = { refId: refIdStr }; } } if (root) traverse(root, 0, cfg, out, refMap, state); for (const k in window.__claudeElementMap) { if (!window.__claudeElementMap[k].deref || !window.__claudeElementMap[k].deref()) delete window.__claudeElementMap[k]; } const pageContent = out .filter((line) => !/^\s*- generic \[ref=ref_\d+\]$/.test(line)) .join('\n'); const end = performance && performance.now ? performance.now() : Date.now(); return { pageContent, focus, viewport: { width: window.innerWidth, height: window.innerHeight, dpr: window.devicePixelRatio || 1, }, stats: { processed: state.processed, included: state.included, durationMs: Math.round(end - start), }, refMap, }; } catch (err) { throw new Error( 'Error generating accessibility tree: ' + (err && err.message ? err.message : 'Unknown error'), ); } } // Expose API on window window.__generateAccessibilityTree = __generateAccessibilityTree; // ============================================================================ // Hover for Ref (DOM Fallback Support) // ============================================================================ async function handleHoverForRef(ref) { if (!ref) return { success: false, error: 'ref is required' }; const el = resolveRef(ref); if (el) { dispatchHoverEvents(el); return { success: true, target: summarizeElement(el) }; } return await forwardHoverRefToChildren(ref); } function resolveRef(ref) { const map = window.__claudeElementMap || {}; const weak = map[ref]; return weak && typeof weak.deref === 'function' ? weak.deref() : null; } function dispatchHoverEvents(el) { const rect = el.getBoundingClientRect(); const center = { x: Math.round(rect.left + rect.width / 2), y: Math.round(rect.top + rect.height / 2), }; ['mousemove', 'mouseover', 'mouseenter'].forEach((type) => { el.dispatchEvent( new MouseEvent(type, { bubbles: true, cancelable: true, clientX: center.x, clientY: center.y, view: window, }), ); }); } function summarizeElement(el) { return { tagName: el.tagName, id: el.id || '', className: el.className || '', text: (el.textContent || '').trim().slice(0, 100), }; } function forwardHoverRefToChildren(ref) { return new Promise((resolve) => { const frames = Array.from(document.querySelectorAll('iframe, frame')); if (!frames.length) { resolve({ success: false, error: `ref "${ref}" not found` }); return; } const reqId = `hover_ref_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; const listener = (ev) => { const data = ev?.data; if (!data || data.type !== 'rr-bridge-hover-ref-result' || data.reqId !== reqId) return; window.removeEventListener('message', listener, true); resolve(data.result); }; window.addEventListener('message', listener, true); setTimeout(() => { window.removeEventListener('message', listener, true); resolve({ success: false, error: `ref "${ref}" not found in child frames` }); }, 1500); for (const frame of frames) { try { frame.contentWindow?.postMessage({ type: 'rr-bridge-hover-ref', reqId, ref }, '*'); } catch {} } }); } // Chrome message bridge for ping and tree generation chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => { try { if (request && request.action === 'chrome_read_page_ping') { sendResponse({ status: 'pong' }); return false; } if (request && request.action === 'rr_overlay') { try { const cmd = request.cmd || 'init'; let root = document.getElementById('__rr_overlay_root'); if (!root) { root = document.createElement('div'); root.id = '__rr_overlay_root'; Object.assign(root.style, { position: 'fixed', right: '8px', bottom: '8px', zIndex: 2_147_483_647, maxWidth: '40vw', maxHeight: '40vh', overflow: 'auto', background: 'rgba(0,0,0,0.6)', color: '#fff', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', fontSize: '12px', padding: '8px', borderRadius: '6px', boxShadow: '0 2px 8px rgba(0,0,0,0.3)', }); const title = document.createElement('div'); title.textContent = 'Record-Replay 运行日志'; Object.assign(title.style, { fontWeight: 'bold', marginBottom: '6px' }); const body = document.createElement('div'); body.id = '__rr_overlay_body'; root.appendChild(title); root.appendChild(body); document.documentElement.appendChild(root); } const body = document.getElementById('__rr_overlay_body'); if (cmd === 'append' && body) { const line = document.createElement('div'); line.textContent = String(request.text || ''); body.appendChild(line); body.scrollTop = body.scrollHeight; } if (cmd === 'done' && root) { root.style.opacity = '0.5'; } sendResponse({ success: true }); return true; } catch (e) { sendResponse({ success: false, error: String(e && e.message ? e.message : e) }); return true; } } // Element picker: start a temporary overlay to let user pick an element if (request && request.action === 'rr_picker_start') { try { // state const state = { active: true }; const hostId = '__rr_picker_host__'; let host = document.getElementById(hostId); if (host) host.remove(); host = document.createElement('div'); host.id = hostId; Object.assign(host.style, { position: 'fixed', inset: '0', zIndex: 2147483646, cursor: 'crosshair', background: 'rgba(0,0,0,0.0)', }); const box = document.createElement('div'); Object.assign(box.style, { position: 'fixed', border: '2px solid #3b82f6', background: 'rgba(59,130,246,0.15)', pointerEvents: 'none', }); const tip = document.createElement('div'); tip.textContent = '点击选取元素(Esc 取消)'; Object.assign(tip.style, { position: 'fixed', top: '10px', left: '10px', background: 'rgba(0,0,0,0.7)', color: '#fff', padding: '6px 10px', borderRadius: '6px', fontSize: '12px', fontFamily: 'system-ui,-apple-system,Segoe UI,Roboto,Arial', }); host.appendChild(box); host.appendChild(tip); document.documentElement.appendChild(host); const cleanup = () => { try { host.remove(); } catch {} try { document.removeEventListener('mousemove', onMove, true); } catch {} try { document.removeEventListener('click', onClick, true); } catch {} try { document.removeEventListener('keydown', onKey, true); } catch {} state.active = false; }; const onMove = (e) => { if (!state.active) return; const el = e.target instanceof Element ? e.target : null; if (!el) return; try { const r = el.getBoundingClientRect(); Object.assign(box.style, { left: `${Math.round(r.left)}px`, top: `${Math.round(r.top)}px`, width: `${Math.round(Math.max(0, r.width))}px`, height: `${Math.round(Math.max(0, r.height))}px`, display: r.width > 0 && r.height > 0 ? 'block' : 'none', }); } catch {} }; const uniqueClassSelector = (node) => { try { const classes = Array.from(node.classList || []).filter( (c) => c && /^[a-zA-Z0-9_-]+$/.test(c), ); for (const cls of classes) { const sel = `.${CSS.escape(cls)}`; if (document.querySelectorAll(sel).length === 1) return sel; } const tag = node.tagName ? node.tagName.toLowerCase() : ''; for (const cls of classes) { const sel = `${tag}.${CSS.escape(cls)}`; if (document.querySelectorAll(sel).length === 1) return sel; } for (let i = 0; i < Math.min(classes.length, 3); i++) { for (let j = i + 1; j < Math.min(classes.length, 3); j++) { const sel = `.${CSS.escape(classes[i])}.${CSS.escape(classes[j])}`; if (document.querySelectorAll(sel).length === 1) return sel; } } } catch {} return ''; }; const computeCandidates = (el) => { const cands = []; // css by id / class / short path if (el.id) { const idSel = `#${CSS.escape(el.id)}`; if (document.querySelectorAll(idSel).length === 1) cands.push({ type: 'css', value: idSel }); } const classSel = uniqueClassSelector(el); if (classSel) cands.push({ type: 'css', value: classSel }); // data-* and name for (const attr of ['data-testid', 'data-cy', 'name']) { const val = el.getAttribute(attr); if (val) { const s = `[${attr}="${CSS.escape(val)}"]`; if (document.querySelectorAll(s).length === 1) cands.push({ type: 'attr', value: s }); } } // aria const aria = el.getAttribute && el.getAttribute('aria-label'); if (aria) cands.push({ type: 'aria', value: `textbox[name=${aria}]` }); // text for clickable const tag = (el.tagName || '').toLowerCase(); if (['button', 'a', 'summary'].includes(tag)) { const text = (el.textContent || '').trim(); if (text) cands.push({ type: 'text', value: text.substring(0, 64) }); } // fallback path selector const gen = (node) => { if (!(node instanceof Element)) return ''; let path = ''; let current = node; while ( current && current.nodeType === Node.ELEMENT_NODE && current.tagName !== 'BODY' ) { let sel = current.tagName.toLowerCase(); const parent = current.parentElement; if (parent) { const siblings = Array.from(parent.children).filter( (child) => child.tagName === current.tagName, ); if (siblings.length > 1) { const index = siblings.indexOf(current) + 1; sel += `:nth-of-type(${index})`; } } path = path ? `${sel} > ${path}` : sel; current = parent; } return path ? `body > ${path}` : 'body'; }; const pathSel = gen(el); if (pathSel) cands.push({ type: 'css', value: pathSel }); return cands; }; const onClick = (e) => { if (!state.active) return; e.preventDefault(); e.stopPropagation(); const el = e.target instanceof Element ? e.target : null; if (!el) { cleanup(); sendResponse({ success: false, error: 'no element' }); return true; } // create ref try { if (!window.__claudeElementMap) window.__claudeElementMap = {}; if (!window.__claudeRefCounter) window.__claudeRefCounter = 0; } catch {} let refId = null; try { for (const k in window.__claudeElementMap) { if ( window.__claudeElementMap[k].deref && window.__claudeElementMap[k].deref() === el ) { refId = k; break; } } if (!refId) { refId = `ref_${++window.__claudeRefCounter}`; window.__claudeElementMap[refId] = new WeakRef(el); } } catch {} const cands = computeCandidates(el); cleanup(); sendResponse({ success: true, ref: refId, candidates: cands }); return true; }; const onKey = (e) => { if (e.key === 'Escape') { cleanup(); sendResponse({ success: false, cancelled: true }); } }; document.addEventListener('mousemove', onMove, true); document.addEventListener('click', onClick, true); document.addEventListener('keydown', onKey, true); return true; // async } catch (e) { sendResponse({ success: false, error: String(e && e.message ? e.message : e) }); return true; } } if (request && request.action === 'rr_picker_stop') { try { const host = document.getElementById('__rr_picker_host__'); if (host) host.remove(); sendResponse({ success: true }); return true; } catch (e) { sendResponse({ success: false, error: String(e && e.message ? e.message : e) }); return true; } } if (request && request.action === 'generateAccessibilityTree') { const result = __generateAccessibilityTree(request.filter || null, { maxDepth: request.depth, refId: request.refId, }); if (result && result.error) { sendResponse({ success: false, error: result.error }); return true; } sendResponse({ success: true, ...result }); return true; } if (request && request.action === 'ensureRefForSelector') { try { // Composite selector support: "frameSelector |> innerSelector" const maybeSel = String(request.selector || '').trim(); const allowMultiple = !!request.allowMultiple; if (maybeSel.includes('|>')) { try { const parts = maybeSel .split('|>') .map((s) => s.trim()) .filter(Boolean); if (parts.length >= 2) { const frameSel = parts[0]; const innerSel = parts.slice(1).join(' |> '); // Find target frame element in current document let frameEl = null; try { frameEl = querySelectorDeepFirst(frameSel) || document.querySelector(frameSel); } catch {} if ( !frameEl || !(frameEl instanceof HTMLIFrameElement || frameEl instanceof HTMLFrameElement) ) { sendResponse({ success: false, error: `Composite frame selector not found: ${frameSel}`, }); return true; } const cw = frameEl.contentWindow; if (!cw) { sendResponse({ success: false, error: 'Unable to obtain contentWindow of target frame', }); return true; } // Bridge to child frame via postMessage with timeout const reqId = `rrc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; const BRIDGE_TIMEOUT_MS = 5000; // 5 second timeout for iframe bridge let responded = false; let timeoutHandle = null; const cleanup = () => { window.removeEventListener('message', listener, true); if (timeoutHandle) { clearTimeout(timeoutHandle); timeoutHandle = null; } }; const listener = (ev) => { try { const data = ev && ev.data; if ( !data || data.type !== 'rr-bridge-ensure-ref-result' || data.reqId !== reqId ) return; // Validate source is the expected frame (security check) if (ev.source !== cw) return; if (responded) return; // Already timed out responded = true; cleanup(); if (data.success) { sendResponse({ success: true, ref: data.ref, center: data.center, href: data.href, }); } else { sendResponse({ success: false, error: data.error || 'child failed' }); } } catch (e) { if (!responded) { responded = true; cleanup(); sendResponse({ success: false, error: String(e && e.message ? e.message : e), }); } } }; // Set up timeout to prevent infinite wait timeoutHandle = setTimeout(() => { if (!responded) { responded = true; cleanup(); sendResponse({ success: false, error: `iframe bridge timeout after ${BRIDGE_TIMEOUT_MS}ms`, }); } }, BRIDGE_TIMEOUT_MS); window.addEventListener('message', listener, true); cw.postMessage( { type: 'rr-bridge-ensure-ref', reqId, selector: innerSel, useText: !!request.useText, isXPath: !!request.isXPath, tagName: String(request.tagName || ''), allowMultiple: !!request.allowMultiple, }, '*', ); return true; // async response via message bridge } } catch (e) { sendResponse({ success: false, error: String(e && e.message ? e.message : e) }); return true; } } // Support CSS selector, XPath, or visible text search const useText = !!request.useText; const textQuery = String(request.text || '').trim(); const sel = String(request.selector || '').trim(); const isXPath = !!request.isXPath; const limitTag = String(request.tagName || '') .trim() .toUpperCase(); let el = null; if (useText && textQuery) { const normalize = (s) => String(s || '') .replace(/\s+/g, ' ') .trim() .toLowerCase(); const query = normalize(textQuery); const bigrams = (s) => { const arr = []; for (let i = 0; i < s.length - 1; i++) arr.push(s.slice(i, i + 2)); return arr; }; const dice = (a, b) => { if (!a || !b) return 0; const A = bigrams(a); const B = bigrams(b); if (A.length === 0 || B.length === 0) return 0; let inter = 0; const map = new Map(); for (const t of A) map.set(t, (map.get(t) || 0) + 1); for (const t of B) { const c = map.get(t) || 0; if (c > 0) { inter++; map.set(t, c - 1); } } return (2 * inter) / (A.length + B.length); }; let best = { el: null, score: 0 }; // Deep traversal including shadow roots const stack = [document.documentElement]; let visited = 0; while (stack.length) { const node = /** @type {any} */ (stack.pop()); if (!node || !(node instanceof Element)) continue; try { if (limitTag && String(node.tagName || '').toUpperCase() !== limitTag) { // still traverse into children/shadow for performance? yes } else { const cs = window.getComputedStyle(node); if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') { /* skip hidden */ } else { const rect = /** @type {HTMLElement} */ (node).getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { const txt = normalize(node.textContent || ''); if (txt) { if (txt.includes(query)) { el = /** @type {Element} */ (node); break; } const sc = dice(txt, query); if (sc > best.score) best = { el: /** @type {Element} */ (node), score: sc }; } } } } } catch {} // push children and shadow children try { const children = node.children || []; for (let i = 0; i < children.length; i++) stack.push(children[i]); } catch {} try { const sr = node.shadowRoot; if (sr && sr.children) { for (let i = 0; i < sr.children.length; i++) stack.push(sr.children[i]); } } catch {} if (++visited > 8000) break; } if (!el && best.el && best.score >= 0.6) el = best.el; } else if (isXPath) { if (!sel) { sendResponse({ success: false, error: 'selector is required' }); return true; } const result = queryXPathWithUniquenessCheck(sel, allowMultiple); if (result.error) { sendResponse({ success: false, error: result.error }); return true; } if (result.matchCount === 0) { sendResponse({ success: false, error: `selector not found: ${sel}` }); return true; } if (!allowMultiple && result.matchCount > 1) { sendResponse({ success: false, error: `Selector "${sel}" matched multiple elements. Please refine the selector to match only one element.`, }); return true; } el = result.element; } else { if (!sel) { sendResponse({ success: false, error: 'selector is required' }); return true; } const result = querySelectorWithUniquenessCheck(sel, allowMultiple); if (result.error) { sendResponse({ success: false, error: result.error }); return true; } if (result.matchCount === 0) { sendResponse({ success: false, error: `selector not found: ${sel}` }); return true; } if (!allowMultiple && result.matchCount > 1) { sendResponse({ success: false, error: `Selector "${sel}" matched multiple elements. Please refine the selector to match only one element.`, }); return true; } el = result.element; } if (!el) { sendResponse({ success: false, error: `selector not found: ${sel}` }); return true; } let refId = null; for (const k in window.__claudeElementMap) { if (window.__claudeElementMap[k].deref && window.__claudeElementMap[k].deref() === el) { refId = k; break; } } if (!refId) { refId = `ref_${++window.__claudeRefCounter}`; window.__claudeElementMap[refId] = new WeakRef(el); } const rect = /** @type {HTMLElement} */ (el).getBoundingClientRect(); sendResponse({ success: true, ref: refId, center: { x: Math.round(rect.left + rect.width / 2), y: Math.round(rect.top + rect.height / 2), }, }); return true; } catch (e) { sendResponse({ success: false, error: String(e && e.message ? e.message : e) }); return true; } } if (request && request.action === 'dispatchHoverForRef') { handleHoverForRef(String(request.ref || '').trim()) .then((result) => sendResponse(result)) .catch((error) => sendResponse({ success: false, error: error?.message || String(error) }), ); return true; } if (request && request.action === 'getAttributeForSelector') { try { const sel = String(request.selector || '').trim(); const name = String(request.name || '').trim(); if (!sel || !name) { sendResponse({ success: false, error: 'selector and name are required' }); return true; } const el = document.querySelector(sel) || querySelectorDeepFirst(sel); if (!el) { sendResponse({ success: false, error: `selector not found: ${sel}` }); return true; } let value = null; if (name === 'text' || name === 'textContent') { value = (el.textContent || '').trim(); } else if (name === 'value') { try { value = /** @type {HTMLInputElement} */ (el).value ?? null; } catch (_) { value = el.getAttribute('value'); } } else { value = el.getAttribute(name); } sendResponse({ success: true, value }); return true; } catch (e) { sendResponse({ success: false, error: String(e && e.message ? e.message : e) }); return true; } } if (request && request.action === 'collectVariables') { try { let vars = Array.isArray(request.variables) ? request.variables : []; if ((!vars || vars.length === 0) && request.payload) { try { const p = JSON.parse(String(request.payload || '{}')); if (Array.isArray(p.variables)) vars = p.variables; } catch {} } const useOverlay = request.useOverlay !== false; // default true const values = {}; if (!useOverlay) { for (const v of vars) { const key = String(v && v.key ? v.key : ''); if (!key) continue; const label = v.label || key; const def = v.default || ''; const promptText = `请输入参数 ${label} (${key})`; let val = window.prompt(promptText, def); if (typeof val !== 'string') val = def; values[key] = val; } sendResponse({ success: true, values }); return true; } // Build overlay form const hostId = '__rr_var_overlay__'; let host = document.getElementById(hostId); if (host) host.remove(); host = document.createElement('div'); host.id = hostId; Object.assign(host.style, { position: 'fixed', inset: '0', background: 'rgba(0,0,0,0.35)', zIndex: 2147483646, display: 'flex', alignItems: 'center', justifyContent: 'center', }); const panel = document.createElement('div'); Object.assign(panel.style, { background: '#fff', borderRadius: '8px', width: 'min(520px, 96vw)', maxHeight: '80vh', overflow: 'auto', boxShadow: '0 8px 24px rgba(0,0,0,0.2)', padding: '16px', fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif', }); const title = document.createElement('div'); title.textContent = '请输入回放参数'; Object.assign(title.style, { fontSize: '16px', fontWeight: '600', marginBottom: '12px' }); const form = document.createElement('form'); for (const v of vars) { const row = document.createElement('div'); Object.assign(row.style, { marginBottom: '10px' }); const label = document.createElement('label'); label.textContent = `${v.label || v.key}${v.sensitive ? ' (敏感)' : ''}`; Object.assign(label.style, { display: 'block', marginBottom: '6px', fontWeight: '500', }); const input = document.createElement('input'); input.type = v.sensitive ? 'password' : 'text'; input.name = String(v.key); input.value = String(v.default || ''); Object.assign(input.style, { width: '100%', boxSizing: 'border-box', padding: '8px 10px', border: '1px solid #d0d7de', borderRadius: '6px', outline: 'none', }); row.appendChild(label); row.appendChild(input); form.appendChild(row); } const actions = document.createElement('div'); Object.assign(actions.style, { display: 'flex', gap: '8px', marginTop: '12px' }); const ok = document.createElement('button'); ok.type = 'submit'; ok.textContent = '确定'; Object.assign(ok.style, { background: '#0969da', color: '#fff', border: 'none', padding: '8px 16px', borderRadius: '6px', cursor: 'pointer', }); const cancel = document.createElement('button'); cancel.type = 'button'; cancel.textContent = '取消'; Object.assign(cancel.style, { background: '#f3f4f6', color: '#111', border: '1px solid #d0d7de', padding: '8px 16px', borderRadius: '6px', cursor: 'pointer', }); actions.appendChild(ok); actions.appendChild(cancel); panel.appendChild(title); panel.appendChild(form); panel.appendChild(actions); host.appendChild(panel); document.documentElement.appendChild(host); const cleanup = () => { try { host.remove(); } catch {} }; cancel.onclick = () => { cleanup(); sendResponse({ success: false, cancelled: true }); }; form.onsubmit = (e) => { e.preventDefault(); for (const v of vars) { const el = form.querySelector(`input[name="${CSS.escape(String(v.key))}"]`); if (el) values[v.key] = /** @type {HTMLInputElement} */ (el).value; } cleanup(); sendResponse({ success: true, values }); }; return true; // async } catch (e) { sendResponse({ success: false, error: String(e && e.message ? e.message : e) }); return true; } } if (request && request.action === 'resolveRef') { const ref = request.ref; try { const map = window.__claudeElementMap; const weak = map && map[ref]; const el = weak && typeof weak.deref === 'function' ? weak.deref() : null; if (!el || !(el instanceof Element)) { sendResponse({ success: false, error: `ref "${ref}" not found or expired` }); return true; } const rect = /** @type {HTMLElement} */ (el).getBoundingClientRect(); sendResponse({ success: true, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, center: { x: Math.round(rect.left + rect.width / 2), y: Math.round(rect.top + rect.height / 2), }, selector: (function () { // Simple selector generation inline to avoid duplication const generateSelector = function (node) { if (!(node instanceof Element)) return ''; if (node.id) { const idSel = `#${CSS.escape(node.id)}`; if (document.querySelectorAll(idSel).length === 1) return idSel; } // prefer unique class selectors if available try { const classes = Array.from(node.classList || []).filter( (c) => c && /^[a-zA-Z0-9_-]+$/.test(c), ); for (const cls of classes) { const sel = `.${CSS.escape(cls)}`; if (document.querySelectorAll(sel).length === 1) return sel; } const tag = node.tagName ? node.tagName.toLowerCase() : ''; for (const cls of classes) { const sel = `${tag}.${CSS.escape(cls)}`; if (document.querySelectorAll(sel).length === 1) return sel; } for (let i = 0; i < Math.min(classes.length, 3); i++) { for (let j = i + 1; j < Math.min(classes.length, 3); j++) { const sel = `.${CSS.escape(classes[i])}.${CSS.escape(classes[j])}`; if (document.querySelectorAll(sel).length === 1) return sel; } } } catch {} for (const attr of ['data-testid', 'data-cy', 'name']) { const val = node.getAttribute(attr); if (val) { const s = `[${attr}="${CSS.escape(val)}"]`; if (document.querySelectorAll(s).length === 1) return s; } } let path = ''; let current = node; while ( current && current.nodeType === Node.ELEMENT_NODE && current.tagName !== 'BODY' ) { let sel = current.tagName.toLowerCase(); const parent = current.parentElement; if (parent) { const siblings = Array.from(parent.children).filter( (c) => c.tagName === current.tagName, ); if (siblings.length > 1) { const idx = siblings.indexOf(current) + 1; sel += `:nth-of-type(${idx})`; } } path = path ? `${sel} > ${path}` : sel; current = parent; } return path ? `body > ${path}` : 'body'; }; return generateSelector(el); })(), }); return true; } catch (e) { sendResponse({ success: false, error: String(e && e.message ? e.message : e) }); return true; } } if (request && request.action === 'verifyFingerprint') { try { const ref = String(request.ref || '').trim(); const fingerprint = String(request.fingerprint || '').trim(); if (!ref || !fingerprint) { sendResponse({ success: false, error: 'ref and fingerprint are required' }); return true; } const map = window.__claudeElementMap; const weak = map && map[ref]; const el = weak && typeof weak.deref === 'function' ? weak.deref() : null; if (!el || !(el instanceof Element)) { sendResponse({ success: false, error: `ref "${ref}" not found or expired` }); return true; } // 验证指纹:解析存储的指纹并与当前元素对比 const parts = fingerprint.split('|'); const storedTag = parts[0] || 'unknown'; const currentTag = el.tagName ? String(el.tagName).toLowerCase() : 'unknown'; // Tag 必须匹配 if (storedTag !== currentTag) { sendResponse({ success: true, match: false }); return true; } // 如果存储的指纹有 id,当前元素必须有相同的 id const storedIdPart = parts.find((p) => p.startsWith('id=')); if (storedIdPart) { const storedId = storedIdPart.slice(3); const currentId = el.id ? String(el.id).trim() : ''; if (storedId !== currentId) { sendResponse({ success: true, match: false }); return true; } } sendResponse({ success: true, match: true }); return true; } catch (e) { sendResponse({ success: false, error: String(e && e.message ? e.message : e) }); return true; } } if (request && request.action === 'focusByRef') { try { const ref = String(request.ref || ''); const map = window.__claudeElementMap || {}; const weak = map[ref]; const el = weak && typeof weak.deref === 'function' ? weak.deref() : null; if (!el || !(el instanceof Element)) { sendResponse({ success: false, error: `ref "${ref}" not found or expired` }); return true; } try { /** @type {HTMLElement} */ (el).scrollIntoView({ behavior: 'instant', block: 'center', inline: 'nearest', }); } catch {} try { /** @type {HTMLElement} */ (el).focus && /** @type {HTMLElement} */ (el).focus(); } catch {} sendResponse({ success: true }); return true; } catch (e) { sendResponse({ success: false, error: String(e && e.message ? e.message : e) }); return true; } } } catch (e) { sendResponse({ success: false, error: e && e.message ? e.message : String(e) }); return true; } return false; }); console.log('Accessibility tree helper script loaded'); // Cross-frame bridge: child listens for ensure-ref requests from parent (composite selector) try { window.addEventListener( 'message', (ev) => { try { const data = ev && ev.data; // Handle hover-ref bridge requests from parent frame if (data && data.type === 'rr-bridge-hover-ref') { handleHoverForRef(data.ref) .then((result) => { ev.source?.postMessage( { type: 'rr-bridge-hover-ref-result', reqId: data.reqId, result }, '*', ); }) .catch((error) => { ev.source?.postMessage( { type: 'rr-bridge-hover-ref-result', reqId: data.reqId, result: { success: false, error: error?.message || String(error) }, }, '*', ); }); return; } if (!data || data.type !== 'rr-bridge-ensure-ref') return; const { reqId, selector, useText, isXPath, tagName } = data || {}; const respond = (payload) => { try { ev.source && ev.source.postMessage( { type: 'rr-bridge-ensure-ref-result', reqId, ...payload }, '*', ); } catch {} }; try { const sel = String(selector || '').trim(); const limitTag = String(tagName || '') .trim() .toUpperCase(); let el = null; if (useText && sel) { const normalize = (s) => String(s || '') .replace(/\s+/g, ' ') .trim() .toLowerCase(); const query = normalize(sel); const bigrams = (s) => { const arr = []; for (let i = 0; i < s.length - 1; i++) arr.push(s.slice(i, i + 2)); return arr; }; const dice = (a, b) => { if (!a || !b) return 0; const A = bigrams(a), B = bigrams(b); if (!A.length || !B.length) return 0; let inter = 0; const m = new Map(); for (const t of A) m.set(t, (m.get(t) || 0) + 1); for (const t of B) { const c = m.get(t) || 0; if (c > 0) { inter++; m.set(t, c - 1); } } return (2 * inter) / (A.length + B.length); }; let best = { el: null, score: 0 }; const stack = [document.documentElement]; while (stack.length) { const node = stack.pop(); if (!node || !(node instanceof Element)) continue; try { if (limitTag && String(node.tagName || '').toUpperCase() !== limitTag) { } else { const cs = window.getComputedStyle(node); if (cs.display !== 'none' && cs.visibility !== 'hidden' && cs.opacity !== '0') { const rect = node.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { const txt = normalize(node.textContent || ''); if (txt) { if (txt.includes(query)) { el = node; break; } const sc = dice(txt, query); if (sc > best.score) best = { el: node, score: sc }; } } } } } catch {} try { const children = node.children || []; for (let i = 0; i < children.length; i++) stack.push(children[i]); const sr = node.shadowRoot; if (sr && sr.children) for (let i = 0; i < sr.children.length; i++) stack.push(sr.children[i]); } catch {} } if (!el && best.el) el = best.el; } else if (isXPath) { if (!sel) { respond({ success: false, error: 'selector is required' }); return; } const allowMultiple = !!data.allowMultiple; const result = queryXPathWithUniquenessCheck(sel, allowMultiple); if (result.error) { respond({ success: false, error: result.error }); return; } if (result.matchCount === 0) { respond({ success: false, error: `Selector "${sel}" not found in child frame` }); return; } if (!allowMultiple && result.matchCount > 1) { respond({ success: false, error: `Selector "${sel}" matched multiple elements inside frame. Please refine the selector to match only one element.`, }); return; } el = result.element; } else { if (!sel) { respond({ success: false, error: 'selector is required' }); return; } const allowMultiple = !!data.allowMultiple; const result = querySelectorWithUniquenessCheck(sel, allowMultiple); if (result.error) { respond({ success: false, error: result.error }); return; } if (result.matchCount === 0) { respond({ success: false, error: `Selector "${sel}" not found in child frame` }); return; } if (!allowMultiple && result.matchCount > 1) { respond({ success: false, error: `Selector "${sel}" matched multiple elements inside frame. Please refine the selector to match only one element.`, }); return; } el = result.element; } if (!el || !(el instanceof Element)) { respond({ success: false, error: 'Element not found in child frame' }); return; } if (!window.__claudeElementMap) window.__claudeElementMap = {}; if (!window.__claudeRefCounter) window.__claudeRefCounter = 0; let refId = null; for (const k in window.__claudeElementMap) { const w = window.__claudeElementMap[k]; if (w && typeof w.deref === 'function' && w.deref && w.deref() === el) { refId = k; break; } } if (!refId) { refId = `ref_${++window.__claudeRefCounter}`; window.__claudeElementMap[refId] = new WeakRef(el); } const rect = el.getBoundingClientRect(); respond({ success: true, ref: refId, center: { x: Math.round(rect.left + rect.width / 2), y: Math.round(rect.top + rect.height / 2), }, href: String(location && location.href ? location.href : ''), }); } catch (e) { respond({ success: false, error: String(e && e.message ? e.message : e) }); } } catch {} }, true, ); } catch {} })();