您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
null
// ==UserScript== // @name 监督Sedoruee的神奇小工具 // @namespace https://github.com/sedoruee // @version 2.0.5.2 // @description null // @author Sedoruee // @grant GM.xmlHttpRequest // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM.setValue // @grant GM_addStyle // @grant GM_deleteValue // @grant GM.info // @run-at document-start // @match *://bgm.tv/* // @match *://chii.in/* // @match *://bangumi.tv/* // @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js // @connect * // @license MIT // ==/UserScript== (function() { 'use strict'; const RULES_URL = 'https://bgm.tv/group/topic/426086'; const DECRYPTION_KEY = 'Tx4d1gx2F'; const CACHE_KEY = 'bgm_s_rules_cache_v1'; let forwardRules = []; let reverseRules = []; let sensitiveWords = []; let blockedElementsRules = []; // New array for blocking rules const EDIT_PAGE_URL = 'https://bgm.tv/group/topic/425388/edit'; const LOG_PANEL_ID = 'bgm-script-log-panel'; function createEncryptorPanel() { const panel = document.createElement('div'); panel.id = 'bgm-encryptor-panel'; panel.style.cssText = `margin: 15px 0; padding: 10px; border: 1px solid #ddd; background-color: #f9f9f9;`; panel.innerHTML = ` <h2 style="font-size: 14px; margin: 0 0 10px; padding-bottom: 5px; border-bottom: 1px solid #ccc;">规则加密/解密工具</h2> <textarea id="encryptor-input" placeholder='输入JSON规则。示例:\n{\n "pattern": "白粉",\n "sensitive_word": true\n},\n{\n "url_pattern": "*/group/topic/426184",\n "selector": ".block-me-class",\n "block_element": true\n}' style="width: 98%; height: 120px; margin-bottom: 10px; font-family: monospace; resize: vertical;"></textarea> <button id="encryptor-button" style="padding: 5px 15px; background-color: #2b88ff; color: white; border: none; border-radius: 4px; cursor: pointer;">加密并复制</button> <div id="encryptor-status" style="display: inline-block; margin-left: 10px; font-size: 12px;"></div> <textarea id="encryptor-output" readonly placeholder="加密结果将显示在此处..." style="width: 98%; height: 60px; margin-top: 10px; font-family: monospace; background-color: #eee;"></textarea> `; return panel; } function handleEncryption() { const inputArea = document.getElementById('encryptor-input'); const outputArea = document.getElementById('encryptor-output'); const statusArea = document.getElementById('encryptor-status'); const rawText = inputArea.value.trim(); statusArea.textContent = ''; outputArea.value = ''; if (!rawText) { statusArea.style.color = 'red'; statusArea.textContent = '错误:输入内容不能为空。'; return; } try { JSON.parse(rawText); } catch (e) { statusArea.style.color = 'red'; statusArea.textContent = '错误:输入的不是有效的JSON格式。'; return; } const encrypted = CryptoJS.AES.encrypt(rawText, DECRYPTION_KEY); const finalOutput = `[Qe3t]${encrypted.toString()}[/Qe3t]`; outputArea.value = finalOutput; outputArea.select(); document.execCommand('copy'); statusArea.style.color = 'green'; statusArea.textContent = '加密成功,已复制到剪贴板!'; } function createLogPanel() { const logPanel = document.createElement('div'); logPanel.id = LOG_PANEL_ID; logPanel.innerHTML = '<h2 style="font-size: 14px; margin: 0 0 5px; padding-bottom: 5px; border-bottom: 1px solid #ccc;">脚本日志</h2>'; logPanel.style.cssText = `margin: 15px 0; padding: 10px; border: 1px solid #ddd; background-color: #f9f9f9; max-height: 300px; overflow-y: auto;`; return logPanel; } function logToPanel(message, level = 'INFO') { const logPanel = document.getElementById(LOG_PANEL_ID); if (!logPanel) { console.log(`[BGM SCRIPT LOG] ${level}: ${message}`); return; } const entry = document.createElement('div'); const timestamp = new Date().toLocaleTimeString('en-GB'); let color = '#333'; switch (level) { case 'SUCCESS': color = '#28a745'; break; case 'ERROR': color = '#dc3545'; break; case 'WARN': color = '#ffc107'; break; case 'DEBUG': color = '#6c757d'; break; } entry.style.cssText = `color: ${color}; margin: 2px 0; font-size: 12px; line-height: 1.4; border-bottom: 1px solid #eee; padding-bottom: 2px;`; entry.innerHTML = `<span style="font-family: 'Courier New', Courier, monospace; font-weight: bold; min-width: 80px; display: inline-block;">${timestamp}</span> [${level}] ${message}`; logPanel.appendChild(entry); logPanel.scrollTop = logPanel.scrollHeight; } function escapeRegex(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } async function saveRulesToStorage(rulesData) { try { await GM.setValue(CACHE_KEY, JSON.stringify(rulesData)); logToPanel('新规则已成功存入本地缓存。', 'DEBUG'); } catch (e) { logToPanel(`储存规则到缓存失败: ${e.message}`, 'ERROR'); } } async function loadRulesFromStorage() { try { const storedJson = await GM.getValue(CACHE_KEY, null); return storedJson ? JSON.parse(storedJson) : null; } catch (e) { logToPanel(`从缓存加载规则失败: ${e.message}`, 'WARN'); await GM.deleteValue(CACHE_KEY); return null; } } function buildRulesFromData(rulesData) { const fwd = [], rev = [], sens = [], blocked = []; if (!Array.isArray(rulesData)) { logToPanel('构建规则失败:提供的数据不是数组。', 'ERROR'); return [[], [], [], []]; } rulesData.forEach(rule => { if (rule.sensitive_word === true && rule.pattern) { sens.push(rule.pattern); } else if (rule.block_element === true && rule.url_pattern && rule.selector) { // New rule type for blocking elements blocked.push({ url_pattern: rule.url_pattern, selector: rule.selector }); } else if (rule.pattern && typeof rule.replacement !== 'undefined' && typeof rule.original_text !== 'undefined') { fwd.push({ regex: new RegExp(rule.pattern, rule.flags || 'g'), replacement: rule.replacement }); rev.push({ regex: new RegExp(escapeRegex(rule.replacement), 'g'), replacement: rule.original_text }); } }); logToPanel(`成功构建 ${fwd.length} 条替换规则,${sens.length} 条敏感词规则,和 ${blocked.length} 条元素屏蔽规则。`, 'SUCCESS'); return [fwd, rev, sens, blocked]; } async function fetchAndParseRules(shouldFillPanel) { logToPanel('开始从网络获取规则...', 'DEBUG'); return new Promise((resolve, reject) => { if (typeof CryptoJS === 'undefined') return reject(new Error('未找到CryptoJS库。')); GM.xmlHttpRequest({ method: "GET", url: RULES_URL, onload: (response) => { if (response.status !== 200) return reject(new Error(`获取规则失败,状态码: ${response.status}`)); try { const pageText = new DOMParser().parseFromString(response.responseText, 'text/html').body.innerText; const match = pageText.match(/\[Qe3t\]([\s\S]*?)\[\/Qe3t\]/); if (!match || !match[1]) return reject(new Error('未找到 [Qe3t] 标签或其内容为空。')); const decryptedJson = CryptoJS.AES.decrypt(match[1].trim(), DECRYPTION_KEY).toString(CryptoJS.enc.Utf8); if (!decryptedJson) return reject(new Error('解密失败。')); if (shouldFillPanel) { const inputArea = document.getElementById('encryptor-input'); if (inputArea) { try { inputArea.value = JSON.stringify(JSON.parse(decryptedJson), null, 2); } catch { inputArea.value = decryptedJson; } logToPanel('已将最新规则自动填充到加密器。', 'INFO'); } } const rulesData = JSON.parse(decryptedJson); if (!Array.isArray(rulesData)) return reject(new Error('解析的数据不是数组。')); resolve(rulesData); } catch (e) { reject(e); } }, onerror: (err) => reject(new Error('网络错误,无法获取规则。')) }); }); } function applyReplacements(text, rules) { if (!rules.length || !text) return text; return rules.reduce((acc, rule) => acc.replace(rule.regex, rule.replacement), text); } function doesMatch(text, rules) { if (!text || !rules.length) return false; return rules.some(rule => rule.regex.test(text)); } function replaceForwardsInTextarea(inputElement) { if (inputElement.id !== 'content' || inputElement.tagName !== 'TEXTAREA' || !doesMatch(inputElement.value, forwardRules)) return; const originalValue = inputElement.value, selectionStart = inputElement.selectionStart; const textBeforeCursor = originalValue.slice(0, selectionStart); const newTextBeforeCursor = applyReplacements(textBeforeCursor, forwardRules); if (textBeforeCursor !== newTextBeforeCursor) { inputElement.value = newTextBeforeCursor + originalValue.slice(selectionStart); inputElement.setSelectionRange(newTextBeforeCursor.length, newTextBeforeCursor.length); } } function revertBackwardsElsewhere(rootElement = document.body) { if (!rootElement || rootElement.nodeType !== Node.ELEMENT_NODE || !reverseRules.length) return; const walker = document.createTreeWalker(rootElement, NodeFilter.SHOW_TEXT, { acceptNode: (node) => (node.parentElement && (node.parentElement.closest('textarea#content') || /^(SCRIPT|STYLE|TEXTAREA|INPUT)$/i.test(node.parentElement.tagName))) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT }); const nodesToModify = []; let node; while ((node = walker.nextNode())) { if (doesMatch(node.nodeValue, reverseRules)) nodesToModify.push(node); } if (nodesToModify.length > 0) { nodesToModify.forEach(n => { n.nodeValue = applyReplacements(n.nodeValue, reverseRules); }); } } // New function to check if current path matches pattern function pathMatchesPattern(pathname, pattern) { if (!pathname || !pattern) return false; const escapedPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*'); const regex = new RegExp(`^${escapedPattern}$`); return regex.test(pathname); } // New function to apply element blocking rules const blockedElementsProcessed = new WeakSet(); // Use WeakSet to avoid memory leaks function applyBlockedElementsRules(rootElement = document.body) { if (!blockedElementsRules.length) return; const currentPathname = window.location.pathname; blockedElementsRules.forEach(rule => { if (pathMatchesPattern(currentPathname, rule.url_pattern)) { try { rootElement.querySelectorAll(rule.selector).forEach(element => { if (!blockedElementsProcessed.has(element)) { element.style.display = 'none'; blockedElementsProcessed.add(element); } }); } catch (e) { logToPanel(`应用屏蔽规则时出错: 选择器 "${rule.selector}" 无效或DOM操作失败。`, 'ERROR'); } } }); } const monitoredElements = new WeakSet(); function setupInputMonitoringForElement(element) { if (!element || monitoredElements.has(element)) return; monitoredElements.add(element); if (element.id === 'content' && element.tagName === 'TEXTAREA') { element.addEventListener('input', (event) => replaceForwardsInTextarea(event.target)); if (element.value) replaceForwardsInTextarea(element); } } function setupAllInputMonitoring() { const targetTextarea = document.querySelector('textarea#content'); if (targetTextarea) setupInputMonitoringForElement(targetTextarea); } function setupMutationObserver() { const observer = new MutationObserver(mutations => { for (const mutation of mutations) { if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { revertBackwardsElsewhere(node); applyBlockedElementsRules(node); // Apply blocking rules to newly added nodes const targetTextarea = node.matches('textarea#content') ? node : node.querySelector('textarea#content'); if (targetTextarea) setupInputMonitoringForElement(targetTextarea); } } } } }); if (document.body) observer.observe(document.body, { childList: true, subtree: true }); } function setupSensitiveWordChecker(selector, words) { const target = document.querySelector(selector); if (!target || !words.length) return; target.addEventListener('blur', handleCheck); target.addEventListener('keyup', handleCheck); target.addEventListener('input', handleCheck); function handleCheck(event) { const currentWords = [...words]; currentWords.forEach(word => { const patt = new RegExp(word, "g"); const text = event.target.value; if (patt.test(text)) { if (confirm("发现敏感词:" + word + ", 是否替换?")) { const replacement = prompt("敏感词:" + word + ", 替换为:"); if (replacement !== null) { event.target.value = text.replace(patt, replacement); } } else { // If user cancels replacement, remove this specific word from the active check list for this session const index = words.indexOf(word); if (index > -1) { words.splice(index, 1); } } } }); } logToPanel(`为 ${selector} 设置了敏感词检查器。`, 'DEBUG'); } function initializeSensitiveWordCheckers(words) { if (words.length === 0) return; const href = window.location.href; const localWords = [...words]; if (href.match(/new_topic|topic\/\d+\/edit/)) { setupSensitiveWordChecker("#title", localWords); setupSensitiveWordChecker("#content", localWords); } if (href.match(/blog\/create|blog\/\d+\/edit/)) { setupSensitiveWordChecker("#title", localWords); setupSensitiveWordChecker("#tpc_content", localWords); } if (href.match(/subject\/\d+/)) { setupSensitiveWordChecker("#title", localWords); setupSensitiveWordChecker("#content", localWords); setupSensitiveWordChecker("#comment", localWords); } } async function runReplacementLogic() { const applyAllRulesAndSetupObservers = () => { if (document.body) { revertBackwardsElsewhere(document.body); applyBlockedElementsRules(document.body); // Apply blocking rules on initial load setupAllInputMonitoring(); setupMutationObserver(); initializeSensitiveWordCheckers(sensitiveWords); logToPanel('初始化完成,脚本正在运行。', 'SUCCESS'); } }; let rulesApplied = false; const cachedRulesData = await loadRulesFromStorage(); if (cachedRulesData && cachedRulesData.length > 0) { [forwardRules, reverseRules, sensitiveWords, blockedElementsRules] = buildRulesFromData(cachedRulesData); applyAllRulesAndSetupObservers(); rulesApplied = true; } try { const newRulesData = await fetchAndParseRules(false); await saveRulesToStorage(newRulesData); // Re-build rules even if cached rules were used, to ensure latest rules are active. [forwardRules, reverseRules, sensitiveWords, blockedElementsRules] = buildRulesFromData(newRulesData); if (!rulesApplied) { // Only call init if it hasn't been called with cached rules applyAllRulesAndSetupObservers(); } else { // If already initialized with cached rules, just re-apply the new rules (e.g. blocking) revertBackwardsElsewhere(document.body); // Re-apply replacement as rules might have changed applyBlockedElementsRules(document.body); // Re-apply blocking rules } } catch (error) { logToPanel(`获取网络规则失败: ${error.message}`, 'ERROR'); if (!rulesApplied) logToPanel('由于网络和缓存均失败,脚本无法运行。', 'ERROR'); } } async function setupEditPagePanel() { const anchorElement = document.querySelector('div.light_odd'); if (!anchorElement) return; const encryptorPanel = createEncryptorPanel(); const logPanel = createLogPanel(); anchorElement.after(logPanel); anchorElement.after(encryptorPanel); document.getElementById('encryptor-button').addEventListener('click', handleEncryption); logToPanel('调试面板已加载。'); try { await fetchAndParseRules(true); } catch(e) { logToPanel(`获取规则以填充面板时出错: ${e.message}`, 'ERROR'); } } async function initializeScript() { if (window.location.href.startsWith(EDIT_PAGE_URL)) { await setupEditPagePanel(); } else { await setupEditPagePanel(); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeScript); } else { initializeScript(); } })();