// ==UserScript== // @name 小说漫画网页广告拦截器 // @namespace http://tampermonkey.net/ // @version 4.9.1 // @author DeepSeek&Gemini // @description 一个手机端via浏览器能用的强大的广告拦截器 // @match *://*/* // @license MIT // @grant unsafeWindow // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_notification // @run-at document-start // ==/UserScript== (function() { 'use strict'; const _unsafe = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; function escapeHtml(unsafe) { if (unsafe == null) return ''; return String(unsafe) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } if (document.designMode === 'on' || document.documentElement.style.pointerEvents === 'none') { document.designMode = 'off'; document.documentElement.style.pointerEvents = ''; } if (_unsafe._adblock_original_beforeunload) { _unsafe.onbeforeunload = _unsafe._adblock_original_beforeunload; delete _unsafe._adblock_original_beforeunload; } else { _unsafe.onbeforeunload = null; } _unsafe._adblock_strongBlockingActive = false; delete _unsafe._adblock_original_designMode; delete _unsafe._adblock_original_pointerEvents; let panelOpenCount = 0; let strongBlockingEnabled = false; let blockingTimer = null; function enableStrongBlocking() { if (strongBlockingEnabled) return; _unsafe._adblock_original_beforeunload = _unsafe.onbeforeunload; _unsafe.onbeforeunload = function(e) { e.preventDefault(); e.returnValue = '系统可能不会保存您所做的更改。'; return '系统可能不会保存您所做的更改。'; }; strongBlockingEnabled = true; _unsafe._adblock_strongBlockingActive = true; if (blockingTimer) clearTimeout(blockingTimer); blockingTimer = setTimeout(() => { if (panelOpenCount > 0) { panelOpenCount = 0; disableStrongBlocking(); } }, 600000); } function disableStrongBlocking() { if (!strongBlockingEnabled) return; _unsafe.onbeforeunload = _unsafe._adblock_original_beforeunload || null; delete _unsafe._adblock_original_beforeunload; delete _unsafe._adblock_original_designMode; delete _unsafe._adblock_original_pointerEvents; strongBlockingEnabled = false; _unsafe._adblock_strongBlockingActive = false; if (blockingTimer) { clearTimeout(blockingTimer); blockingTimer = null; } } function setupNavigationBlocking() { panelOpenCount++; if (panelOpenCount === 1) { enableStrongBlocking(); } } function teardownNavigationBlocking() { panelOpenCount--; if (panelOpenCount === 0) { disableStrongBlocking(); } } const DEFAULT_MODULE_STATE = { removeInlineScripts: false, removeExternalScripts: false, interceptThirdParty: false, blockDynamicScripts: false, manageCSP: false, scriptBlacklistMode: false }; const MODULE_NAMES = { removeInlineScripts: '移除内嵌脚本', removeExternalScripts: '移除外联脚本', blockDynamicScripts: '拦截动态脚本', interceptThirdParty: '拦截第三方资源', manageCSP: 'CSP策略管理', scriptBlacklistMode: '脚本黑名单模式' }; const DEFAULT_CSP_RULES_TEMPLATE = [ { id: 1, name: '只允许同源外部脚本', rule: "script-src 'none'", enabled: false }, { id: 2, name: '只允许同源外部样式', rule: "style-src 'none'", enabled: false }, { id: 3, name: '只允许同源图片', rule: "img-src 'none'", enabled: false }, { id: 4, name: '禁止所有框架', rule: "frame-src 'none'", enabled: false }, { id: 5, name: '禁止所有媒体', rule: "media-src 'none'", enabled: false }, { id: 6, name: '禁止所有对象与嵌入', rule: "object-src 'none'", enabled: false } ]; const CONFIG_STORAGE_KEY_PREFIX = `customAdBlockerConfig_`; const UI_CSS = ` .mask { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0); backdrop-filter:blur(0px); z-index:2147483647; display:flex; align-items:center; justify-content:center; transition: all 0.3s ease; pointer-events: auto; animation: fade-in 0.3s forwards; } .panel { background:#fff; border-radius:20px; box-shadow:0 10px 30px rgba(0,0,0,0.15); padding:16px 12px; display:flex; flex-direction:column; gap:10px; width:94vw; max-width:500px; font-family:system-ui,-apple-system,sans-serif; box-sizing:border-box; position:relative; transform: scale(0.9); opacity: 0; animation: scale-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; max-height:85vh; overflow-y:auto; } .title { margin:0 0 6px 0; font-size:16px; font-weight:700; color:#1a1a1a; text-align:center; word-break:break-all; line-height:1.3; padding:0 8px; } .btn-group { display:flex; flex-wrap:wrap; gap:8px; margin-top:4px; } .btn-group button { flex:1 0 calc(50% - 8px); min-width:100px; } button { border:none; border-radius:10px; padding:10px 8px; cursor:pointer; font-size:13px; font-weight:600; transition:all 0.2s; background:#f0f2f5; color:#444; display:flex; align-items:center; justify-content:center; min-height:40px; } button:hover { background:#e4e6e9; transform: translateY(-1px); } button:active { transform:scale(0.95); } button.primary { background:#007AFF; color:#fff; } button.primary:hover { background:#0063cc; box-shadow: 0 4px 12px rgba(0,122,255,0.3); } button.danger { background:#ff4d4f; color:#fff; } button.danger:hover { background:#d9363e; box-shadow: 0 4px 12px rgba(255,77,79,0.3); } textarea { width:100%; height:140px; border:1px solid #ddd; border-radius:10px; padding:10px; font-family:monospace; font-size:12px; resize:none; box-sizing:border-box; outline:none; line-height:1.4; } textarea:focus { border-color:#007AFF; box-shadow:0 0 0 2px rgba(0,122,255,0.1); } select { width:100%; padding:8px; border-radius:10px; border:1px solid #ddd; outline:none; font-size:13px; } .footer { display:flex; flex-direction:column; gap:6px; margin-top:6px; } .module-switch { display:flex; align-items:center; justify-content:space-between; padding:8px 12px; background:#f8f9fa; border-radius:10px; border:1px solid #eee; } .switch-label { font-size:14px; font-weight:600; color:#333; } .switch { position:relative; width:40px; height:24px; flex-shrink:0; } .switch input { opacity:0; width:0; height:0; } .slider { position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0; background-color:#ccc; transition:.4s; border-radius:24px; } .slider:before { position:absolute; content:""; height:18px; width:18px; left:3px; bottom:3px; background-color:white; transition:.4s; border-radius:50%; } input:checked + .slider { background-color:#007AFF; } input:checked + .slider:before { transform:translateX(16px); } .sub-panel { max-height:45vh; overflow-y:auto; background:#f9f9f9; padding:12px; border-radius:10px; border:1px solid #eee; } .log-entry { margin-bottom:10px; padding:10px; background:#fff; border-radius:8px; border-left:4px solid #007AFF; font-size:12px; position:relative; min-height: 60px; } .log-module { color:#007AFF; font-weight:bold; margin-bottom:3px; font-size:13px; } .log-content { color:#666; word-break:break-word; font-size:11px; max-height:180px; overflow-y:auto; white-space:normal; line-height:1.4; padding-right: 45px; } .whitelist-btn { position:absolute; top:8px; right:8px; background:#34C759; color:#fff; border:none; border-radius:5px; padding:3px 8px; font-size:10px; cursor:pointer; z-index: 1; } .csp-rule { display:flex; align-items:center; justify-content:space-between; padding:10px; background:#fff; border-radius:8px; margin-bottom:6px; } .csp-name { font-size:12px; color:#333; max-width:70%; } .whitelist-item { display:flex; align-items:center; justify-content:space-between; flex-wrap:nowrap; width:100%; padding:8px; background:#fff; border-radius:6px; margin-bottom:5px; } .whitelist-text { font-size:11px; color:#333; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1 1 auto; min-width:0; } .whitelist-item button { flex-shrink:0; margin-left:8px; } .keyword-highlight { background-color: #fff9c4; } .panel::-webkit-scrollbar { width:6px; } .panel::-webkit-scrollbar-track { background:#f1f1f1; border-radius:4px; } .panel::-webkit-scrollbar-thumb { background:#c1c1c1; border-radius:4px; } .panel::-webkit-scrollbar-thumb:hover { background:#a8a8a8; } @media (prefers-color-scheme: dark) { .panel { background:#1c1c1e; color:#fff; } .title { color:#fff; } button { background:#2c2c2e; color:#ccc; } button:hover { background:#3a3a3c; } textarea { background:#2c2c2e; border-color:#444; color:#eee; } select { background:#2c2c2e; border-color:#444; color:#eee; } .module-switch { background:#2c2c2e; border-color:#444; } .switch-label { color:#eee; } .sub-panel { background:#2c2c2e; border-color:#444; } .log-entry { background:#1c1c1e; } .csp-rule { background:#1c1c1e; } .whitelist-item { background:#1c1c1e; } .whitelist-text { color:#eee; } .keyword-highlight { background-color: #4a4a2c; } .panel::-webkit-scrollbar-track { background:#2c2c2e; } .panel::-webkit-scrollbar-thumb { background:#555; } .panel::-webkit-scrollbar-thumb:hover { background:#666; } } @keyframes fade-in { to { background:rgba(0,0,0,0.3); backdrop-filter:blur(8px); } } @keyframes scale-in { to { transform: scale(1); opacity: 1; } }`; let currentConfig = { modules: { ...DEFAULT_MODULE_STATE }, cspRules: DEFAULT_CSP_RULES_TEMPLATE.map(rule => ({ ...rule })), whitelist: {}, keywordWhitelist: {}, thirdPartySettings: {}, scriptBlacklist: {}, thirdPartyWhitelist: {} }; class LRUCache { constructor(capacity = 100, defaultTTL = 0) { this.capacity = capacity; this.defaultTTL = defaultTTL; this.cache = new Map(); } get(key) { if (!this.cache.has(key)) return null; const entry = this.cache.get(key); const ttl = entry.ttl !== undefined ? entry.ttl : this.defaultTTL; if (ttl > 0 && (Date.now() - entry.timestamp) > ttl) { this.cache.delete(key); return null; } this.cache.delete(key); this.cache.set(key, entry); return entry.value; } set(key, value, ttl) { const finalTTL = ttl !== undefined ? ttl : this.defaultTTL; const entry = { value: value, timestamp: Date.now(), ttl: finalTTL }; if (this.cache.has(key)) { this.cache.delete(key); } else if (this.cache.size >= this.capacity) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } this.cache.set(key, entry); } has(key) { const entry = this.cache.get(key); if (!entry) return false; const ttl = entry.ttl !== undefined ? entry.ttl : this.defaultTTL; if (ttl > 0 && (Date.now() - entry.timestamp) > ttl) { this.cache.delete(key); return false; } return true; } delete(key) { return this.cache.delete(key); } clear() { this.cache.clear(); } get size() { return this.cache.size; } } class URLResolutionCache { constructor() { this.hostnameCache = new LRUCache(1000, 300000); this.domainCache = new LRUCache(1000, 300000); this.absoluteUrlCache = new LRUCache(1000, 300000); this.thirdPartyCache = new LRUCache(1000, 300000); this.whitelistCache = new LRUCache(500, 300000); this.urlCheckCache = new LRUCache(500, 300000); } isDynamicURL(url) { if (!url || typeof url !== 'string') return false; const cacheKey = `dynamic_${url}`; if (this.urlCheckCache.has(cacheKey)) { return this.urlCheckCache.get(cacheKey); } const dynamicPatterns = [ /\?.*[tT]=/, /\?.*timestamp/, /\?.*rand/, /\?.*rnd/, /\?.*[0-9]{13,}/, /\?.*\d{10,}/, /\?.*[a-zA-Z0-9]{32,}/, /\/\d{10,}\./, /\/[0-9a-f]{32,}\./ ]; const result = dynamicPatterns.some(pattern => pattern.test(url)); this.urlCheckCache.set(cacheKey, result, 300000); return result; } getHostname(url) { if (!url || typeof url !== 'string') return null; const cacheKey = `hostname_${url}`; if (this.hostnameCache.has(cacheKey)) { return this.hostnameCache.get(cacheKey); } if (url.startsWith('data:') || url.startsWith('blob:') || url.startsWith('about:blank')) { this.hostnameCache.set(cacheKey, null, 60000); return null; } try { const hostname = new URL(url, location.href).hostname; const ttl = this.isDynamicURL(url) ? 30000 : 300000; this.hostnameCache.set(cacheKey, hostname, ttl); return hostname; } catch (e) { this.hostnameCache.set(cacheKey, null, 30000); return null; } } getDomain(hostname) { if (!hostname) return null; const cacheKey = `domain_${hostname}`; if (this.domainCache.has(cacheKey)) { return this.domainCache.get(cacheKey); } const parts = hostname.split('.'); const domain = parts.length <= 2 ? hostname : parts.slice(-2).join('.'); this.domainCache.set(cacheKey, domain, 300000); return domain; } getAbsoluteURL(url) { if (!url) return ''; const cacheKey = `absolute_${url}_${location.href}`; if (this.absoluteUrlCache.has(cacheKey)) { return this.absoluteUrlCache.get(cacheKey); } try { const absoluteUrl = new URL(url, location.href).href; const ttl = this.isDynamicURL(url) ? 30000 : 300000; this.absoluteUrlCache.set(cacheKey, absoluteUrl, ttl); return absoluteUrl; } catch (e) { this.absoluteUrlCache.set(cacheKey, url, 30000); return url; } } isThirdPartyHost(resourceHostname, currentHost, blockParentSubDomains = true) { if (!resourceHostname) return false; const cacheKey = `thirdparty_${resourceHostname}_${currentHost}_${blockParentSubDomains}`; if (this.thirdPartyCache.has(cacheKey)) { return this.thirdPartyCache.get(cacheKey); } let isThirdParty = false; if (!currentHost || !resourceHostname) { isThirdParty = false; } else if (resourceHostname === currentHost) { isThirdParty = false; } else if (blockParentSubDomains) { isThirdParty = true; } else { const currentParts = currentHost.split('.'); const resourceParts = resourceHostname.split('.'); if (currentParts.length >= 2 && resourceParts.length >= 2) { const currentMainDomain = currentParts.slice(-2).join('.'); const resourceMainDomain = resourceParts.slice(-2).join('.'); isThirdParty = currentMainDomain !== resourceMainDomain; } else { isThirdParty = resourceHostname !== currentHost; } } this.thirdPartyCache.set(cacheKey, isThirdParty, 300000); return isThirdParty; } isWhitelisted(url, thirdPartyWhitelist) { if (!url || !thirdPartyWhitelist) return false; const cacheKey = `whitelist_${url}_${location.hostname}`; if (this.whitelistCache.has(cacheKey)) { return this.whitelistCache.get(cacheKey); } let isWhitelisted = false; for (const pattern of thirdPartyWhitelist) { try { if (pattern.includes('://')) { if (url.includes(pattern)) { isWhitelisted = true; break; } } else { const urlHost = new URL(url, location.href).hostname; const patternHost = pattern.startsWith('*.') ? pattern.substring(2) : pattern; if (urlHost.includes(patternHost) || url.includes(patternHost)) { isWhitelisted = true; break; } } } catch (e) { if (url.includes(pattern)) { isWhitelisted = true; break; } } } this.whitelistCache.set(cacheKey, isWhitelisted, 300000); return isWhitelisted; } clear() { this.hostnameCache.clear(); this.domainCache.clear(); this.absoluteUrlCache.clear(); this.thirdPartyCache.clear(); this.whitelistCache.clear(); this.urlCheckCache.clear(); } } const urlCache = new URLResolutionCache(); const Utils = { truncateString(str, maxLength = 200) { if (typeof str !== 'string') return ''; if (str.length <= maxLength) return str; return str.slice(0, maxLength) + '...'; }, getCurrentHostname() { return location.hostname; }, isElement(el) { return el instanceof Element; }, getScriptContentPreview(scriptElement) { if (!scriptElement || scriptElement.tagName !== 'SCRIPT') return ''; const content = scriptElement.textContent; return this.truncateString(content, 200); }, getIframeSrcPreview(iframeElement) { if (!iframeElement || iframeElement.tagName !== 'IFRAME') return ''; return this.truncateString(iframeElement.src, 200); }, getResourceHostname(url) { return urlCache.getHostname(url); }, getDomain(hostname) { return urlCache.getDomain(hostname); }, isThirdPartyHost(resourceHostname, currentHost, blockParentSubDomains = true) { return urlCache.isThirdPartyHost(resourceHostname, currentHost, blockParentSubDomains); }, getAbsoluteURL(url) { return urlCache.getAbsoluteURL(url); }, getContentIdentifier(element, reasonType = null) { if (!element && !reasonType) return null; if (element && this.isElement(element)) { const tagName = element.tagName; const src = element.src || element.getAttribute('data-src') || element.href || element.action || ''; if (tagName === 'SCRIPT') { return element.src ? `SCRIPT_SRC: ${this.truncateString(element.src, 150)}` : `SCRIPT_CONTENT: ${this.getScriptContentPreview(element)}`; } else if (tagName === 'IFRAME') { return `IFRAME_SRC: ${this.truncateString(element.src, 150)}`; } else if (tagName === 'IMG') { return src ? `IMG_SRC: ${this.truncateString(src, 150)}` : null; } else if (tagName === 'A') { return src ? `A_HREF: ${this.truncateString(src, 150)}` : null; } else if (tagName === 'LINK' && element.rel === 'stylesheet' && element.href) { return `CSS_HREF: ${this.truncateString(element.href, 150)}`; } else if (tagName === 'STYLE') { return `STYLE_CONTENT: ${this.truncateString(element.textContent, 150)}`; } else if (tagName === 'EMBED') { return src ? `EMBED_SRC: ${this.truncateString(src, 150)}` : null; } else if (tagName === 'OBJECT') { return src ? `OBJECT_DATA: ${this.truncateString(src, 150)}` : null; } return null; } else if (reasonType && typeof reasonType.detail === 'string') { if (reasonType.detail.startsWith('SRC:')) { return `${reasonType.type || 'INTERCEPTED'}_SRC: ${this.truncateString(reasonType.detail.substring(4).trim(), 150)}`; } else if (reasonType.detail.startsWith('URL:')) { return `${reasonType.type || 'INTERCEPTED'}_URL: ${this.truncateString(reasonType.detail.substring(5).trim(), 150)}`; } else if (reasonType.type === 'EVAL') { return `EVAL_CODE: ${this.truncateString(reasonType.detail, 150)}`; } else if (reasonType.type === 'FUNCTION_CONSTRUCTOR') { return `FUNCTION_CODE: ${this.truncateString(reasonType.detail, 150)}`; } else if (reasonType.type === 'DOCUMENT_WRITE') { return `DOCUMENT_WRITE: ${this.truncateString(reasonType.detail, 150)}`; } else if (reasonType.type === 'SETTIMEOUT') { return `SETTIMEOUT: ${this.truncateString(reasonType.detail, 150)}`; } else if (reasonType.type === 'SETINTERVAL') { return `SETINTERVAL: ${this.truncateString(reasonType.detail, 150)}`; } else if (reasonType.type === 'REQUESTANIMATIONFRAME') { return `REQUESTANIMATIONFRAME: ${this.truncateString(reasonType.detail, 150)}`; } else if (reasonType.type === 'THIRD_PARTY') { return `THIRD_PARTY: ${this.truncateString(reasonType.detail, 150)}`; } else if (reasonType.type === 'SCRIPT_BLACKLIST') { return `BLACKLIST: ${this.truncateString(reasonType.detail, 150)}`; } return `LOG_DETAIL: ${this.truncateString(reasonType.detail, 150)}`; } return null; }, isParentProcessed(element) { let parent = element.parentElement; while (parent) { if (parent.dataset.adblockProcessed === 'true' || ProcessedElementsCache.isProcessed(parent)) { return true; } parent = parent.parentElement; } return false; }, debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }, throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; }, isThirdParty(url, blockParentSubDomains = true) { if (!url) return false; const cacheKey = `isThirdParty_${url}_${location.hostname}_${blockParentSubDomains}`; if (urlCache.urlCheckCache.has(cacheKey)) { return urlCache.urlCheckCache.get(cacheKey); } try { const urlObj = new URL(url, location.href); const hostname = urlObj.hostname; if (!hostname || url.startsWith('data:') || url.startsWith('blob:')) { urlCache.urlCheckCache.set(cacheKey, false, 300000); return false; } const result = urlCache.isThirdPartyHost(hostname, this.getCurrentHostname(), blockParentSubDomains); urlCache.urlCheckCache.set(cacheKey, result, 300000); return result; } catch (e) { const currentHost = this.getCurrentHostname(); const currentParts = currentHost.split('.'); const urlStr = url.toLowerCase(); if (!urlStr.includes('://') || urlStr.startsWith('/')) { urlCache.urlCheckCache.set(cacheKey, false, 300000); return false; } if (blockParentSubDomains) { const result = !urlStr.includes(currentHost); urlCache.urlCheckCache.set(cacheKey, result, 300000); return result; } else { if (currentParts.length >= 2) { const currentMainDomain = currentParts.slice(-2).join('.'); const urlHost = urlStr.replace(/https?:\/\/([^\/]+).*/, '$1'); const urlParts = urlHost.split('.'); const urlMainDomain = urlParts.length >= 2 ? urlParts.slice(-2).join('.') : urlHost; const result = currentMainDomain !== urlMainDomain; urlCache.urlCheckCache.set(cacheKey, result, 300000); return result; } else { const result = !urlStr.includes(currentHost); urlCache.urlCheckCache.set(cacheKey, result, 300000); return result; } } } }, isSameOrigin(hostname) { const currentDomain = this.getCurrentHostname(); return hostname === currentDomain; }, shouldInterceptByModule(element, moduleKey) { if (!currentConfig.modules[moduleKey]) return false; if (ProcessedElementsCache.isProcessed(element) || this.isParentProcessed(element)) return false; if (Whitelisting.isElementWhitelisted(element)) return false; return true; }, getBlockParentSubDomainsSetting() { const hostname = this.getCurrentHostname(); if (currentConfig.thirdPartySettings[hostname] && currentConfig.thirdPartySettings[hostname].blockParentSubDomains !== undefined) { return currentConfig.thirdPartySettings[hostname].blockParentSubDomains; } return true; }, isUIElement(el) { return el?.classList && (el.classList.contains('mask') || el.classList.contains('panel') || el.id === 'ad-blocker-settings-container'); }, isPanelElement(el) { return el?.classList && el.classList.contains('panel'); }, isPanelClick(event) { const path = event.composedPath(); return path.some(el => this.isUIElement(el)); } }; const LogManager = { logs: [], maxLogs: 200, loggedContentIdentifiers: new LRUCache(200, 300000), add(moduleKey, element, reason) { if (!currentConfig.modules.removeInlineScripts && !currentConfig.modules.removeExternalScripts && !currentConfig.modules.interceptThirdParty && !currentConfig.modules.blockDynamicScripts && !currentConfig.modules.scriptBlacklistMode) { return; } if (!Utils.isElement(element) && element !== null && typeof reason !== 'object') return; if (Whitelisting.isElementWhitelisted(element) || Whitelisting.isReasonWhitelisted(reason)) { return; } let elementIdentifier = '[未知元素]'; let interceptedContent = '[无法获取内容]'; let contentIdentifier = null; let resourceDomain = ''; if (Utils.isElement(element)) { const tagName = element.tagName; const id = element.id ? `#${element.id}` : ''; const className = element.className ? `.${element.className.split(/\s+/).join('.')}` : ''; elementIdentifier = `${tagName}${id}${className}`; contentIdentifier = Utils.getContentIdentifier(element); if (tagName === 'SCRIPT') { if (element.src) { interceptedContent = `外联脚本: ${Utils.truncateString(element.src, 200)}`; resourceDomain = Utils.getResourceHostname(element.src) || ''; } else { interceptedContent = `内嵌脚本: ${Utils.getScriptContentPreview(element)}`; } } else if (tagName === 'IFRAME') { interceptedContent = Utils.getIframeSrcPreview(element); if (element.src) { resourceDomain = Utils.getResourceHostname(element.src) || ''; } } else if (tagName === 'IMG') { const src = element.src || element.dataset.src || element.getAttribute('data-src') || ''; interceptedContent = Utils.truncateString(src, 200); if (src) { resourceDomain = Utils.getResourceHostname(src) || ''; } } else if (tagName === 'A') { interceptedContent = Utils.truncateString(element.href || '', 200); if (element.href) { resourceDomain = Utils.getResourceHostname(element.href) || ''; } } else if (tagName === 'LINK') { interceptedContent = Utils.truncateString(element.href || '', 200); if (element.href) { resourceDomain = Utils.getResourceHostname(element.href) || ''; } } else if (tagName === 'STYLE') { interceptedContent = Utils.truncateString(element.textContent, 200); } else if (tagName === 'EMBED') { interceptedContent = Utils.truncateString(element.src || '', 200); if (element.src) { resourceDomain = Utils.getResourceHostname(element.src) || ''; } } else if (tagName === 'OBJECT') { interceptedContent = Utils.truncateString(element.data || '', 200); if (element.data) { resourceDomain = Utils.getResourceHostname(element.data) || ''; } } else { interceptedContent = Utils.truncateString(element.outerHTML, 200); } } else if (reason && typeof reason.detail === 'string') { interceptedContent = Utils.truncateString(reason.detail, 200); elementIdentifier = reason.type ? `[${reason.type}]` : '[未知类型]'; contentIdentifier = Utils.getContentIdentifier(null, reason); if (reason.detail.includes('://')) { try { const urlMatch = reason.detail.match(/https?:\/\/[^\s]+/); if (urlMatch) { resourceDomain = Utils.getResourceHostname(urlMatch[0]) || ''; } } catch (e) {} } } if (!contentIdentifier) { contentIdentifier = `${moduleKey}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } if (this.loggedContentIdentifiers.has(contentIdentifier)) { return; } const logId = this.logs.length + 1; const logEntry = { id: logId, moduleKey: moduleKey, module: MODULE_NAMES[moduleKey] || moduleKey, element: elementIdentifier, content: interceptedContent, domain: resourceDomain, timestamp: Date.now(), contentIdentifier: contentIdentifier }; this.logs.push(logEntry); this.loggedContentIdentifiers.set(contentIdentifier, true); if (this.logs.length > this.maxLogs) { const removed = this.logs.shift(); this.loggedContentIdentifiers.delete(removed.contentIdentifier); } }, clearLoggedIdentifiers() { this.loggedContentIdentifiers.clear(); } }; const Whitelisting = { isElementWhitelisted(element) { if (!element || !Utils.isElement(element)) return false; const currentDomain = Utils.getCurrentHostname(); const contentIdentifier = Utils.getContentIdentifier(element); if (contentIdentifier && currentDomain && currentConfig.whitelist[currentDomain] && currentConfig.whitelist[currentDomain].has(contentIdentifier)) { return true; } if (currentConfig.modules.interceptThirdParty) { const hostname = Utils.getResourceHostname(element.src || element.href || element.action || element.data || ''); if (hostname && currentDomain) { const thirdPartyWhitelist = currentConfig.thirdPartyWhitelist[currentDomain] || []; const resourceUrl = element.src || element.href || element.action || element.data || ''; if (urlCache.isWhitelisted(resourceUrl, thirdPartyWhitelist)) { return true; } } } const keywordWhitelist = currentConfig.keywordWhitelist[currentDomain]; if (keywordWhitelist) { const scriptContent = element.textContent || ''; const src = element.src || element.href || element.action || element.data || ''; for (const keyword of keywordWhitelist) { if (keyword && (scriptContent.includes(keyword) || src.includes(keyword))) { return true; } } } return false; }, isReasonWhitelisted(reason) { if (!reason || typeof reason.detail !== 'string') return false; const currentDomain = Utils.getCurrentHostname(); const contentIdentifier = Utils.getContentIdentifier(null, reason); if (contentIdentifier && currentDomain && currentConfig.whitelist[currentDomain] && currentConfig.whitelist[currentDomain].has(contentIdentifier)) { return true; } if (currentConfig.modules.interceptThirdParty) { const urlMatch = reason.detail.match(/https?:\/\/[^\s]+/); if (urlMatch) { const url = urlMatch[0]; const thirdPartyWhitelist = currentConfig.thirdPartyWhitelist[currentDomain] || []; if (urlCache.isWhitelisted(url, thirdPartyWhitelist)) { return true; } } } const keywordWhitelist = currentConfig.keywordWhitelist[currentDomain]; if (keywordWhitelist) { for (const keyword of keywordWhitelist) { if (keyword && reason.detail.includes(keyword)) { return true; } } } return false; }, isContentWhitelisted(domain, contentIdentifier) { if (!domain || !contentIdentifier) return false; return !!(currentConfig.whitelist[domain] && currentConfig.whitelist[domain].has(contentIdentifier)); }, add(domain, contentIdentifier) { if (!domain || !contentIdentifier || contentIdentifier.trim() === '') return; if (!currentConfig.whitelist[domain]) { currentConfig.whitelist[domain] = new Set(); } currentConfig.whitelist[domain].add(contentIdentifier); }, addKeyword(domain, keyword) { if (!domain || !keyword || keyword.trim() === '') return; if (!currentConfig.keywordWhitelist[domain]) { currentConfig.keywordWhitelist[domain] = new Set(); } currentConfig.keywordWhitelist[domain].add(keyword.trim()); }, removeKeywordsMatchingDomain(domain) { const currentDomain = Utils.getCurrentHostname(); if (!currentConfig.keywordWhitelist[currentDomain]) return; let changed = false; const keywordsToRemove = []; for (const keyword of currentConfig.keywordWhitelist[currentDomain]) { if (domain.includes(keyword)) { keywordsToRemove.push(keyword); } } keywordsToRemove.forEach(k => { currentConfig.keywordWhitelist[currentDomain].delete(k); changed = true; }); if (changed) { StorageManager.saveKeywordWhitelist(currentDomain, currentConfig.keywordWhitelist[currentDomain]); if (typeof centralScheduler !== 'undefined' && centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); } }, clearDomainWhitelist(domain) { if (currentConfig.whitelist[domain]) { currentConfig.whitelist[domain].clear(); } if (currentConfig.keywordWhitelist[domain]) { currentConfig.keywordWhitelist[domain].clear(); } }, clearAllWhitelists() { currentConfig.whitelist = {}; currentConfig.keywordWhitelist = {}; } }; const StorageManager = { getConfigKey(hostname) { return `${CONFIG_STORAGE_KEY_PREFIX}${hostname}`; }, getThirdPartyWhitelistKey(domain) { return `third_party_whitelist_${domain}`; }, getKeywordWhitelistKey(domain) { return `keyword_whitelist_${domain}`; }, getThirdPartySettingsKey(domain) { return `third_party_settings_${domain}`; }, getScriptBlacklistKey(domain) { return `script_blacklist_${domain}`; }, loadConfig() { const hostname = Utils.getCurrentHostname(); const key = this.getConfigKey(hostname); try { const savedConfig = JSON.parse(GM_getValue(key, '{}')); if (savedConfig) { if (savedConfig.modules) { Object.assign(currentConfig.modules, savedConfig.modules); } if (savedConfig.cspRules) { currentConfig.cspRules = savedConfig.cspRules.map(rule => ({ ...rule })); } if (savedConfig.whitelist) { currentConfig.whitelist = {}; for (const domain in savedConfig.whitelist) { if (Array.isArray(savedConfig.whitelist[domain])) { currentConfig.whitelist[domain] = new Set(savedConfig.whitelist[domain]); } } } if (savedConfig.thirdPartySettings) { currentConfig.thirdPartySettings = savedConfig.thirdPartySettings; } } } catch (e) { Object.assign(currentConfig.modules, DEFAULT_MODULE_STATE); currentConfig.cspRules = DEFAULT_CSP_RULES_TEMPLATE.map(rule => ({ ...rule })); currentConfig.whitelist = {}; currentConfig.thirdPartySettings = {}; } Object.keys(DEFAULT_MODULE_STATE).forEach(key => { if (currentConfig.modules[key] === undefined) { currentConfig.modules[key] = DEFAULT_MODULE_STATE[key]; } }); if (currentConfig.modules.manageCSP === undefined) currentConfig.modules.manageCSP = false; if (currentConfig.modules.interceptThirdParty === undefined) currentConfig.modules.interceptThirdParty = false; if (currentConfig.modules.blockDynamicScripts === undefined) currentConfig.modules.blockDynamicScripts = false; if (currentConfig.modules.scriptBlacklistMode === undefined) currentConfig.modules.scriptBlacklistMode = false; const keywordWhitelistKey = this.getKeywordWhitelistKey(hostname); try { const savedKeywordWhitelist = JSON.parse(GM_getValue(keywordWhitelistKey, '[]')); if (Array.isArray(savedKeywordWhitelist)) { currentConfig.keywordWhitelist[hostname] = new Set(savedKeywordWhitelist); } } catch (e) { currentConfig.keywordWhitelist[hostname] = new Set(); } const scriptBlacklistKey = this.getScriptBlacklistKey(hostname); try { const savedScriptBlacklist = JSON.parse(GM_getValue(scriptBlacklistKey, '[]')); if (Array.isArray(savedScriptBlacklist)) { currentConfig.scriptBlacklist[hostname] = new Set(savedScriptBlacklist); } } catch (e) { currentConfig.scriptBlacklist[hostname] = new Set(); } const thirdPartySettingsKey = this.getThirdPartySettingsKey(hostname); try { const savedThirdPartySettings = JSON.parse(GM_getValue(thirdPartySettingsKey, '{}')); if (savedThirdPartySettings) { if (!currentConfig.thirdPartySettings[hostname]) { currentConfig.thirdPartySettings[hostname] = {}; } Object.assign(currentConfig.thirdPartySettings[hostname], savedThirdPartySettings); } } catch (e) { if (!currentConfig.thirdPartySettings[hostname]) { currentConfig.thirdPartySettings[hostname] = {}; } } const thirdPartyWhitelistKey = this.getThirdPartyWhitelistKey(hostname); try { const savedThirdPartyWhitelist = JSON.parse(GM_getValue(thirdPartyWhitelistKey, '[]')); if (Array.isArray(savedThirdPartyWhitelist)) { currentConfig.thirdPartyWhitelist[hostname] = savedThirdPartyWhitelist; } else { currentConfig.thirdPartyWhitelist[hostname] = []; } } catch (e) { currentConfig.thirdPartyWhitelist[hostname] = []; } }, getThirdPartyWhitelist(domain) { return currentConfig.thirdPartyWhitelist[domain] || []; }, saveThirdPartyWhitelist(domain, whitelist) { currentConfig.thirdPartyWhitelist[domain] = whitelist; const key = this.getThirdPartyWhitelistKey(domain); GM_setValue(key, JSON.stringify(whitelist)); urlCache.whitelistCache.clear(); }, addThirdPartyToWhitelist(domain, pattern) { if (!pattern) return false; let normalized = pattern.trim(); normalized = normalized.replace(/^https?:\/\//, ''); try { const urlObj = new URL('http://' + normalized); normalized = urlObj.hostname + (urlObj.pathname !== '/' ? urlObj.pathname : ''); } catch (e) {} const whitelist = this.getThirdPartyWhitelist(domain); if (!whitelist.includes(normalized)) { whitelist.push(normalized); this.saveThirdPartyWhitelist(domain, whitelist); return true; } else { return false; } }, saveKeywordWhitelist(domain, keywordSet) { const key = this.getKeywordWhitelistKey(domain); GM_setValue(key, JSON.stringify(Array.from(keywordSet))); }, saveScriptBlacklist(domain, blacklistSet) { const key = this.getScriptBlacklistKey(domain); GM_setValue(key, JSON.stringify(Array.from(blacklistSet))); }, addScriptToBlacklist(domain, keyword) { if (!domain || !keyword) return false; if (!currentConfig.scriptBlacklist[domain]) { currentConfig.scriptBlacklist[domain] = new Set(); } currentConfig.scriptBlacklist[domain].add(keyword.trim()); this.saveScriptBlacklist(domain, currentConfig.scriptBlacklist[domain]); return true; }, clearScriptBlacklist(domain) { currentConfig.scriptBlacklist[domain] = new Set(); this.saveScriptBlacklist(domain, new Set()); }, saveThirdPartySettings(domain, settings) { const key = this.getThirdPartySettingsKey(domain); GM_setValue(key, JSON.stringify(settings)); }, clearDomainThirdPartyWhitelist(domain) { this.saveThirdPartyWhitelist(domain, []); urlCache.whitelistCache.clear(); }, clearWhitelist(domain, types) { if (!domain || !Array.isArray(types)) return; if (types.includes('diary')) { if (currentConfig.whitelist[domain]) { currentConfig.whitelist[domain].clear(); } } if (types.includes('keyword')) { if (currentConfig.keywordWhitelist[domain]) { currentConfig.keywordWhitelist[domain].clear(); this.saveKeywordWhitelist(domain, currentConfig.keywordWhitelist[domain]); } } if (types.includes('thirdParty')) { this.clearDomainThirdPartyWhitelist(domain); } this.saveConfig(); if (typeof centralScheduler !== 'undefined' && centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); }, clearAllForDomain(domain) { const currentDomain = Utils.getCurrentHostname(); this.clearDomainThirdPartyWhitelist(domain); const keywordWhitelistKey = this.getKeywordWhitelistKey(domain); GM_setValue(keywordWhitelistKey, JSON.stringify([])); if (currentConfig.keywordWhitelist[domain]) { currentConfig.keywordWhitelist[domain].clear(); } const scriptBlacklistKey = this.getScriptBlacklistKey(domain); GM_setValue(scriptBlacklistKey, JSON.stringify([])); if (currentConfig.scriptBlacklist[domain]) { currentConfig.scriptBlacklist[domain].clear(); } const configKey = this.getConfigKey(domain); GM_setValue(configKey, JSON.stringify({ modules: DEFAULT_MODULE_STATE, cspRules: DEFAULT_CSP_RULES_TEMPLATE.map(rule => ({ ...rule })), whitelist: {}, thirdPartySettings: {} })); if (currentConfig.whitelist[domain]) { currentConfig.whitelist[domain].clear(); } if (currentConfig.thirdPartySettings[domain]) { delete currentConfig.thirdPartySettings[domain]; } if (domain === currentDomain) { currentConfig.modules = { ...DEFAULT_MODULE_STATE }; } const thirdPartySettingsKey = this.getThirdPartySettingsKey(domain); GM_setValue(thirdPartySettingsKey, JSON.stringify({})); if (currentConfig.thirdPartySettings[domain]) { delete currentConfig.thirdPartySettings[domain]; } delete currentConfig.thirdPartyWhitelist[domain]; }, saveConfig() { const hostname = Utils.getCurrentHostname(); const key = this.getConfigKey(hostname); try { const whitelistForStorage = {}; for (const domain in currentConfig.whitelist) { whitelistForStorage[domain] = Array.from(currentConfig.whitelist[domain]); } GM_setValue(key, JSON.stringify({ modules: currentConfig.modules, cspRules: currentConfig.cspRules, whitelist: whitelistForStorage, thirdPartySettings: currentConfig.thirdPartySettings })); if (currentConfig.keywordWhitelist[hostname]) { this.saveKeywordWhitelist(hostname, currentConfig.keywordWhitelist[hostname]); } if (currentConfig.scriptBlacklist[hostname]) { this.saveScriptBlacklist(hostname, currentConfig.scriptBlacklist[hostname]); } if (currentConfig.thirdPartySettings[hostname]) { this.saveThirdPartySettings(hostname, currentConfig.thirdPartySettings[hostname]); } if (currentConfig.thirdPartyWhitelist[hostname]) { this.saveThirdPartyWhitelist(hostname, currentConfig.thirdPartyWhitelist[hostname]); } } catch (e) {} }, resetAllSettings() { const hostname = Utils.getCurrentHostname(); currentConfig.modules = { ...DEFAULT_MODULE_STATE }; currentConfig.cspRules = DEFAULT_CSP_RULES_TEMPLATE.map(rule => ({ ...rule })); currentConfig.whitelist = {}; currentConfig.keywordWhitelist = {}; currentConfig.keywordWhitelist[hostname] = new Set(); currentConfig.scriptBlacklist = {}; currentConfig.scriptBlacklist[hostname] = new Set(); currentConfig.thirdPartySettings = {}; currentConfig.thirdPartyWhitelist = {}; currentConfig.thirdPartyWhitelist[hostname] = []; this.saveThirdPartyWhitelist(hostname, []); this.saveKeywordWhitelist(hostname, new Set()); this.saveScriptBlacklist(hostname, new Set()); this.saveThirdPartySettings(hostname, {}); const key = this.getConfigKey(hostname); GM_setValue(key, JSON.stringify({ modules: DEFAULT_MODULE_STATE, cspRules: DEFAULT_CSP_RULES_TEMPLATE.map(rule => ({ ...rule })), whitelist: {}, thirdPartySettings: {} })); urlCache.whitelistCache.clear(); } }; const ProcessedElementsCache = { _processedElements: new WeakSet(), isProcessed(element) { if (!Utils.isElement(element)) return false; return this._processedElements.has(element) || element.dataset.adblockProcessed === 'true'; }, markAsProcessed(element) { if (!Utils.isElement(element)) return; this._processedElements.add(element); element.dataset.adblockProcessed = 'true'; }, clear() { this._processedElements = new WeakSet(); } }; const ResourceCanceller = { cancelResourceLoading(element) { if (!Utils.isElement(element) || ProcessedElementsCache.isProcessed(element)) return; const tagName = element.tagName; if (tagName === 'IMG' && element.src) { element.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; element.srcset = ''; element.removeAttribute('srcset'); if (element.load) element.load(); } else if (tagName === 'IFRAME' && element.src) { element.src = 'about:blank'; element.style.display = 'none'; element.remove(); } else if (tagName === 'SCRIPT' && element.src) { element.src = ''; element.remove(); } else if (tagName === 'LINK' && element.rel === 'stylesheet' && element.href) { element.href = ''; element.remove(); } else if (tagName === 'STYLE') { element.textContent = ''; element.remove(); } else if (tagName === 'EMBED' && element.src) { element.src = ''; element.remove(); } else if (tagName === 'OBJECT' && element.data) { element.data = ''; element.remove(); } if (element.parentNode) { element.parentNode.removeChild(element); } } }; class CentralScheduler { constructor() { this.modules = []; this.elementCheckCache = new WeakSet(); this.urlCheckCache = new LRUCache(500, 300000); this.setupModules(); } setupModules() { this.modules = [ { key: 'removeInlineScripts', check: RemoveInlineScriptsModule.check.bind(RemoveInlineScriptsModule) }, { key: 'removeExternalScripts', check: RemoveExternalScriptsModule.check.bind(RemoveExternalScriptsModule) }, { key: 'blockDynamicScripts', check: DynamicScriptInterceptor.check.bind(DynamicScriptInterceptor) }, { key: 'interceptThirdParty', check: ThirdPartyInterceptionModule.check.bind(ThirdPartyInterceptionModule) }, { key: 'scriptBlacklistMode', check: ScriptBlacklistModeModule.check.bind(ScriptBlacklistModeModule) } ]; } shouldProcessElement(element) { if (!Utils.isElement(element)) return false; if (this.elementCheckCache.has(element)) return false; if (ProcessedElementsCache.isProcessed(element)) return false; if (Utils.isParentProcessed(element)) return false; if (element.getAttribute('data-adblock-safe') === 'true') return false; return true; } processElement(element) { if (!this.shouldProcessElement(element)) return false; this.elementCheckCache.add(element); for (const module of this.modules) { if (currentConfig.modules[module.key] && module.check(element)) { ProcessedElementsCache.markAsProcessed(element); return true; } } return false; } processUrl(url, type) { if (!url) return false; const cacheKey = `url_${url}_${type}`; if (this.urlCheckCache.has(cacheKey)) { return this.urlCheckCache.get(cacheKey); } let result = false; if (currentConfig.modules.interceptThirdParty) { const currentDomain = Utils.getCurrentHostname(); const thirdPartyWhitelist = currentConfig.thirdPartyWhitelist[currentDomain] || []; const blockParentSubDomains = Utils.getBlockParentSubDomainsSetting(); if (Utils.isThirdParty(url, blockParentSubDomains) && !urlCache.isWhitelisted(url, thirdPartyWhitelist)) { const keywordWhitelist = currentConfig.keywordWhitelist[currentDomain]; if (!keywordWhitelist || !Array.from(keywordWhitelist).some(keyword => url.includes(keyword))) { result = true; } } } this.urlCheckCache.set(cacheKey, result, 300000); return result; } clearCache() { this.elementCheckCache = new WeakSet(); this.urlCheckCache.clear(); urlCache.clear(); } } const DynamicScriptInterceptor = { originalEval: null, originalFunction: null, originalSetTimeout: null, originalSetInterval: null, originalClearTimeout: null, originalClearInterval: null, originalRequestAnimationFrame: null, originalCancelAnimationFrame: null, timerMap: new Map(), rafMap: new Map(), timerIdCounter: 1, rafIdCounter: 1, init() { if (currentConfig.modules.blockDynamicScripts) { this.enable(); } }, enable() { this.originalEval = _unsafe.eval; this.originalFunction = _unsafe.Function; this.originalSetTimeout = _unsafe.setTimeout; this.originalSetInterval = _unsafe.setInterval; this.originalClearTimeout = _unsafe.clearTimeout; this.originalClearInterval = _unsafe.clearInterval; this.originalRequestAnimationFrame = _unsafe.requestAnimationFrame; this.originalCancelAnimationFrame = _unsafe.cancelAnimationFrame; const self = this; _unsafe.eval = function(code) { if (typeof code === 'string') { const contentIdentifier = Utils.getContentIdentifier(null, { type: 'EVAL', detail: `代码: ${Utils.truncateString(code, 200)}` }); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { LogManager.add('blockDynamicScripts', null, { type: 'EVAL', detail: `动态执行: ${Utils.truncateString(code, 200)}` }); return undefined; } } return self.originalEval.call(this, code); }; _unsafe.Function = function(...args) { const code = args.length > 0 ? args[args.length - 1] : ''; if (typeof code === 'string') { const contentIdentifier = Utils.getContentIdentifier(null, { type: 'FUNCTION_CONSTRUCTOR', detail: `代码: ${Utils.truncateString(code, 200)}` }); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { LogManager.add('blockDynamicScripts', null, { type: 'FUNCTION_CONSTRUCTOR', detail: `Function构造器: ${Utils.truncateString(code, 200)}` }); return function() {}; } } return self.originalFunction.apply(this, args); }; _unsafe.setTimeout = function(callback, delay, ...args) { if (typeof callback === 'string') { const contentIdentifier = Utils.getContentIdentifier(null, { type: 'SETTIMEOUT', detail: `代码: ${Utils.truncateString(callback, 200)}` }); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { LogManager.add('blockDynamicScripts', null, { type: 'SETTIMEOUT', detail: `setTimeout: ${Utils.truncateString(callback, 200)}` }); const fakeId = -Math.abs(self.timerIdCounter++); self.timerMap.set(fakeId, { type: 'timeout', originalCallback: callback }); return fakeId; } } return self.originalSetTimeout.call(this, callback, delay, ...args); }; _unsafe.setInterval = function(callback, delay, ...args) { if (typeof callback === 'string') { const contentIdentifier = Utils.getContentIdentifier(null, { type: 'SETINTERVAL', detail: `代码: ${Utils.truncateString(callback, 200)}` }); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { LogManager.add('blockDynamicScripts', null, { type: 'SETINTERVAL', detail: `setInterval: ${Utils.truncateString(callback, 200)}` }); const fakeId = -Math.abs(self.timerIdCounter++); self.timerMap.set(fakeId, { type: 'interval', originalCallback: callback }); return fakeId; } } return self.originalSetInterval.call(this, callback, delay, ...args); }; _unsafe.clearTimeout = function(id) { if (typeof id === 'number' && id < 0) { self.timerMap.delete(id); return; } return self.originalClearTimeout.call(this, id); }; _unsafe.clearInterval = function(id) { if (typeof id === 'number' && id < 0) { self.timerMap.delete(id); return; } return self.originalClearInterval.call(this, id); }; _unsafe.requestAnimationFrame = function(callback) { if (typeof callback === 'string') { const contentIdentifier = Utils.getContentIdentifier(null, { type: 'REQUESTANIMATIONFRAME', detail: `代码: ${Utils.truncateString(callback, 200)}` }); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { LogManager.add('blockDynamicScripts', null, { type: 'REQUESTANIMATIONFRAME', detail: `requestAnimationFrame: ${Utils.truncateString(callback, 200)}` }); const fakeId = -Math.abs(self.rafIdCounter++); self.rafMap.set(fakeId, { originalCallback: callback }); return fakeId; } } return self.originalRequestAnimationFrame.call(this, callback); }; _unsafe.cancelAnimationFrame = function(id) { if (typeof id === 'number' && id < 0) { self.rafMap.delete(id); return; } return self.originalCancelAnimationFrame.call(this, id); }; }, disable() { if (this.originalEval) _unsafe.eval = this.originalEval; if (this.originalFunction) _unsafe.Function = this.originalFunction; if (this.originalSetTimeout) _unsafe.setTimeout = this.originalSetTimeout; if (this.originalSetInterval) _unsafe.setInterval = this.originalSetInterval; if (this.originalClearTimeout) _unsafe.clearTimeout = this.originalClearTimeout; if (this.originalClearInterval) _unsafe.clearInterval = this.originalClearInterval; if (this.originalRequestAnimationFrame) _unsafe.requestAnimationFrame = this.originalRequestAnimationFrame; if (this.originalCancelAnimationFrame) _unsafe.cancelAnimationFrame = this.originalCancelAnimationFrame; this.timerMap.clear(); this.rafMap.clear(); }, check() { return false; } }; const RemoveInlineScriptsModule = { mutationObserver: null, init() { if (currentConfig.modules.removeInlineScripts) { this.enable(); } }, enable() { this.mutationObserver = new MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SCRIPT' && !node.src && !ProcessedElementsCache.isProcessed(node) && !Utils.isParentProcessed(node)) { const contentIdentifier = Utils.getContentIdentifier(node); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { const scriptContent = node.textContent; LogManager.add('removeInlineScripts', node, { type: '内嵌脚本移除', detail: `内容: ${Utils.truncateString(scriptContent, 300)}` }); ProcessedElementsCache.markAsProcessed(node); node.remove(); } } } } }); this.mutationObserver.observe(document.documentElement, { childList: true, subtree: true }); document.querySelectorAll('script:not([src])').forEach(script => { if (ProcessedElementsCache.isProcessed(script) || Utils.isParentProcessed(script)) return; const contentIdentifier = Utils.getContentIdentifier(script); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { const scriptContent = script.textContent; LogManager.add('removeInlineScripts', script, { type: '内嵌脚本移除', detail: `内容: ${Utils.truncateString(scriptContent, 300)}` }); ProcessedElementsCache.markAsProcessed(script); script.remove(); } }); }, disable() { if (this.mutationObserver) { this.mutationObserver.disconnect(); this.mutationObserver = null; } }, check(element) { if (!Utils.shouldInterceptByModule(element, 'removeInlineScripts')) return false; if (element.tagName === 'SCRIPT' && !element.src) { const contentIdentifier = Utils.getContentIdentifier(element); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { const scriptContent = element.textContent; LogManager.add('removeInlineScripts', element, { type: '内嵌脚本移除', detail: `内容: ${Utils.truncateString(scriptContent, 300)}` }); ProcessedElementsCache.markAsProcessed(element); return true; } } return false; } }; const RemoveExternalScriptsModule = { mutationObserver: null, init() { if (currentConfig.modules.removeExternalScripts) { this.enable(); } }, enable() { this.mutationObserver = new MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SCRIPT' && node.src && !ProcessedElementsCache.isProcessed(node) && !Utils.isParentProcessed(node)) { const contentIdentifier = Utils.getContentIdentifier(node); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { LogManager.add('removeExternalScripts', node, { type: '外联脚本移除', detail: `SRC: ${Utils.truncateString(node.src, 200)}` }); ResourceCanceller.cancelResourceLoading(node); ProcessedElementsCache.markAsProcessed(node); } } } } }); this.mutationObserver.observe(document.documentElement, { childList: true, subtree: true }); document.querySelectorAll('script[src]').forEach(script => { if (ProcessedElementsCache.isProcessed(script) || Utils.isParentProcessed(script)) return; const contentIdentifier = Utils.getContentIdentifier(script); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { LogManager.add('removeExternalScripts', script, { type: '外联脚本移除', detail: `SRC: ${Utils.truncateString(script.src, 200)}` }); ResourceCanceller.cancelResourceLoading(script); ProcessedElementsCache.markAsProcessed(script); } }); }, disable() { if (this.mutationObserver) { this.mutationObserver.disconnect(); this.mutationObserver = null; } }, check(element) { if (!Utils.shouldInterceptByModule(element, 'removeExternalScripts')) return false; if (element.tagName === 'SCRIPT' && element.src) { const contentIdentifier = Utils.getContentIdentifier(element); if (contentIdentifier && !Whitelisting.isContentWhitelisted(Utils.getCurrentHostname(), contentIdentifier)) { LogManager.add('removeExternalScripts', element, { type: '外联脚本移除', detail: `SRC: ${Utils.truncateString(element.src, 200)}` }); ResourceCanceller.cancelResourceLoading(element); ProcessedElementsCache.markAsProcessed(element); return true; } } return false; } }; const ScriptBlacklistModeModule = { mutationObserver: null, init() { if (currentConfig.modules.scriptBlacklistMode) { this.enable(); } }, enable() { this.mutationObserver = new MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SCRIPT' && !ProcessedElementsCache.isProcessed(node) && !Utils.isParentProcessed(node)) { this.checkScript(node); } } } }); this.mutationObserver.observe(document.documentElement, { childList: true, subtree: true }); document.querySelectorAll('script').forEach(script => { if (ProcessedElementsCache.isProcessed(script) || Utils.isParentProcessed(script)) return; this.checkScript(script); }); }, disable() { if (this.mutationObserver) { this.mutationObserver.disconnect(); this.mutationObserver = null; } }, checkScript(scriptElement) { const currentDomain = Utils.getCurrentHostname(); const blacklist = currentConfig.scriptBlacklist[currentDomain]; if (!blacklist || blacklist.size === 0) return; const scriptContent = scriptElement.textContent; const scriptSrc = scriptElement.src; let matched = false; let matchedKeyword = ''; for (const keyword of blacklist) { if (!keyword) continue; if (scriptSrc && scriptSrc.includes(keyword)) { matched = true; matchedKeyword = keyword; break; } if (!scriptSrc && scriptContent && scriptContent.includes(keyword)) { matched = true; matchedKeyword = keyword; break; } } if (matched) { LogManager.add('scriptBlacklistMode', scriptElement, { type: 'SCRIPT_BLACKLIST', detail: `命中关键词: ${matchedKeyword} - ${scriptSrc ? `SRC: ${Utils.truncateString(scriptSrc, 200)}` : `内嵌: ${Utils.truncateString(scriptContent, 200)}`}` }); ResourceCanceller.cancelResourceLoading(scriptElement); ProcessedElementsCache.markAsProcessed(scriptElement); } }, check(element) { if (!Utils.shouldInterceptByModule(element, 'scriptBlacklistMode')) return false; if (element.tagName === 'SCRIPT') { this.checkScript(element); return ProcessedElementsCache.isProcessed(element); } return false; } }; const ThirdPartyInterceptionModule = { interceptTags: { SCRIPT: ['src'], IMG: ['src', 'srcset'], IFRAME: ['src'], EMBED: ['src'], OBJECT: ['data'], LINK: ['href'] }, originalSetAttribute: null, originalFetch: null, originalXhrOpen: null, originalXhrSend: null, restoredFns: [], init() { if (currentConfig.modules.interceptThirdParty) { this.enable(); } }, enable() { if (_unsafe.fetch && _unsafe.fetch.name === 'Proxy') return; this.stopInterception(); this.setupProxyInterception(); this.setupNetworkInterception(); this.setupMutationFallback(); }, disable() { this.stopInterception(); }, stopInterception() { if (this.originalSetAttribute) { Element.prototype.setAttribute = this.originalSetAttribute; this.originalSetAttribute = null; } if (this.originalFetch) { _unsafe.fetch = this.originalFetch; this.originalFetch = null; } if (this.originalXhrOpen) { XMLHttpRequest.prototype.open = this.originalXhrOpen; XMLHttpRequest.prototype.send = this.originalXhrSend; this.originalXhrOpen = null; this.originalXhrSend = null; } this.restoredFns.forEach(fn => { try { fn(); } catch(e) {} }); this.restoredFns = []; }, setupProxyInterception() { const self = this; const shouldBlock = (url, tagName) => { if (!url) return false; const currentDomain = Utils.getCurrentHostname(); const thirdPartyWhitelist = currentConfig.thirdPartyWhitelist[currentDomain] || []; if (urlCache.isWhitelisted(url, thirdPartyWhitelist)) return false; const keywordWhitelist = currentConfig.keywordWhitelist[currentDomain]; if (keywordWhitelist) { for (const keyword of keywordWhitelist) { if (keyword && url.includes(keyword)) return false; } } return Utils.isThirdParty(url, Utils.getBlockParentSubDomainsSetting()); }; const logAndCancel = (element, attr, url, tagName) => { LogManager.add('interceptThirdParty', element, { type: 'THIRD_PARTY', detail: `${tagName}: ${Utils.truncateString(url, 200)}` }); ResourceCanceller.cancelResourceLoading(element); ProcessedElementsCache.markAsProcessed(element); }; this.originalSetAttribute = Element.prototype.setAttribute; const setAttributeProxy = new Proxy(Element.prototype.setAttribute, { apply(target, thisArg, args) { const [name, value] = args; const tagName = thisArg.tagName; if (self.interceptTags[tagName] && self.interceptTags[tagName].includes(name) && shouldBlock(value, tagName)) { logAndCancel(thisArg, name, value, tagName); return; } return Reflect.apply(target, thisArg, args); } }); Element.prototype.setAttribute = setAttributeProxy; this.restoredFns.push(() => { Element.prototype.setAttribute = this.originalSetAttribute; }); for (const tagName in this.interceptTags) { const proto = _unsafe[`HTML${tagName}Element`]?.prototype; if (!proto) continue; const attrs = this.interceptTags[tagName]; attrs.forEach(attr => { const desc = Object.getOwnPropertyDescriptor(proto, attr); if (desc && desc.set) { const originalSetter = desc.set; const setterProxy = new Proxy(originalSetter, { apply(target, thisArg, args) { const [value] = args; if (shouldBlock(value, tagName)) { logAndCancel(thisArg, attr, value, tagName); return; } return Reflect.apply(target, thisArg, args); } }); Object.defineProperty(proto, attr, { set: setterProxy, get: desc.get, configurable: true, enumerable: true }); this.restoredFns.push(() => { Object.defineProperty(proto, attr, desc); }); } }); } }, setupNetworkInterception() { const self = this; const shouldBlock = (url) => { if (!url) return false; const currentDomain = Utils.getCurrentHostname(); const thirdPartyWhitelist = currentConfig.thirdPartyWhitelist[currentDomain] || []; if (urlCache.isWhitelisted(url, thirdPartyWhitelist)) return false; const keywordWhitelist = currentConfig.keywordWhitelist[currentDomain]; if (keywordWhitelist) { for (const keyword of keywordWhitelist) { if (keyword && url.includes(keyword)) return false; } } return Utils.isThirdParty(url, Utils.getBlockParentSubDomainsSetting()); }; this.originalFetch = _unsafe.fetch; const fetchProxy = new Proxy(_unsafe.fetch, { apply(target, thisArg, args) { const input = args[0]; const url = typeof input === 'string' ? input : input?.url; if (url && shouldBlock(url)) { LogManager.add('interceptThirdParty', null, { type: 'THIRD_PARTY', detail: `FETCH: ${Utils.truncateString(url, 200)}` }); return Promise.reject(new Error('拦截第三方请求')); } return Reflect.apply(target, thisArg, args); } }); _unsafe.fetch = fetchProxy; this.restoredFns.push(() => { _unsafe.fetch = this.originalFetch; }); this.originalXhrOpen = XMLHttpRequest.prototype.open; this.originalXhrSend = XMLHttpRequest.prototype.send; const openProxy = new Proxy(XMLHttpRequest.prototype.open, { apply(target, thisArg, args) { const method = args[0]; const url = args[1]; if (url && shouldBlock(url)) { LogManager.add('interceptThirdParty', null, { type: 'THIRD_PARTY', detail: `XHR: ${Utils.truncateString(url, 200)}` }); try { thisArg.open(method, 'about:blank', ...args.slice(2)); thisArg.abort(); } catch (e) {} throw new Error('拦截第三方请求'); } return Reflect.apply(target, thisArg, args); } }); XMLHttpRequest.prototype.open = openProxy; this.restoredFns.push(() => { XMLHttpRequest.prototype.open = this.originalXhrOpen; XMLHttpRequest.prototype.send = this.originalXhrSend; }); }, setupMutationFallback() { const self = this; const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType !== 1) continue; const tagName = node.tagName; if (self.interceptTags[tagName]) { self.interceptTags[tagName].forEach(attr => { const value = node[attr] || node.getAttribute(attr); if (value) { const currentDomain = Utils.getCurrentHostname(); const thirdPartyWhitelist = currentConfig.thirdPartyWhitelist[currentDomain] || []; if (Utils.isThirdParty(value, Utils.getBlockParentSubDomainsSetting()) && !urlCache.isWhitelisted(value, thirdPartyWhitelist)) { const keywordWhitelist = currentConfig.keywordWhitelist[currentDomain]; if (keywordWhitelist) { for (const keyword of keywordWhitelist) { if (keyword && value.includes(keyword)) return; } } LogManager.add('interceptThirdParty', node, { type: 'THIRD_PARTY', detail: `${tagName}: ${Utils.truncateString(value, 200)}` }); ResourceCanceller.cancelResourceLoading(node); ProcessedElementsCache.markAsProcessed(node); } } }); } } } }); observer.observe(document.documentElement, { childList: true, subtree: true }); this.restoredFns.push(() => observer.disconnect()); }, check(element) { if (!Utils.shouldInterceptByModule(element, 'interceptThirdParty')) return false; const tagName = element.tagName; if (!this.interceptTags[tagName]) return false; const currentDomain = Utils.getCurrentHostname(); const thirdPartyWhitelist = currentConfig.thirdPartyWhitelist[currentDomain] || []; const blockParentSubDomains = Utils.getBlockParentSubDomainsSetting(); let shouldBlock = false; this.interceptTags[tagName].forEach(attr => { const value = element[attr] || element.getAttribute(attr); if (value && Utils.isThirdParty(value, blockParentSubDomains) && !urlCache.isWhitelisted(value, thirdPartyWhitelist)) { const keywordWhitelist = currentConfig.keywordWhitelist[currentDomain]; if (keywordWhitelist) { for (const keyword of keywordWhitelist) { if (keyword && value.includes(keyword)) return; } } shouldBlock = true; } }); if (shouldBlock) { LogManager.add('interceptThirdParty', element, { type: 'THIRD_PARTY', detail: `${tagName}: ${Utils.truncateString(element.src || element.href || element.data || '', 200)}` }); ResourceCanceller.cancelResourceLoading(element); ProcessedElementsCache.markAsProcessed(element); return true; } return false; } }; const CSPModule = { init() {}, applyCSP() { if (!currentConfig.modules.manageCSP) return; const existingMeta = document.querySelector('meta[http-equiv="Content-Security-Policy"]'); if (existingMeta) { existingMeta.remove(); } const enabledRules = currentConfig.cspRules.filter(rule => rule.enabled); if (enabledRules.length === 0) { return; } const directives = {}; enabledRules.forEach(rule => { const [directive, ...values] = rule.rule.split(' '); if (!directives[directive]) { directives[directive] = new Set(); } values.forEach(value => directives[directive].add(value)); }); let policyString = ''; for (const directive in directives) { if (directives.hasOwnProperty(directive)) { const values = Array.from(directives[directive]).join(' '); policyString += `${directive} ${values}; `; } } policyString = policyString.trim(); if (policyString) { const tryInsert = () => { if (document.head) { const meta = document.createElement('meta'); meta.httpEquiv = "Content-Security-Policy"; meta.content = policyString; document.head.appendChild(meta); } else { const observer = new MutationObserver(() => { if (document.head) { observer.disconnect(); const meta = document.createElement('meta'); meta.httpEquiv = "Content-Security-Policy"; meta.content = policyString; document.head.appendChild(meta); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); } }; tryInsert(); } }, removeCSP() { const existingMeta = document.querySelector('meta[http-equiv="Content-Security-Policy"]'); if (existingMeta) { existingMeta.remove(); } }, updateRule(ruleId, enabled) { const rule = currentConfig.cspRules.find(r => r.id === ruleId); if (rule) { rule.enabled = enabled; } } }; let shadowRoot = null; let settingsContainer = null; let centralScheduler = null; const ensureShadow = () => { if (shadowRoot) return; settingsContainer = document.createElement('div'); settingsContainer.id = 'ad-blocker-settings-container'; settingsContainer.style.cssText = 'position:absolute;top:0;left:0;z-index:2147483647;pointer-events:none;'; settingsContainer.setAttribute('data-adblock-safe', 'true'); document.documentElement.appendChild(settingsContainer); shadowRoot = settingsContainer.attachShadow({ mode: 'closed' }); const style = document.createElement('style'); style.textContent = UI_CSS; shadowRoot.appendChild(style); ProcessedElementsCache.markAsProcessed(settingsContainer); }; const showDiaryWhitelistPanel = (parentMask) => { parentMask.remove(); ensureShadow(); setupNavigationBlocking(); const currentDomain = Utils.getCurrentHostname(); const diaryWhitelist = currentConfig.whitelist[currentDomain] ? Array.from(currentConfig.whitelist[currentDomain]) : []; const mask = document.createElement('div'); mask.className = 'mask'; mask.setAttribute('data-adblock-safe', 'true'); const whitelistHtml = diaryWhitelist.length > 0 ? diaryWhitelist.map((item, index) => `
${escapeHtml(item)}
`).join('') : '
日记白名单为空
'; mask.innerHTML = `
日记白名单 (${escapeHtml(diaryWhitelist.length)}项)
查看和管理拦截日记中添加的白名单条目
${whitelistHtml}
`; mask.querySelectorAll('.danger[data-index]').forEach(btn => { btn.onclick = (e) => { try { const index = parseInt(e.target.dataset.index); const item = diaryWhitelist[index]; if (item) { diaryWhitelist.splice(index, 1); currentConfig.whitelist[currentDomain] = new Set(diaryWhitelist); StorageManager.saveConfig(); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); showDiaryWhitelistPanel(mask); } } finally { teardownNavigationBlocking(); } }; }); mask.querySelector('#clearAll').onclick = () => { try { StorageManager.clearWhitelist(currentDomain, ['diary', 'keyword']); showDiaryWhitelistPanel(mask); } finally { teardownNavigationBlocking(); } }; mask.querySelector('#backBtn').onclick = () => { teardownNavigationBlocking(); mask.remove(); showSettings(); }; mask.onclick = (e) => { const path = e.composedPath(); const isPanel = path.some(el => Utils.isPanelElement(el)); if (!isPanel) { teardownNavigationBlocking(); mask.remove(); showSettings(); } }; shadowRoot.appendChild(mask); mask.style.pointerEvents = 'auto'; mask.querySelector('.panel').style.pointerEvents = 'auto'; }; const showKeywordWhitelistPanel = (parentMask) => { parentMask.remove(); ensureShadow(); setupNavigationBlocking(); const currentDomain = Utils.getCurrentHostname(); const keywordSet = currentConfig.keywordWhitelist[currentDomain] || new Set(); const keywords = Array.from(keywordSet); const mask = document.createElement('div'); mask.className = 'mask'; mask.setAttribute('data-adblock-safe', 'true'); const keywordsHtml = keywords.length > 0 ? keywords.map((kw, index) => `
${escapeHtml(kw)}
`).join('') : '
关键词白名单为空
'; mask.innerHTML = `
关键词白名单 (${escapeHtml(keywords.length)}项)
查看和管理通过关键词添加的白名单条目
${keywordsHtml}
`; mask.querySelectorAll('.danger[data-index]').forEach(btn => { btn.onclick = (e) => { try { const index = parseInt(e.target.dataset.index); const kw = keywords[index]; if (kw) { keywords.splice(index, 1); currentConfig.keywordWhitelist[currentDomain] = new Set(keywords); StorageManager.saveKeywordWhitelist(currentDomain, currentConfig.keywordWhitelist[currentDomain]); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); showKeywordWhitelistPanel(mask); } } finally { teardownNavigationBlocking(); } }; }); mask.querySelector('#clearAll').onclick = () => { try { StorageManager.clearWhitelist(currentDomain, ['keyword']); showKeywordWhitelistPanel(mask); } finally { teardownNavigationBlocking(); } }; mask.querySelector('#backBtn').onclick = () => { teardownNavigationBlocking(); mask.remove(); showSettings(); }; mask.onclick = (e) => { const path = e.composedPath(); const isPanel = path.some(el => Utils.isPanelElement(el)); if (!isPanel) { teardownNavigationBlocking(); mask.remove(); showSettings(); } }; shadowRoot.appendChild(mask); mask.style.pointerEvents = 'auto'; mask.querySelector('.panel').style.pointerEvents = 'auto'; }; const showScriptListPanel = (parentMask) => { parentMask.remove(); ensureShadow(); setupNavigationBlocking(); const scripts = Array.from(document.scripts); const scriptItems = scripts.map((script, index) => { const isExternal = !!script.src; let content = ''; if (isExternal) { content = `外联脚本: ${script.src}`; } else { content = `内嵌脚本: ${script.textContent}`; } return { index: index + 1, isExternal, content, script }; }); const mask = document.createElement('div'); mask.className = 'mask'; mask.setAttribute('data-adblock-safe', 'true'); const listHtml = scriptItems.length > 0 ? scriptItems.map(item => `
脚本 #${item.index} - ${item.isExternal ? '外联' : '内嵌'}
${escapeHtml(item.content)}
`).join('') : '
未找到任何脚本
'; mask.innerHTML = `
当前网页脚本列表 (共${escapeHtml(scriptItems.length)}个)
点击「加黑」将整个脚本内容添加到脚本黑名单
${listHtml}
`; mask.querySelectorAll('.blacklist-btn').forEach(btn => { btn.onclick = (e) => { try { e.stopPropagation(); const encodedContent = btn.dataset.content; if (!encodedContent) return; const content = decodeURIComponent(encodedContent); const currentDomain = Utils.getCurrentHostname(); if (StorageManager.addScriptToBlacklist(currentDomain, content)) { btn.textContent = '已加黑'; btn.style.background = '#999'; btn.disabled = true; } } finally { teardownNavigationBlocking(); } }; }); mask.querySelector('#backBtn').onclick = () => { teardownNavigationBlocking(); showScriptBlacklistPanel(mask); }; mask.querySelector('#closeBtn').onclick = () => { teardownNavigationBlocking(); mask.remove(); }; mask.onclick = (e) => { const path = e.composedPath(); const isPanel = path.some(el => Utils.isPanelElement(el)); if (!isPanel) { teardownNavigationBlocking(); mask.remove(); } }; shadowRoot.appendChild(mask); mask.style.pointerEvents = 'auto'; mask.querySelector('.panel').style.pointerEvents = 'auto'; }; const showScriptBlacklistPanel = (parentMask) => { parentMask.remove(); ensureShadow(); setupNavigationBlocking(); const currentDomain = Utils.getCurrentHostname(); const blacklistSet = currentConfig.scriptBlacklist[currentDomain] || new Set(); const blacklist = Array.from(blacklistSet); const mask = document.createElement('div'); mask.className = 'mask'; mask.setAttribute('data-adblock-safe', 'true'); const blacklistHtml = blacklist.length > 0 ? blacklist.map((kw, index) => `
${escapeHtml(kw)}
`).join('') : '
脚本黑名单为空
'; mask.innerHTML = `
脚本黑名单 (${escapeHtml(blacklist.length)}项)
脚本黑名单模式将拦截内嵌/外联脚本中匹配这些关键词的资源
${blacklistHtml}
`; mask.querySelectorAll('.danger[data-index]').forEach(btn => { btn.onclick = (e) => { try { const index = parseInt(e.target.dataset.index); const kw = blacklist[index]; if (kw) { blacklistSet.delete(kw); StorageManager.saveScriptBlacklist(currentDomain, blacklistSet); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); showScriptBlacklistPanel(mask); } } finally { teardownNavigationBlocking(); } }; }); mask.querySelector('#addBlacklist').onclick = () => { try { const input = mask.querySelector('#newBlacklistKeyword'); const kw = input.value.trim(); if (kw) { StorageManager.addScriptToBlacklist(currentDomain, kw); input.value = ''; if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); showScriptBlacklistPanel(mask); } } finally { teardownNavigationBlocking(); } }; mask.querySelector('#newBlacklistKeyword').addEventListener('keypress', (e) => { if (e.key === 'Enter') { mask.querySelector('#addBlacklist').click(); } }); mask.querySelector('#clearAll').onclick = () => { try { if (blacklist.length > 0) { StorageManager.clearScriptBlacklist(currentDomain); showScriptBlacklistPanel(mask); } } finally { teardownNavigationBlocking(); } }; mask.querySelector('#showScriptList').onclick = () => showScriptListPanel(mask); mask.querySelector('#backBtn').onclick = () => { teardownNavigationBlocking(); mask.remove(); showSettings(); }; mask.onclick = (e) => { const path = e.composedPath(); const isPanel = path.some(el => Utils.isPanelElement(el)); if (!isPanel) { teardownNavigationBlocking(); mask.remove(); showSettings(); } }; shadowRoot.appendChild(mask); mask.style.pointerEvents = 'auto'; mask.querySelector('.panel').style.pointerEvents = 'auto'; }; const showSettings = () => { ensureShadow(); const oldMask = shadowRoot.querySelector('.mask'); if (oldMask) oldMask.remove(); setupNavigationBlocking(); const mask = document.createElement('div'); mask.className = 'mask'; mask.setAttribute('data-adblock-safe', 'true'); mask.innerHTML = `
🛡️广告拦截设置
功能模块:
${Object.keys(MODULE_NAMES).map(key => `
${escapeHtml(MODULE_NAMES[key])}
`).join('')}
`; mask.querySelectorAll('.module-toggle').forEach(checkbox => { checkbox.addEventListener('change', (e) => { const key = e.target.dataset.key; const checked = e.target.checked; if (key === 'scriptBlacklistMode' && checked) { alert('脚本黑名单模式已开启,将根据黑名单关键词拦截内嵌和外联脚本。请先在“脚本黑名单”中添加关键词。'); } if (key === 'interceptThirdParty' && checked) { const hostname = Utils.getCurrentHostname(); if (!currentConfig.thirdPartySettings[hostname] || currentConfig.thirdPartySettings[hostname].blockParentSubDomains === undefined) { const exampleText = `举例:\n当前域名:${hostname}\n主域名:${hostname.split('.').slice(-2).join('.')}\n兄弟域名:othersub.${hostname.split('.').slice(-2).join('.')}\n\n是否拦截所有第三方资源(包括主域名、子域名和兄弟域名)?`; const userChoice = confirm(`拦截第三方资源设置\n\n${exampleText}\n\n点击"确定"拦截所有第三方资源(包括主域名、子域名和兄弟域名)\n点击"取消"只拦截完全无关的第三方域名(放行当前域名、主域名、兄弟域名)`); if (!currentConfig.thirdPartySettings[hostname]) { currentConfig.thirdPartySettings[hostname] = {}; } currentConfig.thirdPartySettings[hostname].blockParentSubDomains = userChoice; StorageManager.saveThirdPartySettings(hostname, currentConfig.thirdPartySettings[hostname]); } } currentConfig.modules[key] = checked; StorageManager.saveConfig(); if (strongBlockingEnabled) { disableStrongBlocking(); } panelOpenCount = 0; location.reload(); }); }); mask.querySelector('#viewLogs').onclick = () => showLogsPanel(mask); mask.querySelector('#manageCSP').onclick = () => showCSPPanel(mask); mask.querySelector('#manageDiaryWhitelist').onclick = () => showDiaryWhitelistPanel(mask); mask.querySelector('#manageThirdParty').onclick = () => showThirdPartyPanel(mask); mask.querySelector('#manageKeywordWhitelist').onclick = () => showKeywordWhitelistPanel(mask); mask.querySelector('#manageScriptBlacklist').onclick = () => showScriptBlacklistPanel(mask); mask.querySelector('#closePanel').onclick = () => { teardownNavigationBlocking(); mask.remove(); }; mask.onclick = (e) => { const path = e.composedPath(); const isPanel = path.some(el => Utils.isPanelElement(el)); if (!isPanel) { teardownNavigationBlocking(); mask.remove(); } }; shadowRoot.appendChild(mask); mask.style.pointerEvents = 'auto'; mask.querySelector('.panel').style.pointerEvents = 'auto'; }; const showLogsPanel = (parentMask) => { parentMask.remove(); ensureShadow(); setupNavigationBlocking(); const mask = document.createElement('div'); mask.className = 'mask'; mask.setAttribute('data-adblock-safe', 'true'); const logsHtml = LogManager.logs.length > 0 ? LogManager.logs.map((log, index) => `
${escapeHtml(log.module)} - ${escapeHtml(log.element)}
${escapeHtml(log.content)}
${log.domain ? `
域名: ${escapeHtml(log.domain)}
` : ''}
`).join('') : '
暂无拦截记录
'; mask.innerHTML = `
拦截日志 (${escapeHtml(LogManager.logs.length)}条)
关键词加白(添加包含关键词的脚本到白名单):
${logsHtml}
`; mask.querySelectorAll('.whitelist-btn').forEach(btn => { btn.onclick = (e) => { try { const index = parseInt(e.target.dataset.index); const logEntry = LogManager.logs[index]; if (logEntry && logEntry.contentIdentifier) { const currentDomain = Utils.getCurrentHostname(); if (logEntry.domain && currentConfig.modules.interceptThirdParty) { StorageManager.addThirdPartyToWhitelist(currentDomain, logEntry.domain); logEntry.content = logEntry.content + ' (域名已加白)'; } else { Whitelisting.add(currentDomain, logEntry.contentIdentifier); logEntry.content = logEntry.content + ' (已加白)'; } StorageManager.saveConfig(); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); btn.textContent = '已加白'; btn.style.background = '#999'; btn.disabled = true; } } finally { teardownNavigationBlocking(); } }; }); mask.querySelector('#addKeywordWhitelist').onclick = () => { try { const input = mask.querySelector('#keywordWhitelistInput'); const keyword = input.value.trim(); if (keyword) { const currentDomain = Utils.getCurrentHostname(); Whitelisting.addKeyword(currentDomain, keyword); StorageManager.saveKeywordWhitelist(currentDomain, currentConfig.keywordWhitelist[currentDomain]); const logEntries = mask.querySelectorAll('.log-entry'); logEntries.forEach((entry, index) => { const logEntry = LogManager.logs[index]; if (logEntry && (logEntry.content.includes(keyword) || logEntry.element.includes(keyword))) { if (logEntry.moduleKey) { if (logEntry.moduleKey === 'removeInlineScripts' || logEntry.moduleKey === 'removeExternalScripts' || logEntry.moduleKey === 'blockDynamicScripts') { if (logEntry.contentIdentifier && !Whitelisting.isContentWhitelisted(currentDomain, logEntry.contentIdentifier)) { Whitelisting.add(currentDomain, logEntry.contentIdentifier); } } else if (logEntry.moduleKey === 'interceptThirdParty') { if (logEntry.domain) { StorageManager.addThirdPartyToWhitelist(currentDomain, logEntry.domain); } } } else { const moduleName = logEntry.module; if (moduleName.includes('内嵌脚本') || moduleName.includes('外联脚本') || moduleName.includes('动态脚本')) { if (logEntry.contentIdentifier && !Whitelisting.isContentWhitelisted(currentDomain, logEntry.contentIdentifier)) { Whitelisting.add(currentDomain, logEntry.contentIdentifier); } } else if (moduleName.includes('第三方资源')) { if (logEntry.domain) { StorageManager.addThirdPartyToWhitelist(currentDomain, logEntry.domain); } } } const whitelistBtn = entry.querySelector('.whitelist-btn'); if (whitelistBtn) { whitelistBtn.textContent = '关键词已加白'; whitelistBtn.style.background = '#999'; whitelistBtn.disabled = true; } entry.classList.add('keyword-highlight'); } }); StorageManager.saveConfig(); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); input.value = ''; } } finally { teardownNavigationBlocking(); } }; mask.querySelector('#keywordWhitelistInput').addEventListener('keypress', (e) => { if (e.key === 'Enter') { mask.querySelector('#addKeywordWhitelist').click(); } }); mask.querySelector('#backBtn').onclick = () => { teardownNavigationBlocking(); mask.remove(); showSettings(); }; mask.onclick = (e) => { const path = e.composedPath(); const isPanel = path.some(el => Utils.isPanelElement(el)); if (!isPanel) { teardownNavigationBlocking(); mask.remove(); showSettings(); } }; shadowRoot.appendChild(mask); mask.style.pointerEvents = 'auto'; mask.querySelector('.panel').style.pointerEvents = 'auto'; }; const showCSPPanel = (parentMask) => { parentMask.remove(); ensureShadow(); setupNavigationBlocking(); const mask = document.createElement('div'); mask.className = 'mask'; mask.setAttribute('data-adblock-safe', 'true'); const rulesHtml = currentConfig.cspRules.map(rule => `
${escapeHtml(rule.name)}
`).join(''); mask.innerHTML = `
CSP策略管理
当前状态: ${currentConfig.modules.manageCSP ? '✅已启用' : '❌已禁用'}
${rulesHtml}
`; mask.querySelectorAll('.csp-toggle').forEach(checkbox => { checkbox.addEventListener('change', (e) => { const id = parseInt(e.target.dataset.id); const enabled = e.target.checked; CSPModule.updateRule(id, enabled); currentConfig.modules.manageCSP = currentConfig.cspRules.some(rule => rule.enabled); StorageManager.saveConfig(); location.reload(); }); }); mask.querySelector('#enableCSP').onclick = () => { currentConfig.modules.manageCSP = true; StorageManager.saveConfig(); location.reload(); }; mask.querySelector('#disableCSP').onclick = () => { currentConfig.modules.manageCSP = false; StorageManager.saveConfig(); location.reload(); }; mask.querySelector('#allOn').onclick = () => { currentConfig.cspRules.forEach(rule => rule.enabled = true); currentConfig.modules.manageCSP = true; StorageManager.saveConfig(); location.reload(); }; mask.querySelector('#allOff').onclick = () => { currentConfig.cspRules.forEach(rule => rule.enabled = false); currentConfig.modules.manageCSP = false; StorageManager.saveConfig(); location.reload(); }; mask.querySelector('#backBtn').onclick = () => { teardownNavigationBlocking(); mask.remove(); showSettings(); }; mask.onclick = (e) => { const path = e.composedPath(); const isPanel = path.some(el => Utils.isPanelElement(el)); if (!isPanel) { teardownNavigationBlocking(); mask.remove(); showSettings(); } }; shadowRoot.appendChild(mask); mask.style.pointerEvents = 'auto'; mask.querySelector('.panel').style.pointerEvents = 'auto'; }; const showThirdPartyPanel = (parentMask) => { parentMask.remove(); ensureShadow(); setupNavigationBlocking(); const currentDomain = Utils.getCurrentHostname(); const whitelist = currentConfig.thirdPartyWhitelist[currentDomain] || []; const mask = document.createElement('div'); mask.className = 'mask'; mask.setAttribute('data-adblock-safe', 'true'); const whitelistHtml = whitelist.length > 0 ? whitelist.map((item, index) => `
${escapeHtml(item)}
`).join('') : '
白名单为空
'; mask.innerHTML = `
第三方白名单 (${escapeHtml(whitelist.length)}项)
已拦截的第三方域名可以添加到白名单中
${whitelistHtml}
`; mask.querySelectorAll('.danger[data-index]').forEach(btn => { btn.onclick = (e) => { try { const index = parseInt(e.target.dataset.index); const item = whitelist[index]; if (item) { whitelist.splice(index, 1); StorageManager.saveThirdPartyWhitelist(currentDomain, whitelist); Whitelisting.removeKeywordsMatchingDomain(item); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); showThirdPartyPanel(mask); } } finally { teardownNavigationBlocking(); } }; }); mask.querySelector('#addWhitelist').onclick = () => { try { const input = mask.querySelector('#newWhitelist'); const value = input.value.trim(); if (value) { StorageManager.addThirdPartyToWhitelist(currentDomain, value); input.value = ''; if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); showThirdPartyPanel(mask); } } finally { teardownNavigationBlocking(); } }; mask.querySelector('#newWhitelist').addEventListener('keypress', (e) => { if (e.key === 'Enter') { mask.querySelector('#addWhitelist').click(); } }); mask.querySelector('#clearAll').onclick = () => { try { StorageManager.clearWhitelist(currentDomain, ['thirdParty']); showThirdPartyPanel(mask); } finally { teardownNavigationBlocking(); } }; mask.querySelector('#backBtn').onclick = () => { teardownNavigationBlocking(); mask.remove(); showSettings(); }; mask.onclick = (e) => { const path = e.composedPath(); const isPanel = path.some(el => Utils.isPanelElement(el)); if (!isPanel) { teardownNavigationBlocking(); mask.remove(); showSettings(); } }; shadowRoot.appendChild(mask); mask.style.pointerEvents = 'auto'; mask.querySelector('.panel').style.pointerEvents = 'auto'; }; const UIController = { initialized: false, mutationObserver: null, batchProcessingQueue: [], batchSize: 20, isProcessingBatch: false, lastProcessTime: 0, init() { if (this.initialized) return; this.initialized = true; this.applyInitialModuleStates(); this.registerMenuCommands(); this.applyModuleSettings(); this.setupObservers(); this.setupResourceScan(); }, applyInitialModuleStates() { Object.keys(DEFAULT_MODULE_STATE).forEach(key => { if (currentConfig.modules[key] === undefined) { currentConfig.modules[key] = DEFAULT_MODULE_STATE[key]; } }); if (currentConfig.modules.manageCSP === undefined) { currentConfig.modules.manageCSP = false; } if (currentConfig.modules.interceptThirdParty === undefined) { currentConfig.modules.interceptThirdParty = false; } if (currentConfig.modules.blockDynamicScripts === undefined) { currentConfig.modules.blockDynamicScripts = false; } if (currentConfig.modules.scriptBlacklistMode === undefined) { currentConfig.modules.scriptBlacklistMode = false; } }, registerMenuCommands() { GM_registerMenuCommand('⚙️ 广告拦截设置面板', () => showSettings()); GM_registerMenuCommand('🗑️ 清空所有白名单', () => { if (confirm('确定清空当前域名的所有白名单(包括第三方白名单、关键词白名单等)吗?')) { const hostname = Utils.getCurrentHostname(); Whitelisting.clearDomainWhitelist(hostname); StorageManager.clearDomainThirdPartyWhitelist(hostname); if (currentConfig.keywordWhitelist[hostname]) { currentConfig.keywordWhitelist[hostname].clear(); StorageManager.saveKeywordWhitelist(hostname, currentConfig.keywordWhitelist[hostname]); } StorageManager.saveConfig(); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); GM_notification({ text: '所有白名单已清空', title: '广告拦截器' }); location.reload(); } }); GM_registerMenuCommand('🔄 重置所有设置', () => { if (confirm('确定重置所有设置吗?这将清空所有白名单并关闭所有模块,恢复到初始状态。')) { StorageManager.resetAllSettings(); if (centralScheduler) centralScheduler.clearCache(); urlCache.clear(); LogManager.clearLoggedIdentifiers(); GM_notification({ text: '所有设置已重置', title: '广告拦截器' }); location.reload(); } }); }, applyModuleSettings() { RemoveInlineScriptsModule.init(); RemoveExternalScriptsModule.init(); ThirdPartyInterceptionModule.init(); DynamicScriptInterceptor.init(); ScriptBlacklistModeModule.init(); CSPModule.init(); centralScheduler = new CentralScheduler(); }, setupObservers() { const relevantModulesEnabled = Object.keys(MODULE_NAMES).some(key => currentConfig.modules[key] && ( key === 'removeInlineScripts' || key === 'removeExternalScripts' || key === 'interceptThirdParty' || key === 'blockDynamicScripts' || key === 'scriptBlacklistMode' ) ); if (!relevantModulesEnabled) return; this.mutationObserver = new MutationObserver(Utils.throttle((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && !ProcessedElementsCache.isProcessed(node) && !Utils.isParentProcessed(node)) { this.addToBatchProcessingQueue(node); } } } }, 100)); this.mutationObserver.observe(document.documentElement, { childList: true, subtree: true, }); this.processExistingElementsBatch(); }, setupResourceScan() { if (currentConfig.modules.interceptThirdParty) { document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { this.scanExistingResources(); }, 1000); }); } }, scanExistingResources() { const currentDomain = Utils.getCurrentHostname(); const thirdPartyWhitelist = currentConfig.thirdPartyWhitelist[currentDomain] || []; const blockParentSubDomains = Utils.getBlockParentSubDomainsSetting(); const selectors = [ 'script[src]', 'iframe[src]', 'img[src]', 'img[data-src]', 'embed[src]', 'object[data]', 'link[href]' ]; selectors.forEach(selector => { try { const elements = document.querySelectorAll(selector); elements.forEach(element => { if (ProcessedElementsCache.isProcessed(element) || Utils.isParentProcessed(element)) { return; } const tagName = element.tagName; let url = ''; if (tagName === 'SCRIPT') url = element.src; else if (tagName === 'IFRAME') url = element.src; else if (tagName === 'IMG') url = element.src || element.getAttribute('data-src'); else if (tagName === 'EMBED') url = element.src; else if (tagName === 'OBJECT') url = element.data; else if (tagName === 'LINK') url = element.href; if (url && Utils.isThirdParty(url, blockParentSubDomains) && !urlCache.isWhitelisted(url, thirdPartyWhitelist)) { const contentIdentifier = Utils.getContentIdentifier(element); if (contentIdentifier && !Whitelisting.isContentWhitelisted(currentDomain, contentIdentifier)) { LogManager.add('interceptThirdParty', element, { type: 'THIRD_PARTY_SCAN', detail: `扫描发现: ${tagName}: ${Utils.truncateString(url, 200)}` }); } } }); } catch (e) {} }); }, addToBatchProcessingQueue(element) { if (element.getAttribute && element.getAttribute('data-adblock-safe') === 'true') { ProcessedElementsCache.markAsProcessed(element); return; } this.batchProcessingQueue.push(element); if (!this.isProcessingBatch) { this.processBatch(); } }, processBatch() { if (!centralScheduler) return; // 防止竞态条件 this.isProcessingBatch = true; const processChunk = () => { const now = Date.now(); if (now - this.lastProcessTime < 16) { requestAnimationFrame(processChunk); return; } const chunk = this.batchProcessingQueue.splice(0, this.batchSize); this.lastProcessTime = now; chunk.forEach(node => { centralScheduler.processElement(node); }); if (this.batchProcessingQueue.length > 0) { requestAnimationFrame(processChunk); } else { this.isProcessingBatch = false; } }; requestAnimationFrame(processChunk); }, processExistingElementsBatch() { const selector = 'script, iframe, img, a[href], style, link[rel="preload"], link[rel="prefetch"], embed, object, link[href]'; const elementsToProcess = Array.from(document.querySelectorAll(selector)); const processInChunks = () => { const chunk = elementsToProcess.splice(0, this.batchSize * 2); chunk.forEach(element => { if (!ProcessedElementsCache.isProcessed(element) && !Utils.isParentProcessed(element)) { this.addToBatchProcessingQueue(element); } }); if (elementsToProcess.length > 0) { setTimeout(processInChunks, 0); } }; processInChunks(); } }; StorageManager.loadConfig(); if (currentConfig.modules.blockDynamicScripts) { DynamicScriptInterceptor.init(); } function safeInit() { if (document.documentElement) { UIController.init(); if (currentConfig.modules.manageCSP) CSPModule.applyCSP(); return true; } return false; } if (!safeInit()) { const mo = new MutationObserver(() => { if (safeInit()) mo.disconnect(); }); mo.observe(document, { childList: true }); document.addEventListener('DOMContentLoaded', () => { if (!safeInit()) setTimeout(safeInit, 100); }, { once: true }); setTimeout(() => { if (!safeInit()) { UIController.init(); } }, 5000); } })();