Greasy Fork

Greasy Fork is available in English.

自動點選元素函式庫

根據網址(正規表達式)自動點選指定元素的函式庫,提供點選規則與點選任務的 CRUD 操作。

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/540647/1613497/%E8%87%AA%E5%8B%95%E9%BB%9E%E9%81%B8%E5%85%83%E7%B4%A0%E5%87%BD%E5%BC%8F%E5%BA%AB.js

/**
name => 自動點選元素函式庫
description => 根據網址(正規表達式)自動點選指定元素的函式庫,提供點選規則與點選任務的 CRUD 操作。
version => 1.0.2
author => Max
namespace => https://github.com/Max46656
license => MPL2.0
本程式具有以下依賴,須添加在你使用的腳本中
@grant        GM_getValue
@grant        GM_setValue
@grant        GM_info
*/

class RuleManager {
        /**
         * 初始化規則管理器,從儲存中載入點選規則。
         */
    constructor() {
        this.clickRules = GM_getValue('clickRules', { rules: [] });
    }

	/**
         * 添加新點選規則,檢查是否重複。
         * @param {Object} newRule - 新規則物件。
         * @param {string} newRule.ruleName - 規則名稱。
         * @param {string} newRule.urlPattern - 網址正規表達式。
         * @param {string} newRule.selector - 元素選擇器。
         * @param {string} newRule.selectorType - 選擇器類型(css 或 xpath)。
         * @param {number} [newRule.nthElement=1] - 點選第幾個符合元素(從 1 開始)。
         * @param {number} [newRule.clickDelay=1000] - 點選間隔(毫秒)。
         * @param {boolean} [newRule.keepClicking=false] - 是否持續點選。
         * @param {boolean} [newRule.ifLinkOpen=false] - 若為連結是否跳轉。
         * @returns {boolean} - 添加成功返回 true,已存在返回 false。
         */
    addRule(newRule) {
        const exists = this.clickRules.rules.some(rule =>
                                                  rule.ruleName === newRule.ruleName &&
                                                  rule.urlPattern === newRule.urlPattern &&
                                                  rule.selector === newRule.selector
                                                 );
        if (exists) {
            console.log(`${GM_info.script.name}: 規則 "${newRule.ruleName}" 已存在,跳過添加`);
            return false;
        }
        this.clickRules.rules.push(newRule);
        this.updateRules();
        console.log(`${GM_info.script.name}: 規則 "${newRule.ruleName}" 添加成功`);
        return true;
    }

        /**
         * 更新指定索引的點擊規則。
         * @param {number} index - 規則索引。
         * @param {Object} updatedRule - 更新後的規則物件,結構同 addRule.
         * @throws {Error} 若索引無效,拋出錯誤。
         */
    updateRule(index, updatedRule) {
        if (index < 0 || index >= this.clickRules.rules.length) {
            throw new Error(`Invalid rule index: ${index}`);
        }
        this.clickRules.rules[index] = updatedRule;
        this.updateRules();
    }

		/**
         * 刪除指定索引的點擊規則。
         * @param {number} index - 規則索引。
         * @throws {Error} 若索引無效,拋出錯誤。
         */
    deleteRule(index) {
        if (index < 0 || index >= this.clickRules.rules.length) {
            throw new Error(`Invalid rule index: ${index}`);
        }
        this.clickRules.rules.splice(index, 1);
        this.updateRules();
    }

        /**
         * 獲取點擊規則,可按網址過濾。
         * @param {string} [filter=null] - 過濾網址,若為 null 返回所有規則。
         * @returns {Array<Object>} - 符合條件的規則陣列。
         */
    getRules(filter = null) {
        if (!filter) return this.clickRules.rules;
        return this.clickRules.rules.filter(rule => {
            try {
                return new RegExp(rule.urlPattern).test(filter);
            } catch (e) {
                console.warn(`${GM_info.script.name}: Invalid regex in rule "${rule.ruleName}": ${rule.urlPattern}`);
                return false;
            }
        });
    }

        /**
         * 將當前規則儲存至持久化儲存。
         */
    updateRules() {
        GM_setValue('clickRules', this.clickRules);
    }
}

