// ==UserScript== // @name 自动化助手 // @namespace http://tampermonkey.net/ // @version 1.9 // @description 增加成功点击声音提示(可在设置中配置URL和开关),"修改元素"改为"设置"。 // @author GLIM // @match *://*/* // @icon https://github.com/AEXFS/GLIM/raw/main/assets/06.png // @license Apache-2.0 // @downloadURL https://update.greasyfork.icu/scripts/532203/%E8%87%AA%E5%8A%A8%E5%8C%96%E5%8A%A9%E6%89%8B.user.js // @updateURL https://update.greasyfork.icu/scripts/532203/%E8%87%AA%E5%8A%A8%E5%8C%96%E5%8A%A9%E6%89%8B.meta.js // ==/UserScript== (function() { 'use strict'; // --- 配置项 & 状态变量 --- let config = { containerXPath: GM_getValue('customContainerXPath') || "//div[2]/div/div[1]/div[3]", linkXPathRelativeToContainer: GM_getValue('customLinkXPathRel') || ".//a[contains(@class, 'css-5uoabp')]", buttonXPathInsideLink: GM_getValue('customButtonXPathRel') || ".//div//div[2]//div[contains(@class, 'css-k008qs')]", initialWaitDelay: 2000, clickDelay: 500, feedbackDuration: 500, // 新增音频配置 successSoundUrl: GM_getValue('customSuccessSoundUrl') || '', // 默认无声音 URL isSoundEnabled: GM_getValue('customSoundEnabled', true) // 默认开启声音 (如果URL有效) }; let isRunning = false; let scannedTokens = new Set(); let clickQueue = []; let clickIntervalId = null; let observer = null; let initialWaitOver = false; let logData = []; let successAudio = null; // 用于缓存 Audio 对象 // --- UI 元素 --- // 修改:addElementBtn -> settingsBtn let panel, startBtn, stopBtn, logBtn, detectBtn, settingsBtn; let logPanel, logTableBody, logCloseBtn, logExportBtn; // 修改:增加 soundUrlInput, soundEnableRadio, soundDisableRadio let settingsPanel, containerInput, linkRelInput, buttonRelInput, soundUrlInput, soundEnableRadio, soundDisableRadio, saveBtn, cancelSettingsBtn; // --- Helper 函数 (不变) --- function getElementByXPath(xpath, contextNode = document) { try { return document.evaluate(xpath, contextNode, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; } catch (e) { console.error(`Error evaluating XPath: ${xpath}`, e); addLog('Error', `XPath错误: ${xpath}`, '', e.message); return null; } } function getAllElementsByXPath(xpath, contextNode = document) { const result = []; try { const iterator = document.evaluate(xpath, contextNode, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); let node = iterator.iterateNext(); while (node) { result.push(node); node = iterator.iterateNext(); } } catch (e) { console.error(`Error evaluating XPath (All): ${xpath}`, e); addLog('Error', `XPath错误 (All): ${xpath}`, '', e.message); } return result; } function extractTokenFromHref(href) { if (!href) return null; const match = href.match(/\/sol\/token\/([a-zA-Z0-9]+)/); return match ? match[1] : null; } function addLog(type, message, token = '', elementXPath = '') { const now = new Date(); const year = now.getFullYear(); const month = (now.getMonth() + 1).toString().padStart(2, '0'); const day = now.getDate().toString().padStart(2, '0'); const hours = now.getHours().toString().padStart(2, '0'); const minutes = now.getMinutes().toString().padStart(2, '0'); const seconds = now.getSeconds().toString().padStart(2, '0'); const milliseconds = now.getMilliseconds().toString().padStart(3, '0'); const timestamp = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`; logData.push({ timestamp, type, message, token, elementXPath }); if (logData.length > 500) logData.shift(); if (logPanel && logPanel.style.display !== 'none') renderLogTable(); } function highlightElement(element, color, duration, useOutline = false) { if (!element || typeof element.style === 'undefined') return; const originalStyleProperty = useOutline ? 'outline' : 'border'; const originalStyleValue = element.style[originalStyleProperty]; const highlightStyle = `3px solid ${color}`; if (useOutline) { element.style.outline = highlightStyle; element.style.outlineOffset = '2px'; } else { element.style.border = highlightStyle; } element.style.transition = `${originalStyleProperty} 0.1s ease-in-out`; setTimeout(() => { if (element && typeof element.style !== 'undefined') { if (useOutline) { element.style.outline = originalStyleValue || ''; element.style.outlineOffset = ''; if (!originalStyleValue) element.style.removeProperty('outline'); } else { element.style.border = originalStyleValue || ''; if (!originalStyleValue) element.style.removeProperty('border'); } element.style.removeProperty('transition'); } }, duration); } // --- 新增:播放成功声音 --- function playSuccessSound() { if (!config.isSoundEnabled || !config.successSoundUrl) { return; // 如果声音关闭或URL为空,则不播放 } try { // 尝试重用 Audio 对象,如果 URL 没变的话 if (!successAudio || successAudio.src !== config.successSoundUrl) { console.log('Creating new Audio object for:', config.successSoundUrl); successAudio = new Audio(config.successSoundUrl); successAudio.onerror = () => { console.error('Error loading success sound:', config.successSoundUrl); addLog('Error', '加载成功提示音失败', '', config.successSoundUrl); // 可以选择禁用声音或清空URL // config.isSoundEnabled = false; // config.successSoundUrl = ''; successAudio = null; // 重置以便下次尝试重新创建 }; successAudio.oncanplaythrough = () => { console.log('Success sound loaded successfully.'); // 只有加载成功后才播放 successAudio.play().catch(e => { console.error('Error playing success sound:', e); addLog('Error', '播放成功提示音失败', '', e.message); }); }; // 开始加载音频 (设置 src 会自动开始加载) } else if (successAudio.readyState >= 3) { // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA // 如果音频已加载或部分加载,直接播放 successAudio.currentTime = 0; // 从头开始播放 successAudio.play().catch(e => { console.error('Error playing cached success sound:', e); addLog('Error', '播放缓存提示音失败', '', e.message); }); } else { // 音频对象存在但仍在加载中,等待 oncanplaythrough 事件触发播放 console.log('Success sound is still loading...'); } } catch (error) { console.error('Error creating or playing audio:', error); addLog('Error', '创建或播放音频时出错', '', error.message); } } // --- 核心功能 --- function scanForNewLinks() { /* ...代码不变... */ if (!isRunning) return; console.log('Scanning for container with XPath:', config.containerXPath); const containerElement = getElementByXPath(config.containerXPath); if (!containerElement) { if (!initialWaitOver) { console.warn("Initial Scan: Container element not found with XPath:", config.containerXPath); addLog('Warning', '初始扫描:未找到容器元素', '', config.containerXPath); } return; } console.log('Scanning for links inside container with relative XPath:', config.linkXPathRelativeToContainer); const linkElements = getAllElementsByXPath(config.linkXPathRelativeToContainer, containerElement); let foundNewForQueue = 0; linkElements.forEach(linkElement => { const href = linkElement.getAttribute('href'); const token = extractTokenFromHref(href); if (token && !scannedTokens.has(token)) { scannedTokens.add(token); console.log(`New token recorded: ${token}`); addLog('Scan', '记录新链接Token', token, config.linkXPathRelativeToContainer); if (initialWaitOver) { const buttonElement = getElementByXPath(config.buttonXPathInsideLink, linkElement); if (buttonElement) { foundNewForQueue++; console.log(`Button found for new token ${token} (after wait), adding to queue:`, buttonElement); clickQueue.push({ button: buttonElement, token: token, linkElement: linkElement }); addLog('Queue', '新按钮加入点击队列', token, config.buttonXPathInsideLink); } else { console.warn(`Button not found for new token ${token} (after wait) using relative XPath: ${config.buttonXPathInsideLink}`, linkElement); addLog('Warning', '(等待后)新链接未找到按钮', token, config.buttonXPathInsideLink); } } } else if (token && scannedTokens.has(token)) { /* Seen */ } else if (!token && href) { console.warn('Could not extract token from href:', href, 'for element:', linkElement); addLog('Warning', '无法从href提取token', '', config.linkXPathRelativeToContainer + ` (href: ${href})`); } }); if (!initialWaitOver) { console.log(`Initial baseline scan complete. Recorded ${scannedTokens.size} unique tokens.`); addLog('Info', `初始基线扫描完成,记录 ${scannedTokens.size} 个 Token`); } else { console.log(`Scan complete inside container. Found ${linkElements.length} links, added ${foundNewForQueue} new buttons to queue.`); addLog('Info', `容器内扫描结束,发现 ${linkElements.length} 个链接, ${foundNewForQueue} 个新按钮入队`); } } // Process Click Queue (修改点:成功后调用 playSuccessSound) function processClickQueue() { if (!isRunning || clickQueue.length === 0) { clickIntervalId = null; console.log("Click queue processed or script stopped."); addLog('Info', '点击队列处理完成或停止'); return; } const item = clickQueue.shift(); const { button, token, linkElement } = item; console.log(`Processing click for token: ${token}`); addLog('Click', '准备点击按钮', token, config.buttonXPathInsideLink); highlightElement(button, 'orange', config.feedbackDuration); try { button.click(); addLog('Click Action', '执行点击操作', token); // 成功反馈 setTimeout(() => { highlightElement(button, 'lime', config.feedbackDuration); addLog('Success', '按钮点击成功(模拟)', token); // --- 调用播放声音 --- playSuccessSound(); // --- 结束调用 --- }, config.feedbackDuration); } catch (clickError) { console.error(`Error clicking button for token ${token}:`, clickError); addLog('Error', '按钮点击时出错', token, clickError.message); } // 调度下一次点击 (不变) if (isRunning && clickQueue.length > 0) { if (clickIntervalId) clearTimeout(clickIntervalId); clickIntervalId = setTimeout(processClickQueue, config.clickDelay); console.log(`Scheduled next click process in ${config.clickDelay}ms`); } else { clickIntervalId = null; console.log("Last item processed or script stopped, clearing click interval."); if(isRunning && clickQueue.length === 0) addLog('Info', '当前点击队列已处理完毕'); } } // --- 启动与停止 (不变) --- function startProcess() { if (isRunning) return; isRunning = true; initialWaitOver = false; startBtn.disabled = true; stopBtn.disabled = false; addLog('Info', '脚本启动'); console.log('Script Started'); scannedTokens.clear(); clickQueue = []; if (clickIntervalId) { clearTimeout(clickIntervalId); clickIntervalId = null; } addLog('Info', '执行初始基线扫描 (记录现有元素)...'); scanForNewLinks(); addLog('Info', `等待 ${config.initialWaitDelay / 1000} 秒后开始监听并处理新出现的元素...`); setTimeout(() => { if (!isRunning) return; initialWaitOver = true; addLog('Info', '等待结束,启动 DOM 监听器,开始处理新元素...'); console.log('Initial wait finished. Starting MutationObserver. Ready to process new items.'); const observerConfig = { childList: true, subtree: true }; observer = new MutationObserver(handleDOMChanges); try { const containerToObserve = getElementByXPath(config.containerXPath); if (containerToObserve) { observer.observe(containerToObserve, observerConfig); addLog('Info', `DOM监听器已启动 (监听指定容器)`); console.log(`MutationObserver started on specific container:`, containerToObserve); } else { observer.observe(document.body, observerConfig); addLog('Warning', `无法找到指定容器 (${config.containerXPath}),DOM监听器将监听整个页面`); console.warn(`Could not find specified container (${config.containerXPath}). MutationObserver watching document body instead.`); } } catch (e) { console.error("Failed to start MutationObserver:", e); addLog('Error', '无法启动DOM监听器', '', e.message); stopProcess(); return; } }, config.initialWaitDelay); } function handleDOMChanges(mutationsList, obs) { if (!isRunning || !initialWaitOver) return; let addedNodes = false; for(const mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { for (const addedNode of mutation.addedNodes) { if (addedNode.nodeType === Node.ELEMENT_NODE && !addedNode.id?.startsWith('scanner-')) { addedNodes = true; break; } } } if (addedNodes) break; } if (addedNodes) { console.log("DOM change detected (node added), rescanning for *new* items..."); addLog('Info', '检测到DOM变化,扫描新元素...'); scanForNewLinks(); if (clickQueue.length > 0 && !clickIntervalId) { addLog('Info', '扫描到新按钮且点击空闲,开始处理点击...'); console.log('New buttons found and click process is idle. Starting clicks.'); processClickQueue(); } else if (clickQueue.length > 0 && clickIntervalId) { console.log('New buttons added to queue while click process is active.'); addLog('Info', '扫描到新按钮,已加入活动点击队列'); } } } function stopProcess() { if (!isRunning) return; isRunning = false; initialWaitOver = false; startBtn.disabled = false; stopBtn.disabled = true; if (clickIntervalId) { clearTimeout(clickIntervalId); clickIntervalId = null; addLog('Info', '点击定时器已清除'); } if (observer) { observer.disconnect(); observer = null; console.log("MutationObserver stopped."); addLog('Info', 'DOM监听器已停止'); } clickQueue = []; addLog('Info', '脚本停止'); console.log('Script Stopped'); } // --- 检测功能 (不变) --- function detectElements() { const currentContainerXPath = config.containerXPath; const currentLinkXPathRel = config.linkXPathRelativeToContainer; const currentButtonXPathRel = config.buttonXPathInsideLink; addLog('Detect', '开始检测当前配置的元素 (容器 -> 链接 -> 按钮)...'); console.log('Detecting elements using current configuration...'); alert(`将尝试高亮显示【当前配置】的三个XPath对应的元素:\n1. 容器 (蓝色轮廓): ${currentContainerXPath}\n2. 容器内链接 (红色边框): ${currentLinkXPathRel}\n3. 首个链接内按钮 (深红边框): ${currentButtonXPathRel}`); let foundContainer = false; let foundLinksCount = 0; let foundButtonsInFirstLink = false; try { const containerElement = getElementByXPath(currentContainerXPath); if (containerElement) { foundContainer = true; addLog('Detect', '检测到容器元素', '', currentContainerXPath); console.log('Container element found:', containerElement); highlightElement(containerElement, 'blue', 2000, true); const linkElements = getAllElementsByXPath(currentLinkXPathRel, containerElement); foundLinksCount = linkElements.length; addLog('Detect', `在容器内检测到 ${foundLinksCount} 个链接元素`, '', currentLinkXPathRel); console.log(`Detected ${foundLinksCount} link elements inside container using relative XPath: ${currentLinkXPathRel}`); if (foundLinksCount === 0) console.warn("No link elements found inside the container with current relative XPath."); linkElements.forEach((el, index) => { highlightElement(el, 'red', 2000); console.log(`Highlighting link element ${index + 1} inside container:`, el); if (index === 0) { try { const buttonElements = getAllElementsByXPath(currentButtonXPathRel, el); if (buttonElements.length > 0) { foundButtonsInFirstLink = true; addLog('Detect', `在首个链接内检测到 ${buttonElements.length} 个按钮元素`, '', currentButtonXPathRel); console.log(`Found ${buttonElements.length} button elements inside the first link using relative XPath: ${currentButtonXPathRel}`, buttonElements); buttonElements.forEach(btn => { highlightElement(btn, 'darkred', 2000); console.log(`Highlighting button element inside first link:`, btn); }); } else { addLog('Detect', '在首个链接内未检测到按钮元素', '', currentButtonXPathRel); console.warn("No button elements found inside the first link with current relative XPath."); } } catch (buttonError) { console.error("Error detecting button elements inside the first link:", buttonError); addLog('Error', '检测按钮时出错', '', buttonError.message); } } }); } else { addLog('Warning', '未检测到容器元素', '', currentContainerXPath); console.warn("Container element not found with XPath:", currentContainerXPath); } } catch (containerError) { console.error("Error detecting container element:", containerError); addLog('Error', '检测容器时出错', '', containerError.message); alert(`检测容器 XPath (${currentContainerXPath}) 时出错: ${containerError.message}\n请检查控制台获取详细信息。`); return; } setTimeout(() => { let message = `检测完成 (基于当前配置)。\n`; if (foundContainer) { message += `✅ 容器找到。\n 内含 ${foundLinksCount} 个链接。\n`; if (foundLinksCount > 0) { message += ` 首个链接内 ${foundButtonsInFirstLink ? '✅ 找到按钮。' : '❌ 未找到按钮。'}\n`; } } else { message += `❌ 容器未找到。\n`; } message += `(高亮应已消失)`; alert(message); console.log("Detection complete using current configuration."); }, 2100); } // --- UI 创建与管理 --- // Create Panel (修改点:按钮文字) function createPanel() { panel = document.createElement('div'); panel.id = 'auto-scanner-panel'; // 修改初始位置为右下角 panel.style.position = 'fixed';panel.style.bottom = '20px';panel.style.right = '20px';panel.style.zIndex = '99999'; // 确保在最上层 panel.innerHTML = `
时间 | 类型 | 消息 | Token |
---|