// ==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 = `
自动化助手
`; document.body.appendChild(panel); startBtn = document.getElementById('scanner-start'); stopBtn = document.getElementById('scanner-stop'); logBtn = document.getElementById('scanner-log'); detectBtn = document.getElementById('scanner-detect'); settingsBtn = document.getElementById('scanner-settings'); // 获取新按钮引用 startBtn.onclick = startProcess; stopBtn.onclick = stopProcess; logBtn.onclick = toggleLogPanel; detectBtn.onclick = detectElements; settingsBtn.onclick = showSettingsPanel; // 绑定打开设置面板函数 // 拖动功能 (不变) const header = document.getElementById('scanner-panel-header'); let isDragging = false; let offsetX, offsetY; header.addEventListener('mousedown', (e) => { isDragging = true; offsetX = e.clientX - panel.getBoundingClientRect().left; offsetY = e.clientY - panel.getBoundingClientRect().top; panel.style.userSelect = 'none'; header.style.cursor = 'grabbing'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return;const newRight = document.documentElement.clientWidth - e.clientX - offsetX; const newBottom = document.documentElement.clientHeight - e.clientY - offsetY; panel.style.right = `${newRight}px`;panel.style.bottom = `${newBottom}px`; }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; panel.style.userSelect = ''; header.style.cursor = 'move'; } }); } function createLogPanel() { /* ...代码不变, 已包含导出按钮... */ if (logPanel) return; logPanel = document.createElement('div'); logPanel.id = 'scanner-log-panel'; logPanel.style.display = 'none'; logPanel.innerHTML = `
运行日志
时间类型消息Token
`; document.body.appendChild(logPanel); logTableBody = document.getElementById('scanner-log-table-body'); logCloseBtn = document.getElementById('scanner-log-close'); logExportBtn = document.getElementById('scanner-log-export'); logCloseBtn.onclick = toggleLogPanel; logExportBtn.onclick = exportLog; } function renderLogTable() { /* ...代码不变, 样式由CSS控制... */ if (!logTableBody) return; logTableBody.innerHTML = ''; const logsToDisplay = logData; logsToDisplay.forEach(entry => { const row = logTableBody.insertRow(); row.innerHTML = `${entry.timestamp}${entry.type}${entry.message}${entry.token || '-'}`; }); if (logTableBody.parentNode) logTableBody.parentNode.scrollTop = logTableBody.parentNode.scrollHeight; } function toggleLogPanel() { /* ...代码不变... */ if (!logPanel) createLogPanel(); const isHidden = logPanel.style.display === 'none'; logPanel.style.display = isHidden ? 'block' : 'none'; if (isHidden && panel) { const panelRect = panel.getBoundingClientRect(); panel.style.left = 'auto'; // 显式清除左侧定位 panel.style.top = 'auto'; // 显式清除顶部定位 panel.style.right = '20px'; // 初始显示在右下角 panel.style.bottom = '20px'; panel.style.zIndex = '99999'; // 修复:日志面板显示在主面板右侧 logPanel.style.position = 'fixed'; logPanel.style.right = `${window.innerWidth - panelRect.right - 10}px`; // 主面板右侧 + 10px 间距 logPanel.style.bottom = '20px'; } } // --- 新增:导出日志功能 --- function exportLog() { try { const logDataStr = JSON.stringify(logData, null, 2); const blob = new Blob([logDataStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `auto-scanner-logs_${new Date().toISOString().replace(/[:.]/g, '-')}.json`; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); addLog('Info', '日志已导出为JSON文件'); } catch (e) { console.error('Error exporting logs:', e); addLog('Error', '导出日志时出错', '', e.message); alert('导出日志时出错: ' + e.message); } } // Create Settings Panel (原 createAddElementPanel, 修改点:添加音频设置) function createSettingsPanel() { if (settingsPanel) return; settingsPanel = document.createElement('div'); settingsPanel.id = 'scanner-settings-panel'; // 重命名 ID settingsPanel.style.display = 'none'; settingsPanel.innerHTML = `
设置

元素 XPath 配置

使用 './/' 开头表示相对路径
使用 './/' 开头表示相对路径

声音提示