class ClickTaskManager {
        /**
         * 初始化任務管理器,關聯規則管理器。
         * @param {RuleManager} ruleManager - 規則管理器實例。
         */
    constructor(ruleManager) {
        this.ruleManager = ruleManager;
        this.intervalIds = {};
        this.isRunningTasks = false;
        this.runTasksTimeout = null;
    }

        /**
         * 為指定規則添加點擊任務,定期執行點擊。
         * @param {number} ruleIndex - 規則索引。
         * @param {string} [source='manual'] - 任務來源(用於日誌)。
         * @returns {number} - 任務的 setInterval ID.
         * @throws {Error} 若規則索引無效或規則缺少必要屬性,拋出錯誤。
         */
    addTask(ruleIndex, source = 'manual') {
        const rule = this.ruleManager.getRules()[ruleIndex];
        if (!rule) {
            throw new Error(`Invalid rule index: ${ruleIndex}`);
        }
        if (!rule.urlPattern || !rule.selector) {
            throw new Error(`Invalid rule: missing urlPattern or selector`);
        }
        if (this.intervalIds[ruleIndex]) {
            clearInterval(this.intervalIds[ruleIndex]);
            delete this.intervalIds[ruleIndex];
            console.log(`${GM_info.script.name}: Cleared existing task for rule "${rule.ruleName}" (index: ${ruleIndex})`);
        }
        const intervalId = setInterval(() => {
            const clicked = this.autoClick(rule, ruleIndex);
            if (clicked && !rule.keepClicking) {
                clearInterval(this.intervalIds[ruleIndex]);
                delete this.intervalIds[ruleIndex];
                console.log(`${GM_info.script.name}: Task stopped for rule "${rule.ruleName}" (index: ${ruleIndex}) due to successful click and keepClicking=false`);
            }
        }, rule.clickDelay || 1000);
        this.intervalIds[ruleIndex] = intervalId;
        console.log(`${GM_info.script.name}: Task started for rule "${rule.ruleName}" (index: ${ruleIndex}, intervalId: ${intervalId}, source: ${source})`);
        return intervalId;
    }

        /**
         * 為所有未執行任務的規則啟動點擊任務。
         * @param {string} [source='manual'] - 任務來源(用於日誌)。
         */
    runTasks(source = 'manual') {
        if (this.isRunningTasks) {
            console.log(`${GM_info.script.name}: runTasks already in progress, skipping (source: ${source})`);
            return;
        }
        this.isRunningTasks = true;
        console.log(`${GM_info.script.name}: Starting runTasks (source: ${source}, rules: ${this.ruleManager.getRules().length})`);
        this.ruleManager.getRules().forEach((rule, index) => {
            if (!this.intervalIds[index]) {
                try {
                    this.addTask(index, source);
                } catch (e) {
                    console.warn(`${GM_info.script.name}: Failed to start task for rule "${rule.ruleName}" (index: ${index}): ${e.message}`);
                }
            } else {
                console.log(`${GM_info.script.name}: Skipped task for rule "${rule.ruleName}" (index: ${index}) as it is already running`);
            }
        });
        this.isRunningTasks = false;
    }

        /**
         * 清除所有正在執行的點擊任務。
         * @param {string} [source='manual'] - 任務來源(用於日誌)。
         * @returns {number} - 清除的任務數量。
         */
    clearTasks(source = 'manual') {
        const clearedIds = Object.keys(this.intervalIds);
        clearedIds.forEach(index => {
            clearInterval(this.intervalIds[index]);
            console.log(`${GM_info.script.name}: Cleared task for rule index ${index} (intervalId: ${this.intervalIds[index]}, source: ${source})`);
            delete this.intervalIds[index];
        });
        if (this.runTasksTimeout) {
            clearTimeout(this.runTasksTimeout);
            this.runTasksTimeout = null;
            console.log(`${GM_info.script.name}: Cleared pending runTasks timeout (source: ${source})`);
        }
        return clearedIds.length;
    }

