Greasy Fork

来自缓存

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();
        }
    });
}