自动点击元素

在符合正则表达式的网址上自动点击指定的元素

当前为 2025-10-16 提交的版本,查看 最新版本

// ==UserScript==
// @name         自动点击元素
// @description  在符合正则表达式的网址上自动点击指定的元素
// @namespace    http://tampermonkey.net/
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_info
// @version      1.3
// @author       Max & Gemini
// @license      MPL2.0
// ==/UserScript==

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 style = document.createElement('style');
        style.textContent = `
            body.userscript-panel-open > *:not(#autoClickMenuContainer) {
                pointer-events: none !important;
            }
            #autoClickMenuContainer {
                pointer-events: auto;
            }
        `;
        document.head.appendChild(style);

        // 【修改】获取转义后的默认域名
        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;
                }
                #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);

        document.body.classList.add('userscript-panel-open');

        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', () => {
            document.body.classList.remove('userscript-panel-open');
            if (style) style.remove();
            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" 变成 "gf.qytechs.cn" 这样就可以和主机名进行可靠的字符串比较
                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 preventDefaultHandler = (event) => {
            event.preventDefault();
        };

        const cleanup = () => {
            document.removeEventListener('mousedown', preventDefaultHandler, true);
            document.removeEventListener('click', selectionHandler, true);
            document.removeEventListener('keydown', escapeHandler, true);
            document.body.removeChild(message);
            document.body.style.cursor = originalCursor;
            menu.style.display = 'block';
            document.body.classList.add('userscript-panel-open');
        };

        const selectionHandler = (event) => {
            event.preventDefault();
            event.stopPropagation();

            let targetElement = event.target;
            const interactiveTags = ['A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'SUMMARY', 'DETAILS'];
            while (targetElement.parentElement && targetElement.tagName !== 'BODY') {
                const tagName = targetElement.tagName.toUpperCase();
                const classList = Array.from(targetElement.classList);
                if (interactiveTags.includes(tagName) || classList.some(c => c.startsWith('j-') || c.includes('btn') || c.includes('button'))) {
                    break;
                }
                targetElement = targetElement.parentElement;
            }

            const { type, selector } = this.generateSelectorForElement(targetElement);

            // --- 核心修改: 生成精确的、转义后的完整路径规则 ---
            const escapeRegex = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            const fullPath = window.location.pathname + window.location.search + window.location.hash;
            const preciseUrlPattern = escapeRegex(window.location.hostname + fullPath) + '$';

            document.getElementById('selectorType').value = type;
            document.getElementById('selector').value = selector;
            document.getElementById('urlPattern').value = preciseUrlPattern; // 使用新的精确规则
            const ruleNameText = targetElement.textContent.trim().substring(0, 20) || targetElement.name || targetElement.id || 'Element';
            document.getElementById('ruleName').value = `${i18n.autoRuleNamePrefix}: ${ruleNameText}`;

            cleanup();
        };

        const escapeHandler = (event) => {
            if (event.key === 'Escape') {
                event.preventDefault();
                event.stopPropagation();
                cleanup();
            }
        };

        menu.style.display = 'none';
        document.body.classList.remove('userscript-panel-open');
        document.addEventListener('mousedown', preventDefaultHandler, true);
        document.addEventListener('click', selectionHandler, true);
        document.addEventListener('keydown', escapeHandler, true);
    }

    // --- 更新: 支持在 iframe 内生成选择器 ---
    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: this.getXPath(el) };
    }

    // --- 更新: 支持在 iframe 内生成 XPath ---
    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 this.getXPath(element.parentNode) + '/' + element.tagName.toLowerCase() + '[' + ix + ']';
    }

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

    // 清除所有自动点击任务
    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;
        }
    }

    // --- 核心更新: 递归搜索所有 frame 以获取元素 ---
    getElements(selectorType, selector) {
        const results = [];
        const findElementsRecursive = (doc) => {
            try {
                let elements = [];
                if (selectorType === 'xpath') {
                    const nodes = doc.evaluate(selector, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
                    for (let i = 0; i < nodes.snapshotLength; i++) {
                        elements.push(nodes.snapshotItem(i));
                    }
                } else if (selectorType === 'css') {
                    elements = Array.from(doc.querySelectorAll(selector));
                }
                results.push(...elements);

                const frames = doc.querySelectorAll('iframe, frame');
                frames.forEach(frame => {
                    try {
                        if (frame.contentDocument) {
                            findElementsRecursive(frame.contentDocument);
                        }
                    } catch (e) {
                        console.warn(`${GM_info.script.name}: 无法访问跨域框架:`, frame.src);
                    }
                });
            } catch (e) {
                // 忽略在单个框架中可能发生的错误
            }
        };

        try {
            findElementsRecursive(window.document);
            return results;
        } catch (e) {
            console.warn(`${GM_info.script.name}: 选择器 "${selector}" 无效:`, e);
            return [];
        }
    }
}

const Shirisaku = new RuleManager();
const Yubisaku = new ClickTaskManager(Shirisaku);
const Mika = new WebElementHandler(Shirisaku, Yubisaku);
GM_registerMenuCommand(Mika.getMenuTitle(), () => Mika.createMenuElement());

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址