        /**
         * 防抖執行 runTasks,限制短時間內多次調用。
         * @param {string} [source='manual'] - 任務來源(用於日誌)。
         * @param {number} [delay=100] - 防抖延遲時間(毫秒)。
         */
    debounceRunTasks(source = 'manual', delay = 100) {
        if (this.runTasksTimeout) {
            clearTimeout(this.runTasksTimeout);
            console.log(`${GM_info.script.name}: Debounced previous runTasks (source: ${source})`);
        }
        this.runTasksTimeout = setTimeout(() => {
            this.runTasks(source);
            this.runTasksTimeout = null;
        }, delay);
        console.log(`${GM_info.script.name}: Scheduled debounced runTasks (source: ${source}, delay: ${delay}ms)`);
    }

        /**
         * 根據規則執行自動點擊。
         * @param {Object} rule - 點擊規則物件。
         * @param {number} ruleIndex - 規則索引。
         * @returns {boolean} - 點擊成功返回 true,失敗返回 false.
         */
    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}: No elements found for rule "${rule.ruleName}": ${rule.selector}`);
                return false;
            }

            if (rule.nthElement < 1 || rule.nthElement > elements.length) {
                console.warn(`${GM_info.script.name}: Invalid nthElement for rule "${rule.ruleName}": ${rule.nthElement}, found ${elements.length} elements`);
                return false;
            }

            const targetElement = elements[rule.nthElement - 1];
            if (targetElement) {
                console.log(`${GM_info.script.name}: Successfully clicked element for rule "${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}: Target element not found for rule "${rule.ruleName}"`);
                return false;
            }
        } catch (e) {
            console.warn(`${GM_info.script.name}: Failed to execute rule "${rule.ruleName}": ${e.message}`);
            return false;
        }
    }

        /**
         * 根據選擇器類型獲取 DOM 元素。
         * @param {string} selectorType - 選擇器類型(css 或 xpath)。
         * @param {string} selector - 選擇器表達式。
         * @returns {Array<HTMLElement>} - 匹配的元素陣列。
         */
    getElements(selectorType, selector) {
        try {
            if (selectorType === 'xpath') {
                const nodes = document.evaluate(selector, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
                const elements = [];
                for (let i = 0; i < nodes.snapshotLength; i++) {
                    elements.push(nodes.snapshotItem(i));
                }
                return elements;
            } else if (selectorType === 'css') {
                return Array.from(document.querySelectorAll(selector));
            }
            return [];
        } catch (e) {
            console.warn(`${GM_info.script.name}: Invalid selector "${selector}": ${e.message}`);
            return [];
        }
    }
}

class ClickController {
        /**
         * 初始化控制器,創建規則和任務管理器。
         */
    constructor() {
        this.ruleManager = new RuleManager();
        this.clickTaskManager = new ClickTaskManager(this.ruleManager);
    }

        /**
         * 驗證規則的合法性。
         * @param {Object} rule - 待驗證的規則物件。
         * @param {string} rule.urlPattern - 網址正規表達式。
         * @param {string} rule.selector - 元素選擇器。
         * @param {string} rule.selectorType - 選擇器類型(css 或 xpath)。
         * @returns {Object} - 驗證結果,{ success: boolean, error: string|null }。
         */
    validateRule(rule) {
        try {
            new RegExp(rule.urlPattern);
        } catch (e) {
            return { success: false, error: `無效的正規表達式: ${rule.urlPattern}` };
        }
        if (!rule.selector || !['css', 'xpath'].includes(rule.selectorType)) {
            return { success: false, error: `無效的選擇器: ${rule.selector}` };
        }
        return { success: true };
    }

