在符合正则表达式的网址上自动点击指定的元素
// ==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" 变成 "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 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();
}
});
}
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址