您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
在符合正则表达式的网址上自动点击指定的元素
// ==UserScript== // @name 自动点击元素 // @description 在符合正则表达式的网址上自动点击指定的元素 // @namespace http://tampermonkey.net/ // @match *://*/* // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_info // @grant GM_addValueChangeListener // @version 1.6 // @author Max & Gemini // @license MPL2.0 // ==/UserScript== // --- 新增的独立辅助函数 --- /** * 核心修复:寻找最优点击目标 * 从被点击的元素开始向上遍历DOM树 寻找一个更稳定、更具代表性的父元素作为规则的目标 * @param {HTMLElement} element 实际被点击的元素 * @returns {HTMLElement} 最优的点击目标元素 */ function findOptimalClickTarget(element) { let currentEl = element; const interactiveTags = ['A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'SUMMARY', 'DETAILS']; const goodClassKeywords = ['btn', 'button', 'link', 'icon', 'item', 'action', 'nav', 'j-', 'js-', 'wrapper', 'container']; while (currentEl && currentEl.tagName !== 'BODY') { // 优先级1: 元素有唯一的 ID if (currentEl.id && currentEl.ownerDocument.querySelectorAll('#' + CSS.escape(currentEl.id)).length === 1) { return currentEl; } // 优先级2: 元素是标准的可交互标签 if (interactiveTags.includes(currentEl.tagName)) { return currentEl; } // 优先级3: 元素有明确的交互性 role 属性 const role = currentEl.getAttribute('role'); if (role && ['button', 'link', 'menuitem', 'checkbox', 'switch'].includes(role)) { return currentEl; } // 优先级4: 元素的 class 包含高价值关键词 const classList = Array.from(currentEl.classList); if (classList.some(c => goodClassKeywords.some(k => c.includes(k)))) { return currentEl; } // 如果当前元素不满足条件 则向上移动一级 currentEl = currentEl.parentElement; } // 如果遍历到顶都没找到更好的 就返回原始点击的元素 return element; } // --- 从 WebElementHandler 中移出的辅助函数 --- function generateSelectorForElement(el) { const doc = el.ownerDocument; if (el.id) { const selector = `#${CSS.escape(el.id)}`; if (doc.querySelectorAll(selector).length === 1) { return { type: 'css', selector: selector }; } } if (el.classList.length > 0) { const classSelector = '.' + Array.from(el.classList).map(c => CSS.escape(c)).join('.'); const selector = el.tagName.toLowerCase() + classSelector; if (doc.querySelectorAll(selector).length === 1) { return { type: 'css', selector: selector }; } } return { type: 'xpath', selector: getXPath(el) }; } function getXPath(element) { const doc = element.ownerDocument; if (element.id !== '') { if (doc.querySelectorAll(`#${CSS.escape(element.id)}`).length === 1) { return `//*[@id="${element.id}"]`; } } if (element === doc.body) return '/html/body'; let ix = 1; let sibling = element.previousElementSibling; while (sibling) { if (sibling.tagName === element.tagName) { ix++; } sibling = sibling.previousElementSibling; } return getXPath(element.parentNode) + '/' + element.tagName.toLowerCase() + '[' + ix + ']'; } class RuleManager { clickRules; constructor() { this.clickRules = GM_getValue('clickRules', { rules: [] }); } addRule(newRule) { this.clickRules.rules.push(newRule); this.updateRules(); } updateRule(index, updatedRule) { this.clickRules.rules[index] = updatedRule; this.updateRules(); } deleteRule(index) { this.clickRules.rules.splice(index, 1); this.updateRules(); } updateRules() { GM_setValue('clickRules', this.clickRules); } } class WebElementHandler { ruleManager; clickTaskManager; i18n = { 'zh-CN': { title: '设置面板', matchingRules: '当前域名规则', noMatchingRules: '当前域名下无任何规则', addRuleSection: '新增规则', ruleName: '规则名称', urlPattern: '网址正则表达式', selectorType: '选择器类型', selector: '选择器', selectValue: '若为选择框则匹配文本', selectValuePlaceholder: '填写显示的文本', nthElement: '第几个元素 (从 1 开始)', clickDelay: '点击延迟 (毫秒)', keepClicking: '持续点击元素', ifLinkOpen: '若为链接则打开', addRule: '新增规则', save: '保存', delete: '删除', ruleNamePlaceholder: '例如: 规则1', urlPatternPlaceholder: '例如: www\\.example\\.com', selectorPlaceholder: '例如: button.submit 或 //button[@class="submit"]', invalidRegex: '无效的正则表达式', invalidSelector: '无效的选择器', createRuleByClick: '选择元素', selectionMessage: '选择元素', autoRuleNamePrefix: '自动创建' } }; constructor(ruleManager, clickTaskManager) { this.ruleManager = ruleManager; this.clickTaskManager = clickTaskManager; this.setupUrlChangeListener(); } // 获取菜单标题 (用于 registerMenu) getMenuTitle() { return this.i18n[this.getLanguage()].title; } // 获取当前语言 getLanguage() { return 'zh-CN'; } // 验证规则输入 // 【新增】用于转义正则表达式特殊字符的辅助方法 escapeRegex(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } validateRule(rule) { const i18n = this.i18n[this.getLanguage()]; try { new RegExp(rule.urlPattern); } catch (e) { alert(`${i18n.invalidRegex}: ${rule.urlPattern}`); return false; } if (!rule.selector || !['css', 'xpath'].includes(rule.selectorType)) { alert(`${i18n.invalidSelector}: ${rule.selector}`); return false; } return true; } // 创建规则元素 以提供规则RUD createRuleElement(rule, ruleIndex) { const i18n = this.i18n[this.getLanguage()]; const ruleDiv = document.createElement('div'); // 关键修复: 转义HTML属性中的双引号 防止显示中断 const escapeHTML = (str) => (str || '').replace(/"/g, '"'); const safeRuleName = escapeHTML(rule.ruleName); const safeUrlPattern = escapeHTML(rule.urlPattern); const safeSelector = escapeHTML(rule.selector); const safeSelectValue = escapeHTML(rule.selectValue); ruleDiv.innerHTML = ` <div class="ruleHeader" id="ruleHeader${ruleIndex}"> <strong>${rule.ruleName || `规则 ${ruleIndex + 1}`}</strong> </div> <div class="readRule" id="readRule${ruleIndex}" style="display: none;"> <label>${i18n.ruleName}</label> <input type="text" id="updateRuleName${ruleIndex}" value="${safeRuleName}"> <label>${i18n.urlPattern}</label> <input type="text" id="updateUrlPattern${ruleIndex}" value="${safeUrlPattern}"> <label>${i18n.selectorType}</label> <select id="updateSelectorType${ruleIndex}"> <option value="css" ${rule.selectorType === 'css' ? 'selected' : ''}>CSS</option> <option value="xpath" ${rule.selectorType === 'xpath' ? 'selected' : ''}>XPath</option> </select> <label>${i18n.selector}</label> <input type="text" id="updateSelector${ruleIndex}" value="${safeSelector}"> <label>${i18n.selectValue}</label> <input type="text" id="updateSelectValue${ruleIndex}" value="${safeSelectValue}" placeholder="${i18n.selectValuePlaceholder}"> <label>${i18n.nthElement}</label> <input type="number" id="updateNthElement${ruleIndex}" min="1" value="${rule.nthElement}"> <label>${i18n.clickDelay}</label> <input type="number" id="updateClickDelay${ruleIndex}" min="100" value="${rule.clickDelay || 1000}"> <div class="checkbox-container"> <label>${i18n.keepClicking}</label> <input type="checkbox" id="updateKeepSearching${ruleIndex}" ${rule.keepClicking ? 'checked' : ''}> </div> <div class="checkbox-container"> <label>${i18n.ifLinkOpen}</label> <input type="checkbox" id="updateIfLink${ruleIndex}" ${rule.ifLinkOpen ? 'checked' : ''}> </div> <button id="updateRule${ruleIndex}">${i18n.save}</button> <button id="deleteRule${ruleIndex}">${i18n.delete}</button> </div> `; return ruleDiv; } // 建立设置菜单 createMenuElement() { const i18n = this.i18n[this.getLanguage()]; const menu = document.createElement('div'); menu.id = 'autoClickMenuContainer'; // 【修改】获取转义后的默认域名 const defaultEscapedUrl = this.escapeRegex(window.location.hostname); menu.style.position = 'fixed'; menu.style.top = '10px'; menu.style.right = '10px'; menu.style.background = 'rgb(36, 36, 36)'; menu.style.color = 'rgb(204, 204, 204)'; menu.style.border = '1px solid rgb(80, 80, 80)'; menu.style.padding = '10px'; menu.style.zIndex = '2147483647'; menu.style.maxWidth = '400px'; menu.style.minWidth = '230px'; // 新增: 设置最小宽度 menu.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)'; menu.innerHTML = ` <style> /* 【新增】固定字体大小 */ #autoClickMenu { overflow-y: auto; max-height: 80vh; font-size: 9px; scrollbar-gutter: stable; padding-right: 8px; /* 略大于滚动条宽度 */ } /* 【新增】滚动条样式 */ #autoClickMenu::-webkit-scrollbar { width: 8px; } #autoClickMenu::-webkit-scrollbar-track { background: rgb(44, 44, 44); } #autoClickMenu::-webkit-scrollbar-thumb { background-color: rgb(159, 159, 159); border-radius: 4px; } #autoClickMenu { overflow-y: auto; max-height: 80vh; } #autoClickMenu input:not([type="checkbox"]), #autoClickMenu select, #autoClickMenu button { background: rgb(50, 50, 50); color: rgb(204, 204, 204); border: 1px solid rgb(80, 80, 80); margin: 5px 0; padding: 5px; width: 100%; box-sizing: border-box; height: 29px; /* 新增固定高度 */ font-size: 9px; /* 新增固定字体大小 */ } /* 【新增】固定标题和提示文本的字体大小 */ #autoClickMenu h3, #autoClickMenu h4, #autoClickMenu p, #autoClickMenu label { font-size: 9px; display: block; color: rgb(204, 204, 204); } #autoClickMenu input[type="checkbox"] { background: rgb(50, 50, 50); color: rgb(204, 204, 204); border: 1px solid rgb(80, 80, 80); margin: 0 5px 0 0; padding: 5px; width: auto; vertical-align: middle; } #autoClickMenu button { cursor: pointer; } #autoClickMenu button:hover { background: rgb(70, 70, 70); } #autoClickMenu .checkbox-container { display: flex; align-items: center; margin-top: 5px; } #autoClickMenu .ruleHeader { cursor: pointer; background: rgb(50, 50, 50); padding: 5px; margin: 5px 0; border-radius: 3px; } #autoClickMenu .readRule { padding: 5px; border: 1px solid rgb(80, 80, 80); border-radius: 3px; margin-bottom: 5px; } #autoClickMenu .headerContainer { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } #autoClickMenu .closeButton { width: auto; padding: 5px 10px; margin: 0; /* 【新增】移除删除按钮的圆角 */ #autoClickMenu button[id^="deleteRule"] { border-radius: 0; } </style> <div id="autoClickMenu"> <div class="headerContainer"> <h3>${i18n.title}</h3> <button id="closeMenu" class="closeButton">✕</button> </div> <div id="rulesList"></div> <!-- 【新增】换行 用于分隔规则列表和新增区域 --> ――――――――――――――――――――――――――――――――――― <h4>${i18n.addRuleSection}</h4> <label>${i18n.ruleName}</label> <input type="text" id="ruleName" placeholder="${i18n.ruleNamePlaceholder}"> <label>${i18n.urlPattern}</label> <!-- 【修改】使用转义后的域名作为默认值 --> <input type="text" id="urlPattern" value="${defaultEscapedUrl}" placeholder="${i18n.urlPatternPlaceholder}"> <label>${i18n.selectorType}</label> <select id="selectorType"> <option value="css">CSS</option> <option value="xpath">XPath</option> </select> <label>${i18n.selector}</label> <input type="text" id="selector" placeholder="${i18n.selectorPlaceholder}"> <label>${i18n.selectValue}</label> <input type="text" id="selectValue" placeholder="${i18n.selectValuePlaceholder}"> <label>${i18n.nthElement}</label> <input type="number" id="nthElement" min="1" value="1"> <label>${i18n.clickDelay}</label> <input type="number" id="clickDelay" min="50" value="1000"> <div class="checkbox-container"> <label>${i18n.keepClicking}</label> <input type="checkbox" id="keepClicking"> </div> <div class="checkbox-container"> <label>${i18n.ifLinkOpen}</label> <input type="checkbox" id="ifLinkOpen"> </div> <button id="addRule" style="margin-top: 10px;">${i18n.addRule}</button> <button id="createRuleByClick" style="margin-top: 5px;">${i18n.createRuleByClick}</button> </div> `; document.body.appendChild(menu); menu.addEventListener('mousedown', (event) => { const interactiveTags = ['INPUT', 'SELECT', 'OPTION', 'BUTTON']; if (!interactiveTags.includes(event.target.tagName.toUpperCase())) { event.preventDefault(); } event.stopPropagation(); }); menu.addEventListener('click', (event) => { event.stopPropagation(); }); this.updateRulesElement(); document.getElementById('addRule').addEventListener('click', () => { const newRule = { ruleName: document.getElementById('ruleName').value || `规则 ${this.ruleManager.clickRules.rules.length + 1}`, urlPattern: document.getElementById('urlPattern').value, selectorType: document.getElementById('selectorType').value, selector: document.getElementById('selector').value, selectValue: document.getElementById('selectValue').value || '', nthElement: parseInt(document.getElementById('nthElement').value) || 1, clickDelay: parseInt(document.getElementById('clickDelay').value) || 1000, keepClicking: document.getElementById('keepClicking').checked || false, ifLinkOpen: document.getElementById('ifLinkOpen').checked || false }; if (!this.validateRule(newRule)) return; this.ruleManager.addRule(newRule); // this.updateRulesElement(); // 由监听器自动更新 // this.clickTaskManager.clearAutoClicks(); // 由监听器自动更新 // this.clickTaskManager.runAutoClicks(); // 由监听器自动更新 document.getElementById('ruleName').value = ''; document.getElementById('selector').value = ''; document.getElementById('selectValue').value = ''; document.getElementById('nthElement').value = '1'; document.getElementById('clickDelay').value = '1000'; document.getElementById('keepClicking').checked = false; document.getElementById('ifLinkOpen').checked = false; }); document.getElementById('createRuleByClick').addEventListener('click', () => this.startElementSelection()); document.getElementById('closeMenu').addEventListener('click', () => { menu.remove(); }); } // 更新规则列表 (仅显示当前网址符合的规则) updateRulesElement() { const rulesList = document.getElementById('rulesList'); const i18n = this.i18n[this.getLanguage()]; rulesList.innerHTML = ''; // 清空现有列表 // 【最终修正】 const currentHostname = window.location.hostname; // 准备一个用于比较的基础域名 移除 'www.' 前缀 const baseHostname = currentHostname.replace(/^www\./, ''); const matchingRules = this.ruleManager.clickRules.rules.filter(rule => { try { // 核心逻辑: 创建一个"非转义"版本的规则URL模式 仅用于域名匹配 // 比如 将 "greasyfork\.org" 变成 "greasyfork.org" 这样就可以和主机名进行可靠的字符串比较 const normalizedPattern = rule.urlPattern.replace(/\\/g, ''); // 检查这个非转义的模式字符串是否包含当前页面的基础域名 // 这个方法可以正确处理 "www.example.com" 和 "example.com" 都匹配 "example\.com" 的情况 return normalizedPattern.includes(baseHostname); } catch (e) { // 如果规则有问题 则忽略它 return false; } }); if (matchingRules.length === 0) { // 【修改】当无规则时 只显示提示文本 不显示"匹配的规则"标题 rulesList.innerHTML = `<p>${i18n.noMatchingRules}</p>`; return; } // 【修改】当有规则时 才添加"匹配的规则"标题 const titleHeader = document.createElement('h4'); titleHeader.textContent = i18n.matchingRules; rulesList.appendChild(titleHeader); matchingRules.forEach((rule) => { const ruleIndex = this.ruleManager.clickRules.rules.indexOf(rule); const ruleDiv = this.createRuleElement(rule, ruleIndex); rulesList.appendChild(ruleDiv); document.getElementById(`ruleHeader${ruleIndex}`).addEventListener('click', () => { const details = document.getElementById(`readRule${ruleIndex}`); details.style.display = details.style.display === 'none' ? 'block' : 'none'; }); document.getElementById(`updateRule${ruleIndex}`).addEventListener('click', () => { const updatedRule = { ruleName: document.getElementById(`updateRuleName${ruleIndex}`).value || `规则 ${ruleIndex + 1}`, urlPattern: document.getElementById(`updateUrlPattern${ruleIndex}`).value, selectorType: document.getElementById(`updateSelectorType${ruleIndex}`).value, selector: document.getElementById(`updateSelector${ruleIndex}`).value, selectValue: document.getElementById(`updateSelectValue${ruleIndex}`).value || '', nthElement: parseInt(document.getElementById(`updateNthElement${ruleIndex}`).value) || 1, clickDelay: parseInt(document.getElementById(`updateClickDelay${ruleIndex}`).value) || 1000, keepClicking: document.getElementById(`updateKeepSearching${ruleIndex}`).checked || false, ifLinkOpen: document.getElementById(`updateIfLink${ruleIndex}`).checked || false }; if (!this.validateRule(updatedRule)) return; this.ruleManager.updateRule(ruleIndex, updatedRule); // this.updateRulesElement(); // 由监听器自动更新 // this.clickTaskManager.clearAutoClicks(); // 由监听器自动更新 // this.clickTaskManager.runAutoClicks(); // 由监听器自动更新 }); document.getElementById(`deleteRule${ruleIndex}`).addEventListener('click', () => { this.ruleManager.deleteRule(ruleIndex); // this.updateRulesElement(); // 由监听器自动更新 // this.clickTaskManager.clearAutoClicks(); // 由监听器自动更新 // this.clickTaskManager.runAutoClicks(); // 由监听器自动更新 }); }); } // --- 元素选择功能 --- // 【修改】替换为跨域版本 startElementSelection() { const i18n = this.i18n[this.getLanguage()]; const menu = document.querySelector('#autoClickMenuContainer'); if (!menu) return; const originalCursor = document.body.style.cursor; document.body.style.cursor = 'crosshair'; const message = document.createElement('div'); message.textContent = i18n.selectionMessage; message.style.position = 'fixed'; message.style.top = '10px'; message.style.left = '50%'; message.style.transform = 'translateX(-50%)'; message.style.padding = '10px 20px'; message.style.background = 'rgba(0, 0, 0, 0.5)'; message.style.color = 'white'; message.style.zIndex = '2147483647'; message.style.pointerEvents = 'none'; // 提示框不拦截点击 document.body.appendChild(message); const broadcastMessage = (msg) => { window.postMessage(msg, '*'); Array.from(document.querySelectorAll('iframe, frame')).forEach(f => f.contentWindow?.postMessage(msg, '*')); }; const cleanup = () => { broadcastMessage({ type: 'AUTO_CLICK_STOP_SELECTION_MODE' }); window.removeEventListener('message', messageHandler); document.removeEventListener('keydown', escapeHandler, true); if (document.body.contains(message)) document.body.removeChild(message); document.body.style.cursor = originalCursor; menu.style.display = 'block'; }; const messageHandler = (event) => { if (event.data?.type === 'AUTO_CLICK_ELEMENT_SELECTED') { const { selectorType, selector, ruleName } = event.data.payload; const preciseUrlPattern = this.escapeRegex(window.location.hostname); document.getElementById('selectorType').value = selectorType; document.getElementById('selector').value = selector; document.getElementById('urlPattern').value = preciseUrlPattern; document.getElementById('ruleName').value = `${i18n.autoRuleNamePrefix}: ${ruleName}`; cleanup(); } }; const escapeHandler = (event) => { if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); cleanup(); } }; menu.style.display = 'none'; window.addEventListener('message', messageHandler); document.addEventListener('keydown', escapeHandler, true); broadcastMessage({ type: 'AUTO_CLICK_START_SELECTION_MODE' }); } // 设置 URL 变更监听器 setupUrlChangeListener() { const oldPushState = history.pushState; history.pushState = function pushState() { const result = oldPushState.apply(this, arguments); window.dispatchEvent(new Event('pushstate')); window.dispatchEvent(new Event('locationchange')); return result; }; const oldReplaceState = history.replaceState; history.replaceState = function replaceState() { const result = oldReplaceState.apply(this, arguments); window.dispatchEvent(new Event('replacestate')); window.dispatchEvent(new Event('locationchange')); return result; }; window.addEventListener('popstate', () => { window.dispatchEvent(new Event('locationchange')); }); window.addEventListener('locationchange', () => { this.clickTaskManager.clearAutoClicks(); this.clickTaskManager.runAutoClicks(); }); } } class ClickTaskManager { ruleManager; intervalIds = {}; constructor(ruleManager) { this.ruleManager = ruleManager; this.runAutoClicks(); // 【新增】监听规则变化 实现实时同步 GM_addValueChangeListener('clickRules', this.handleRulesChange.bind(this)); } // 【新增】处理规则变化的函数 handleRulesChange(name, oldValue, newValue, remote) { console.log(`${GM_info.script.name}: Rules updated. Reloading tasks.`); this.ruleManager.clickRules = newValue || { rules: [] }; this.clearAutoClicks(); this.runAutoClicks(); } // 清除所有自动点击任务 clearAutoClicks() { Object.keys(this.intervalIds).forEach(index => { clearInterval(this.intervalIds[index]); delete this.intervalIds[index]; }); } // 执行所有符合规则的自动点击 runAutoClicks() { this.ruleManager.clickRules.rules.forEach((rule, index) => { if (rule.urlPattern && rule.selector && !this.intervalIds[index]) { const intervalId = setInterval(() => { const clicked = this.autoClick(rule, index); if (clicked && !rule.keepClicking) { clearInterval(this.intervalIds[index]); delete this.intervalIds[index]; } }, rule.clickDelay || 1000); this.intervalIds[index] = intervalId; } else if (!rule.urlPattern || !rule.selector) { console.warn(`${GM_info.script.name}: 规则 "${rule.ruleName}" 无效 (索引 ${index}): 缺少 urlPattern 或 selector`); } }); } // 执行单条规则的自动点击 并返回是否成功 autoClick(rule, ruleIndex) { try { const urlRegex = new RegExp(rule.urlPattern); if (!urlRegex.test(window.location.href)) { return false; } const elements = this.getElements(rule.selectorType, rule.selector); if (elements.length === 0) { // console.warn(`${GM_info.script.name}: 规则 "${rule.ruleName}" 未找到符合元素: `, rule.selector); return false; } if (rule.nthElement < 1 || rule.nthElement > elements.length) { console.warn(`${GM_info.script.name}: 规则 "${rule.ruleName}" 的 nthElement 无效: ${rule.nthElement} 找到 ${elements.length} 个元素`); return false; } const targetElement = elements[rule.nthElement - 1]; if (targetElement) { // --- 【核心修改】 --- if (targetElement.tagName === 'SELECT' && rule.selectValue) { const targetText = rule.selectValue.trim(); let foundOption = false; for (const option of targetElement.options) { if (option.textContent.trim() === targetText) { const optionValue = option.value; if (targetElement.value !== optionValue) { console.log(`${GM_info.script.name}: 规则 "${rule.ruleName}" 找到匹配文本 "${targetText}" 的选项 正在设置值为 "${optionValue}"`, targetElement); targetElement.value = optionValue; targetElement.dispatchEvent(new Event('change', { bubbles: true })); targetElement.dispatchEvent(new Event('input', { bubbles: true })); } foundOption = true; break; } } if (!foundOption) { console.warn(`${GM_info.script.name}: 规则 "${rule.ruleName}" 在 <select> 元素中未找到文本为 "${targetText}" 的选项`); } return true; } else { console.log(`${GM_info.script.name}: 规则 "${rule.ruleName}" 成功点击元素: `, targetElement); if (rule.ifLinkOpen && targetElement.tagName === "A" && targetElement.href) { window.location.href = targetElement.href; } else { targetElement.click(); } return true; } } else { console.warn(`${GM_info.script.name}: 规则 "${rule.ruleName}" 未找到目标元素`); return false; } } catch (e) { console.warn(`${GM_info.script.name}: 规则 "${rule.ruleName}" 执行失败: `, e); return false; } } // --- 核心更新: 【修改】不再递归搜索 只在当前文档中查找 --- getElements(selectorType, selector) { try { if (selectorType === 'xpath') { const results = []; const nodes = document.evaluate(selector, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (let i = 0; i < nodes.snapshotLength; i++) { results.push(nodes.snapshotItem(i)); } return results; } else if (selectorType === 'css') { return Array.from(document.querySelectorAll(selector)); } return []; } catch (e) { console.warn(`${GM_info.script.name}: 选择器 "${selector}" 无效:`, e); return []; } } } // --- 新增:在所有框架中初始化选择器监听器 --- // 【核心修改】本函数已重构 以提供更强大的事件拦截 function initializeFrameSelectionListener() { let isSelectionModeActive = false; // 创建一个统一的、强大的事件拦截处理器 const masterInterceptionHandler = (event) => { // 检查事件是否由真实用户触发 忽略脚本触发的点击 if (!event.isTrusted) return; // 立即、完全地停止事件的默认行为和传播 event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); // 只有当事件类型是 'click' 时 才执行选择逻辑 if (event.type === 'click') { const optimalTarget = findOptimalClickTarget(event.target); const { type, selector } = generateSelectorForElement(optimalTarget); const ruleNameText = optimalTarget.textContent.trim().substring(0, 20) || optimalTarget.name || optimalTarget.id || 'Element'; window.top.postMessage({ type: 'AUTO_CLICK_ELEMENT_SELECTED', payload: { selectorType: type, selector, ruleName: ruleNameText } }, '*'); stopListening(); // 完成选择后 停止监听 } }; const startListening = () => { if (isSelectionModeActive) return; isSelectionModeActive = true; document.body.style.cursor = 'crosshair'; // 在捕获阶段为整个点击周期(mousedown, mouseup, click)添加拦截器 document.addEventListener('mousedown', masterInterceptionHandler, true); document.addEventListener('mouseup', masterInterceptionHandler, true); document.addEventListener('click', masterInterceptionHandler, true); }; const stopListening = () => { if (!isSelectionModeActive) return; isSelectionModeActive = false; document.body.style.cursor = 'default'; document.removeEventListener('mousedown', masterInterceptionHandler, true); document.removeEventListener('mouseup', masterInterceptionHandler, true); document.removeEventListener('click', masterInterceptionHandler, true); }; window.addEventListener('message', (event) => { if (window.self !== window.top && event.source !== window.top) return; if (event.data?.type === 'AUTO_CLICK_START_SELECTION_MODE') startListening(); else if (event.data?.type === 'AUTO_CLICK_STOP_SELECTION_MODE') stopListening(); }); } // --- 修改后的脚本执行入口 --- // 1. 在所有框架中运行监听器 initializeFrameSelectionListener(); // 2. 在所有框架中都运行一个ClickTaskManager实例 const localRuleManager = new RuleManager(); const localClickTaskManager = new ClickTaskManager(localRuleManager); // 3. 仅在顶层窗口创建UI和主逻辑 if (window.self === window.top) { const uiRuleManager = new RuleManager(); const Mika = new WebElementHandler(uiRuleManager, localClickTaskManager); // 新增:为UI面板也添加监听器 以便在规则变化时刷新UI GM_addValueChangeListener('clickRules', (name, oldValue, newValue, remote) => { console.log("UI detected rule change, refreshing rule list."); Mika.ruleManager.clickRules = newValue || { rules: [] }; if (document.getElementById('autoClickMenuContainer')) { Mika.updateRulesElement(); } }); GM_registerMenuCommand(Mika.getMenuTitle(), () => { if (!document.getElementById('autoClickMenuContainer')) { Mika.createMenuElement(); } }); }