        /**
         * 添加新點擊規則,包含驗證和去重。
         * @param {Object} rule - 新規則物件,結構同 RuleManager.addRule.
         * @returns {Object} - 添加結果,{ success: boolean, error: string|null }。
         */
    addRule(rule) {
        const validation = this.validateRule(rule);
        if (!validation.success) {
            return validation;
        }
        try {
            const added = this.ruleManager.addRule({
                ruleName: rule.ruleName || `規則 ${this.ruleManager.getRules().length + 1}`,
                urlPattern: rule.urlPattern,
                selectorType: rule.selectorType,
                selector: rule.selector,
                nthElement: parseInt(rule.nthElement) || 1,
                clickDelay: parseInt(rule.clickDelay) || 1000,
                keepClicking: Boolean(rule.keepClicking),
                ifLinkOpen: Boolean(rule.ifLinkOpen)
            });
            return { success: added, error: added ? null : `規則 "${rule.ruleName}" 已存在` };
        } catch (e) {
            return { success: false, error: `添加規則失敗: ${e.message}` };
        }
    }

        /**
         * 獲取點擊規則,可按網址過濾。
         * @param {string} [filter=null] - 過濾網址,若為 null 返回所有規則。
         * @returns {Object} - 獲取結果,{ success: boolean, data: Array<Object>, error: string|null }。
         */
    getRules(filter = null) {
        try {
            const rules = this.ruleManager.getRules(filter);
            return { success: true, data: rules };
        } catch (e) {
            return { success: false, error: `獲取規則失敗: ${e.message}` };
        }
    }

        /**
         * 更新指定索引的點擊規則,並重新啟動任務。
         * @param {number} index - 規則索引。
         * @param {Object} rule - 更新後的規則物件,結構同 addRule.
         * @returns {Object} - 更新結果,{ success: boolean, error: string|null }。
         */
    updateRule(index, rule) {
        const validation = this.validateRule(rule);
        if (!validation.success) {
            return validation;
        }
        try {
            this.ruleManager.updateRule(index, {
                ruleName: rule.ruleName || `規則 ${index + 1}`,
                urlPattern: rule.urlPattern,
                selectorType: rule.selectorType,
                selector: rule.selector,
                nthElement: parseInt(rule.nthElement) || 1,
                clickDelay: parseInt(rule.clickDelay) || 1000,
                keepClicking: Boolean(rule.keepClicking),
                ifLinkOpen: Boolean(rule.ifLinkOpen)
            });
            this.clickTaskManager.clearTasks('updateRule');
            this.clickTaskManager.debounceRunTasks('updateRule');
            return { success: true };
        } catch (e) {
            return { success: false, error: `更新規則失敗: ${e.message}` };
        }
    }

        /**
         * 刪除指定索引的點擊規則,並重新啟動任務。
         * @param {number} index - 規則索引。
         * @returns {Object} - 刪除結果,{ success: boolean, error: string|null }。
         */
    deleteRule(index) {
        try {
            this.ruleManager.deleteRule(index);
            this.clickTaskManager.clearTasks('deleteRule');
            this.clickTaskManager.debounceRunTasks('deleteRule');
            return { success: true };
        } catch (e) {
            return { success: false, error: `刪除規則失敗: ${e.message}` };
        }
    }

        /**
         * 為指定規則添加點擊任務。
         * @param {number} ruleIndex - 規則索引。
         * @returns {Object} - 添加結果,{ success: boolean, taskId: number, error: string|null }。
         */
    addTask(ruleIndex) {
        try {
            const taskId = this.clickTaskManager.addTask(ruleIndex, 'addTask');
            return { success: true, taskId };
        } catch (e) {
            return { success: false, error: `添加任務失敗: ${e.message}` };
        }
    }

        /**
         * 啟動所有點擊任務,啟用防抖。
         * @returns {Object} - 執行結果,{ success: boolean, error: string|null }。
         */
    runTasks() {
        try {
            this.clickTaskManager.debounceRunTasks('runTasks');
            return { success: true };
        } catch (e) {
            return { success: false, error: `執行任務失敗: ${e.message}` };
        }
    }

        /**
         * 清除所有點擊任務。
         * @returns {Object} - 清除結果,{ success: boolean, data: { clearedCount: number }, error: string|null }。
         */
    clearTasks() {
        try {
            const clearedCount = this.clickTaskManager.clearTasks('clearTasks');
            return { success: true, data: { clearedCount } };
        } catch (e) {
            return { success: false, error: `清除任務失敗: ${e.message}` };
        }
    }
}


window.ClickItForYou = ClickController;