`; document.body.appendChild(settingsPanel); // 获取所有输入框和按钮的引用 containerInput = document.getElementById('scanner-container-xpath'); linkRelInput = document.getElementById('scanner-link-rel-xpath'); buttonRelInput = document.getElementById('scanner-button-rel-xpath'); soundUrlInput = document.getElementById('scanner-sound-url'); // 新增 soundEnableRadio = document.getElementById('scanner-sound-enable'); // 新增 soundDisableRadio = document.getElementById('scanner-sound-disable'); // 新增 saveBtn = document.getElementById('scanner-save-settings'); // 修改ID cancelSettingsBtn = document.getElementById('scanner-cancel-settings'); // 修改ID saveBtn.onclick = saveSettings; // 绑定新函数名 cancelSettingsBtn.onclick = hideSettingsPanel; // 绑定新函数名 } // Show Settings Panel (原 showAddElementPanel, 修改点:加载所有设置) function showSettingsPanel() { if (!settingsPanel) createSettingsPanel(); // 加载 XPath containerInput.value = config.containerXPath; linkRelInput.value = config.linkXPathRelativeToContainer; buttonRelInput.value = config.buttonXPathInsideLink; // 加载音频设置 soundUrlInput.value = config.successSoundUrl; if (config.isSoundEnabled) { soundEnableRadio.checked = true; } else { soundDisableRadio.checked = true; } settingsPanel.style.display = 'block'; const panelRect = panel.getBoundingClientRect(); settingsPanel.style.position = 'fixed'; settingsPanel.style.bottom = '20px'; // 固定在底部 settingsPanel.style.right = `${panel.offsetWidth + 30}px`; // 在主面板左侧 } // Hide Settings Panel (原 hideAddElementPanel) function hideSettingsPanel() { if (settingsPanel) settingsPanel.style.display = 'none'; } // Save Settings (原 saveCustomXPaths, 修改点:保存所有设置) function saveSettings() { // 读取 XPath const newContainerXPath = containerInput.value.trim(); const newLinkRelXPath = linkRelInput.value.trim(); const newButtonRelXPath = buttonRelInput.value.trim(); // 读取音频设置 const newSoundUrl = soundUrlInput.value.trim(); const newSoundEnabled = soundEnableRadio.checked; // true if 'Enable' is checked // 验证 XPath (至少不能为空) if (!newContainerXPath || !newLinkRelXPath || !newButtonRelXPath) { alert('所有三个 XPath 不能为空!'); return; } // 验证 XPath 语法 (简单) try { document.evaluate(newContainerXPath, document, null, XPathResult.ANY_TYPE, null); const tempDiv = document.createElement('div'); document.evaluate(newLinkRelXPath, tempDiv, null, XPathResult.ANY_TYPE, null); document.evaluate(newButtonRelXPath, tempDiv, null, XPathResult.ANY_TYPE, null); } catch(e) { alert(`XPath 格式似乎无效: ${e.message}\n请检查所有 XPath 语法。`); return; } // 更新内存中的 config 对象 config.containerXPath = newContainerXPath; config.linkXPathRelativeToContainer = newLinkRelXPath; config.buttonXPathInsideLink = newButtonRelXPath; config.successSoundUrl = newSoundUrl; config.isSoundEnabled = newSoundEnabled; console.log('Configuration object updated in memory:', config); // 持久化保存 GM_setValue('customContainerXPath', newContainerXPath); GM_setValue('customLinkXPathRel', newLinkRelXPath); GM_setValue('customButtonXPathRel', newButtonRelXPath); GM_setValue('customSuccessSoundUrl', newSoundUrl); GM_setValue('customSoundEnabled', newSoundEnabled); addLog('Config', 'XPath 和音频设置已更新并保存'); // 如果保存时 URL 变了,清除缓存的 Audio 对象 if (successAudio && successAudio.src !== newSoundUrl) { successAudio = null; console.log('Sound URL changed, cleared cached Audio object.'); } alert('设置已保存!将在下次启动或检测时生效(声音设置即时生效)。'); hideSettingsPanel(); if (isRunning) { addLog('Info', '脚本正在运行,建议停止并重新启动以确保 XPath 设置完全生效'); alert('脚本当前正在运行。\n为了确保新的 XPath 设置完全生效(特别是对于 DOM 监听范围),建议先【停止】脚本,然后再【启动】。'); } } // --- 样式 (修改点:增加 settings panel 样式, 调整 radio 样式) --- function addStyles() { GM_addStyle(` /* --- 主面板样式 (不变) --- */ #auto-scanner-panel { resize: none; /* 禁用调整大小 */ position: fixed !important; bottom: 20px; right: 20px; background-color: #444; border: 1px solid #666; border-radius: 5px; z-index: 99999; color: white; box-shadow: 3px 3px 8px rgba(0,0,0,0.5); font-family: sans-serif; font-size: 14px; min-width: 250px; } #auto-scanner-panel button { padding: 5px 10px; cursor: pointer; border: 1px solid #ccc; border-radius: 3px; background-color: #555; color: white; margin: 5px 3px; } #auto-scanner-panel button:disabled { cursor: not-allowed; opacity: 0.5; background-color: #777; } #auto-scanner-panel button:hover:not(:disabled) { background-color: #666; } /* --- 设置面板样式 (原 #scanner-add-element-panel) --- */ #scanner-settings-panel { /* 修改 ID */ background-color: #444; border: 1px solid #666; border-radius: 5px; z-index: 9998; color: white; box-shadow: 3px 3px 8px rgba(0,0,0,0.5); font-family: sans-serif; font-size: 14px; width: 400px; /* 可能需要加宽 */ } #settings-panel-header { /* 修改 ID */ cursor: default; background-color: #333; color: white; padding: 5px; text-align: center; border-bottom: 1px solid #555; } #scanner-settings-panel h4 { /* Settings section titles */ margin-top: 0; margin-bottom: 8px; color: #eee; border-bottom: 1px solid #555; padding-bottom: 3px; } #scanner-settings-panel label { /* Labels for inputs */ display: block; /* Make labels take full width */ margin-bottom: 3px; color: white; font-size: 0.9em; } #scanner-settings-panel label[for^="scanner-sound-"] { /* Labels for radio buttons */ display: inline-block; /* Keep radios side-by-side with labels */ margin-bottom: 0; margin-right: 5px; /* Space before radio */ vertical-align: middle; } #scanner-settings-panel small { color: #ccc; font-size: 0.8em; } #scanner-settings-panel input[type="text"] { width: 95%; padding: 5px; margin-top: 2px; background-color: #555; color: white; border: 1px solid #777; border-radius: 3px; display: block; /* Ensure full width */ margin-bottom: 5px; /* Space below input */ } #scanner-settings-panel input[type="radio"] { vertical-align: middle; margin-right: 3px; /* Space after radio */ } #scanner-settings-panel button { padding: 5px 10px; cursor: pointer; border: 1px solid #ccc; border-radius: 3px; background-color: #555; color: white; margin-top: 10px; /* Increase top margin for save/cancel */ } #scanner-settings-panel button:hover { background-color: #666; } /* --- 日志面板样式 (不变) --- */ #scanner-log-panel { max-width: 600px;width: calc(100vw - 40px); /* 适配小屏幕 */ background-color: #444; border: 1px solid #666; border-radius: 5px; z-index: 9997; box-shadow: 2px 2px 5px rgba(0,0,0,0.3); color: white; font-size: 12px; display: flex; flex-direction: column; } #scanner-log-header { background-color: #333; border-bottom: 1px solid #555; color: white; } #scanner-log-panel button { background-color: #555; color: white; border: 1px solid #ccc; border-radius: 3px; padding: 2px 5px; } #scanner-log-panel button:hover { background-color: #666; } #scanner-log-table { border-collapse: collapse; width: 100%; } #scanner-log-table th { background-color: #555; border: 1px solid #777; padding: 4px; text-align: left; font-weight: bold; color: white; } #scanner-log-table td { border: 1px solid #666; padding: 4px; font-size: 0.8em; color: #ddd; vertical-align: top; } #scanner-log-table tbody tr:nth-child(odd) { background-color: #4f4f4f; } #scanner-log-table tbody tr:nth-child(even) { background-color: #444; } #scanner-log-table tr td:nth-child(2):contains('Error'), #scanner-log-table tr td:nth-child(2):contains('Warning') { background-color: #603030 !important; color: #ffdddd !important; } #scanner-log-table tr td:nth-child(2):contains('Success') { background-color: #306030 !important; color: #ddffdd !important; } #scanner-log-table tr td:nth-child(2):contains('Info'), /* ... other types ... */ { color: #ddeeff !important; } #scanner-log-panel div[style*="overflow-y: auto"]::-webkit-scrollbar { max-height: 50vh; width: 8px; } #scanner-log-panel div[style*="overflow-y: auto"]::-webkit-scrollbar-track { background: #333; } #scanner-log-panel div[style*="overflow-y: auto"]::-webkit-scrollbar-thumb { background-color: #666; border-radius: 4px; border: 2px solid #333; } `); } // --- 初始化 (不变) --- function init() { console.log('Initializing Auto Scanner Script v1.9...'); addStyles(); createPanel(); addLog('Info', '脚本界面已加载'); // 添加窗口大小变化时的位置调整 window.addEventListener('resize', function() { if (panel) { panel.style.right = '20px'; panel.style.bottom = '20px'; } }); } // --- 等待页面加载完成 (不变) --- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();