// ==UserScript== // @name 网页广告拦截器 // @namespace http://tampermonkey.net/ // @version 6.3.0 // @author DeepSeek&Gemini // @description 一个电脑端油猴及手机端via浏览器适用的强大广告拦截器(其他浏览器因脚本注入时机较晚、广告脚本先执行等原因,拦截效果会打折扣) // @match *://*/* // @license MIT // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @grant GM_notification // @grant GM_listValues // @grant GM_xmlhttpRequest // @run-at document-start // ==/UserScript== (function () { 'use strict'; // ======================== 调试辅助 ======================== // 【功能】全脚本统一的异常/调试日志入口 _debugCatch:所有 try/catch 的失败信息都从这里输出。 // 【机制】仅当 设置→高级→调试日志(DEBUG) 开启时才输出到 F12→Console([AB] 前缀),并自动附带调用位置行号; // 自身带防递归保护,catch 块里调用它不会二次抛错。 // 【用户体验】排查问题三步:① 开启 DEBUG → ② 复现问题 → ③ F12 Console 看 [AB] 日志; // 拦截记录看 设置面板→「拦截日志」(每条可点「加白」放行)。 const _debugCatch = function(e, ctx) { if (_debugCatch._isRunning) return; _debugCatch._isRunning = true; try { if (typeof currentConfig !== 'undefined' && currentConfig.DEBUG && typeof console !== 'undefined') { // 自动附带调用位置(行号), 无需各调用点手动传入截断上下文串 var _loc = ''; try { var _st = new Error().stack || ''; var _ln = _st.split('\n')[2]; if (_ln) _loc = ' @' + _ln.trim().slice(0, 90); } catch(_e2) {} console.warn('[AB]', ctx || '', _loc, e && e.message ? e.message : e); } } catch(_) { try { console.error('[AB] _debugCatch error:', _ && _.message ? _.message : _); } catch(__) {} } finally { _debugCatch._isRunning = false; } }; // ======================== 初始样式注入 ======================== // 【功能】页面一启动就注入全局隐藏类 ._uh 的样式定义。 // 【机制】被拦截的元素统一加上 ._uh 类 → visibility:hidden + opacity:0 + pointer-events:none, // 保留原布局尺寸(不引发页面跳动),且不可点击;加白恢复时移除该类即可。 // 【注意】此样式独立于任何模块,模块全关也保留;data-hs 属性用于识别脚本自身的样式标签。 (function () { try { var s = document.createElement('style'); s.setAttribute('data-hs', 'true'); s.textContent = '._uh{visibility:hidden!important;opacity:0!important;pointer-events:none!important}'; (document.documentElement || document).appendChild(s); } catch(e) { _debugCatch(e, "初始隐藏样式注入失败"); } })(); // ======================== 默认配置 ======================== // 【功能】定义脚本的全部默认状态:模块开关、高级配置项、CSP 规则模板、内置黑名单关键词、启发式正则、标题黑白名单。 // 【机制】面板里的开关/设置修改后写入 GM 存储(按域名隔离),刷新后生效;「全局重置」恢复本文件默认值。 // 【注意】内置黑名单是广告特征字符串(统计站域名/混淆代码片段),命中即拦,可在「脚本黑名单」面板单独移除; // 启发式黑名单默认关闭(误杀风险较高,需在高级设置手动开启)。 const DEFAULT_MODULE_STATE = { removeInlineScripts: false, removeExternalScripts: false, interceptThirdParty: false, blockDynamicScripts: false, manageCSP: false, overlayIntercept: false, scriptBlacklistMode: true, }; const DEFAULT_CONFIG_STATE = { inlineScriptStrictMode: false, dynamicScriptStrictMode: false, thirdPartyStrictMode: false, thirdPartyStrictMethod: false, spoofUAEnabled: false, simplePlatformSpoof: false, residualCleanupEnabled: false, iframeUIFix: false, redirectBlockerEnabled: true, builtinBlacklistEnabled: true, redirectBlockerCompatibilityMode: true, heuristicBlacklistEnabled: false, crossTagPatternThreshold: 3, trackingParamsCleanerEnabled: true, redirectTitleBlacklistEnabled: true, redirectTitleWhitelistEnabled: true, redirectBlockAllEnabled: false, redirectWhitelistBlockMode: false, DEBUG: false, adLinkClickBlock: true, logNoiseReduction: true, overlayStrictMode: false, }; const DEFAULT_CSP_RULES_TEMPLATE = [ { id: 1, name: '只允许同源外部脚本', rule: "script-src 'self'", enabled: false }, { id: 2, name: '只允许同源外部样式', rule: "style-src 'self'", enabled: false }, { id: 3, name: '只允许同源图片', rule: "img-src 'self'", enabled: false }, { id: 4, name: '只允许同源框架', rule: "frame-src 'self'", enabled: false }, { id: 5, name: '只允许同源媒体', rule: "media-src 'self'", enabled: false }, { id: 6, name: '只允许同源对象与嵌入', rule: "object-src 'self'", enabled: false }, { id: 7, name: '只允许同源网络请求', rule: "connect-src 'self'", enabled: false }, ]; // 内置黑名单关键词:常见广告/统计脚本特征串(如统计站域名、混淆代码片段), // 脚本内容或地址命中任意一条即拦截。 const BUILTIN_BLACKLIST_KEYWORDS = [ 'hm.baidu.com', 'tongji()', 'tj()', 'push.js', 'cnzz', 'histats.com', 'https://hongosi.xn--', '{return void 0!==b[a]?b[a]:a}).join("")}', '${scripts[randomIndex]}', '${scripts[Math.random()', 'https://"+Date.parse(new Date())+', 'https://"+(new Date().getDate())+"', 'https://{randomstr}.', 'new Function(t)()', 'new Function(b)()', 'new Function(c)()', 'new Function(t);', 'new Function(b);', 'new Function(c);', "new Function('d',e)", 'new Function(document[', 'new Function(function(p,a,c,k,e,d)', 'function a(a){', 'function b(b){', 'function c(c){', 'Math.floor(2147483648 * Math.random());', 'Math.floor(Math.random()*url.length)', 'Math.floor(Math.random() * urls.length)', "new Date()['getTime']()", 'newDate=new window', 'Math.floor(((new Date()).getTime()', '&&navigator[', '=navigator;', 'navigator.platform){setTimeout(function', 'disableDebugger', 'blockDeveloperTools', '["Date"]())[\'getTime\']()', '\')', '<\\/\'+\'s\'+\'c\'+\'ri\'+\'pt\'+\'>\')', '(\'#htmlContenthtml\').html', 'D.createElement(\'span\');', 'window.$m(', '{win:false,mac:false,xll:false}', 'function|getDate', 'parseInt(Math[\'random\'' ]; var _heuristicRegexGroups = null; function getHeuristicRegexGroups() { if (_heuristicRegexGroups !== null) return _heuristicRegexGroups; if (!currentConfig.heuristicBlacklistEnabled) return null; try { const hexGroup = [ '(?:_0x[a-zA-Z0-9]{4,}.*){5,}', 'parseInt\\s*\\(\\s*[\'"\\x60]0x[0-9a-fA-F]{2,}[\'"\\x60]?\\s*\\)\\s*(?:\\||\\^|&)', '(?:\\\\x[0-9a-fA-F]{2}){10,}', '(?:\\\\u[0-9a-fA-F]{4}){10,}', '\\\\x[0-9a-fA-F]{2}(?:\\\\x[0-9a-fA-F]{2}){20,}', '(?:\\^|\\||~)\\s*0x[0-9a-fA-F]{2,}\\s*(?:\\^|\\||~)', 'parseInt\\s*\\([^)]+\\)\\s*\\^\\s*0x[0-9a-fA-F]{2,}', ]; const domGroup = [ '(?:window|document|navigator|location)\\s*\\[\\s*(?![\'"](?:getElementById|querySelector|querySelectorAll|addEventListener|removeEventListener|createElement|getElementsByClassName|getElementsByTagName|getElementsByName|getComputedStyle|matchMedia|requestAnimationFrame|cancelAnimationFrame|fetch|XMLHttpRequest|open|close|alert|confirm|prompt|location|top|parent|self|opener|document|history|navigator|screen|localStorage|sessionStorage|postMessage|setTimeout|setInterval|clearTimeout|clearInterval|atob|btoa|dispatchEvent|stopPropagation|preventDefault))[\'"]\\s*[^\\+]+?\\]\\s*\\(', '\\bwith\\s*\\(\\s*(?:document|window)\\s*\\)\\s*\\{', '(?:window|document)\\s*\\[\\s*[\'"][^\'"]+[\'"]\\s*\\+\\s*[\'"][^\'"]+[\'"]\\s*\\]\\s*\\(', ]; const controlGroup = [ 'blockDeveloperTools|disableDebugger|devtoolschange', 'setTimeout\\s*\\(\\s*[\'"\\x60](?:javascript:|eval\\s*\\(|Function\\s*\\()', 'setInterval\\s*\\(\\s*[\'"\\x60](?:javascript:|eval\\s*\\(|Function\\s*\\()', 'setTimeout\\s*\\(\\s*(?:function|\\(\\)\\s*=>)\\s*\\{[^}]*?location\\.(?:href|replace|assign)', '\\b(?:document|window)\\.(?:oncontextmenu|onkeydown|onkeyup|onselectstart|ondragstart|onmousedown)\\s*=\\s*function', '\\bwith\\s*\\(', ]; const cipherGroup = [ 'atob\\s*\\([^)]*\\)[^;]{0,100}?(?:eval|Function)\\s*\\(', '(?:eval|Function|document\\.write)\\s*\\([^)]{0,200}?atob\\s*\\(', 'atob\\s*\\(.+?\\.split\\s*\\(\\s*[\'"][\'"]\\s*\\)\\s*\\.map', '\\batob\\s*\\(\\s*[\'"][A-Za-z0-9+/=]{80,}[\'"]\\s*\\)[^;]{0,200}?(?:eval|Function|document\\.write|\\.src\\s*=|\\.href\\s*=)', 'atob\\s*\\(.*?\\).*?atob\\s*\\(', 'unescape\\s*\\(.*?\\)\\s*\\+\\s*unescape', '(?:eval|Function)\\s*\\([^)]{0,200}?unescape\\s*\\(', 'unescape\\s*\\([^)]*\\)[^;]{0,100}?(?:eval|Function)\\s*\\(', '(?:eval|Function|JSON\\.parse)\\s*\\([^)]{0,200}?decodeURIComponent\\s*\\(', 'decodeURIComponent\\s*\\([^)]*\\)[^;]{0,100}?(?:eval|Function)\\s*\\(', 'JSON\\s*\\.\\s*parse\\s*\\(\\s*(?:atob|decodeURIComponent|unescape)\\s*\\(', '"[\\w\\-~!@#$%^&*()+=\\[\\]{}|\\\\:;\'<>,.?/` ]{60,}"[^;]{0,120}?(?:eval|Function)\\s*\\(', '\\.replace\\s*\\(\\s*\\/\\\\\\\\[a-f0-9]+\\/g\\s*,\\s*[\'"\\x60]\\\\\\\\x[\'"\\x60]\\s*\\).*?(?:eval|Function)\\s*\\(', '(?:\\\\x[0-9a-fA-F]{2}){10,}[^;]{0,100}?(?:eval|Function)\\s*\\(', '(?:\\\\u[0-9a-fA-F]{4}){5,}[^;]{0,100}?(?:eval|Function)\\s*\\(', '(?:decodeURIComponent|atob|unescape)\\s*\\([^)]{5,}\\)[^;]{0,300}?(?:decodeURIComponent|atob|unescape)\\s*\\(', '(?:var|let|const)\\s+\\w+\\s*=\\s*[\'"][A-Za-z0-9+/=]{300,}[\'"][^;]{0,80}?(?:eval|Function)\\s*\\(', '(?:String\\.fromCharCode|fromCodePoint)\\s*\\([^)]{20,}\\)[^;]{0,100}?(?:eval|Function)\\s*\\(', '\\[\\d{2,3}(?:,\\d{2,3}){5,}\\][^;]{0,50}?(?:eval|Function|String\\.fromCharCode)\\s*\\(', '\\.split\\s*\\(\\s*""\\s*\\)\\s*\\.\\s*map\\s*\\([^)]{0,1000}\\)[^;]{0,1000}?new\\s+Function', '\\.split\\s*\\(\\s*[\'"][\'"]\\s*\\)\\.reverse\\s*\\(\\s*\\)\\.join', '\\.split\\s*\\(\\s*[\'"][^\'"]{1,5}[\'"]\\s*\\)\\s*\\.reverse\\s*\\(\\s*\\)\\s*\\.join', '\\.split\\s*\\(\\s*[\'"][^\'"]{1,5}[\'"]\\s*\\)\\s*\\.map\\s*\\([^)]{0,1000}\\)[^;]{0,500}?\\.(?:join|toString)', 'String\\.fromCharCode\\.apply\\s*\\(null,\\s*\\[', '\\(function\\s*\\([^)]*\\)\\s*\\{[^}]{0,800}?(?:atob[^}]{0,800}?(?:eval|Function)|(?:eval|Function)[^}]{0,800}?atob)\\s*\\(', 'function\\s+\\w+\\s*\\([^)]*\\)\\s*\\{[^}]*?atob[^}]*?(?:eval|Function)\\s*\\(', '(?:toString|split|replace|substring|substr|slice|trim|charAt|charCodeAt|join|reverse){5,}[^;]{0,200}?(?:eval|Function|atob|document\\.write)\\s*\\(', '(?:setTimeout|setInterval)\\s*\\(\\s*(?:function|\\(\\)\\s*=>)\\s*\\{[^}]{0,300}?(?:eval|Function|atob)\\s*\\(', '(?:fetch|XMLHttpRequest)\\s*\\(?[^)]{0,200}?\\)[^;]{0,300}?(?:eval|Function)\\s*\\(', '\\beval\\s*\\(\\s*atob\\s*\\(', '\\beval\\s*\\(\\s*String\\.fromCharCode\\s*\\(', '\\blocation\\s*\\.\\s*href\\s*=\\s*(?:atob|String\\.fromCharCode|decodeURIComponent)', '\\blocation\\s*(?:\\.\\s*href|\\[\\s*[\'"]href[\'"]\\s*\\])\\s*=\\s*[\'"][^\'"]{200,}', 'setTimeout\\s*\\(\\s*[\'"][^\'"]*location\\.href[^\'"]*[\'"]', 'new\\s+Function\\s*\\(\\s*[\'"\\x60][^\'"\\x60]{200,}[\'"\\x60]\\s*\\)', '(?:eval|Function)\\s*\\(\\s*(?:[\\w$]+\\s*\\+\\s*){2,}[\\w$]+\\s*\\)', '(?:eval|Function)\\s*\\(\\s*[\\w$]+\\s*\\.\\s*(?:split|slice|substring|replace|join|reverse)\\s*\\(', '(?:eval|Function)\\s*\\(\\s*\\[\\s*[^\\]]{0,300}?\\]\\s*\\.\\s*join\\s*\\(\\s*[\'"\\x60][\'"\\x60]\\s*\\)\\s*\\)', '\\b(?:atob|decodeURIComponent|unescape)\\s*\\([^)]{30,}\\)\\s*;?\\s*(?:eval|Function)\\s*\\(', 'String\\.fromCharCode\\s*\\(\\s*\\d{1,3}(?:\\s*,\\s*\\d{1,3})+\\s*\\)\\s*\\+', '\\b\\w{1,4}\\s*=\\s*(?:atob|decodeURIComponent)\\s*\\(\\s*[\'"\\x60][A-Za-z0-9+/=]{80,}[\'"\\x60]\\s*\\)', '\\b(?:window|document|self|top)\\s*\\[\\s*[\'"\\x60][^\'"\\x60]{1,10}[\'"\\x60]\\s*\\]\\s*=\\s*(?:atob|String\\.fromCharCode|decodeURIComponent|unescape)', '(?:eval|Function)\\s*\\(\\s*[\'"\\x60][^\'"\\x60]{0,120}[\'"\\x60]\\s*\\+', '\\b[a-z_][a-zA-Z0-9_]{1,5}\\s*\\(\\s*\\d{5,}(?:\\s*,\\s*\\d+)*\\s*\\)', '\\$\\s*\\(\\s*(?:function\\s*\\([^)]*\\)\\s*\\{|document\\)\\.ready)\\s*[^}]*?\\w{2,}\\s*\\(\\s*\\d{5,}', '^[a-zA-Z_$]\\w{2,}\\s*\\(\\s*\\d{4,}(?:\\s*,\\s*\\d+)*\\s*\\)\\s*;?\\s*$', ]; const miscGroup = [ 'constructor\\s*\\(\\s*[\'"\\x60]return\\s+this[\'"\\x60]\\s*\\)\\s*\\(', '\\.constructor\\.constructor\\([^\\)]+\\)\\s*\\(', 'try\\s*\\{[^}]{0,200}?(?:eval|Function|atob|document\\.write)\\s*\\([^)]{0,200}\\}\\s*catch\\s*\\(\\s*[a-zA-Z_]+\\s*\\)\\s*\\{\\s*\\}', 'try\\s*\\{[^}]{0,150}\\}\\s*catch\\s*\\(\\s*\\w+\\s*\\)\\s*\\{\\s*return\\s+(?:_0x|this|constructor)', '\\.style\\.position\\s*=\\s*[\'"]fixed[\'"][\\s\\S]{0,50}?\\.style\\.zIndex\\s*=\\s*[\'"]?[1-9]\\d{4,}', '_0x[0-9a-f]+\\s*=\\s*function\\s*\\(\\s*_0x[0-9a-f]+\\s*,\\s*_0x[0-9a-f]+\\s*\\)', '_0x[0-9a-f]+\\s*=\\s*function\\s*\\(\\s*_0x[0-9a-f]+\\s*,\\s*_0x[0-9a-f]+\\s*,\\s*_0x[0-9a-f]+\\s*\\)', '(?:Object\\.keys|Object\\.values)\\s*\\(\\s*_0x[0-9a-f]+\\s*\\)\\.map\\s*\\(', '_0x[0-9a-f]+\\s*\\.forEach\\s*\\(\\s*(?:_0x[0-9a-f]+|function)\\s*\\(', '(?:const|let|var)\\s*\\{[^}]{5,}\\}\\s*=\\s*_0x[0-9a-f]+\\s*\\(', '!function\\s*\\(\\s*\\)\\s*\\{[^}]{0,200}return\\s+_0x[0-9a-f]+\\s*\\(', 'JSON\\.parse\\s*\\(\\s*_0x[0-9a-f]+\\s*\\(', '(?:window|document|navigator)\\s*\\[\\s*[\'"][^\'"]{2,10}[\'"]\\s*\\+\\s*[\'"][^\'"]{2,10}[\'"]\\s*\\]\\s*\\(', 'function\\s+[a-z]\\d[a-z0-9]{6,}', '\\[\\s*[\'"][^\'"]{1,10}[\'"]\\s*\\+\\s*[\'"][^\'"]{1,10}[\'"]\\s*\\+\\s*[\'"][^\'"]{1,10}[\'"]\\s*\\]', 'arguments\\s*\\[\\s*\\d+\\s*\\]\\s*\\(\\s*\\d+\\s*\\)', 'try\\s*\\{\\s*[a-zA-Z_]\\w*?\\d{1,4}\\s*\\([^)]*\\)\\s*;?\\s*\\}\\s*catch\\s*\\(\\s*\\w+\\s*\\)\\s*\\{\\s*\\}?\\s*;?\\s*try\\s*\\{\\s*[a-zA-Z_]\\w*?\\d{1,4}\\s*\\([^)]*\\)\\s*;?\\s*\\}\\s*catch\\s*\\(\\s*\\w+\\s*\\)\\s*\\{\\s*\\}?\\s*;?\\s*try\\s*\\{\\s*[a-zA-Z_]\\w*?\\d{1,4}\\s*\\([^)]*\\)\\s*;?\\s*\\}\\s*catch\\s*\\(\\s*\\w+\\s*\\)\\s*\\{\\s*\\}?\\s*', 'document\\.cookie[^;]{0,200}?(?:new\\s+)?Image\\s*[=(]', '_0x[0-9a-f]+\\s*\\(\\s*_0x[0-9a-f]+\\s*\\)[^;]{0,50}?(?:eval|Function|atob|JSON\\.parse)\\s*\\(', ]; const baseGroups = [ new RegExp(hexGroup.map(p => `(?:${p})`).join('|')), new RegExp(domGroup.map(p => `(?:${p})`).join('|')), new RegExp(controlGroup.map(p => `(?:${p})`).join('|')), new RegExp(cipherGroup.map(p => `(?:${p})`).join('|')), new RegExp(miscGroup.map(p => `(?:${p})`).join('|')), ]; _heuristicRegexGroups = baseGroups; return baseGroups; } catch (e) { _heuristicRegexGroups = null; return null; } } const DEFAULT_REDIRECT_TITLE_BLACKLIST = ['小说', '章节', '阅读', '在线', '热播', '影院', '影音', 're:第[\\d零一二三四五六七八九十百千万亿]+[章节页]']; const DEFAULT_REDIRECT_TITLE_WHITELIST = ['银行', '转账', '支付', '安全中心', '登录', '验证', '密码', '交易', '脚本', '搜索']; let currentConfig = { ...DEFAULT_CONFIG_STATE, modules: { ...DEFAULT_MODULE_STATE }, cspRules: DEFAULT_CSP_RULES_TEMPLATE.map((rule) => ({ ...rule })), whitelist: new Set(), keywordWhitelist: new Set(), scriptBlacklist: new Set(), thirdPartyWhitelist: [], removedBuiltinKeywords: new Set(), whitelistDisplayNames: new Map(), redirectTitleBlacklistKeywords: [...DEFAULT_REDIRECT_TITLE_BLACKLIST], redirectTitleWhitelistKeywords: [...DEFAULT_REDIRECT_TITLE_WHITELIST], trackingParamKeys: [], }; const ConfigUpdater = { saveNow() { // 存储异常隔离: GM_setValue 失败(配额/瞬时错误)不中断调用方流程, // 否则 change handler 会在保存处中断, 表现为"开关点了没反应" try { StorageManager.saveConfig(); } catch (e) { _debugCatch(e, '配置保存失败(GM 存储异常)'); } }, }; // ======================== 配置存储管理 ======================== // 【功能】配置的加载/保存/重置:按域名隔离存储(每个网站一套设置),并同步全局参数(标题黑白名单、追踪参数)。 // 【机制】GM_getValue/GM_setValue 读写 JSON;加载时逐项合并到 currentConfig(缺省项用默认值兜底); // 保存失败不影响当前内存配置(异常已被隔离,不会卡住面板开关)。 // 【用户体验】每个网站可独立配置;「当前域名重置」只清本站设置,「全局重置」清空全部数据。 const StorageManager = { getConfigKey(domain) { return `adblock_unified_config_${domain}`; }, loadConfig() { const hostname = location.hostname; const key = this.getConfigKey(hostname); try { const saved = GM_getValue(key, null); if (saved) { const data = JSON.parse(saved); if (data.modules) Object.assign(currentConfig.modules, data.modules); if (data.cspRules) currentConfig.cspRules = data.cspRules.map((r) => ({ ...r })); if (Array.isArray(data.whitelist)) currentConfig.whitelist = new Set(data.whitelist); if (Array.isArray(data.keywordWhitelist)) currentConfig.keywordWhitelist = new Set(data.keywordWhitelist); if (Array.isArray(data.scriptBlacklist)) currentConfig.scriptBlacklist = new Set(data.scriptBlacklist); if (Array.isArray(data.thirdPartyWhitelist)) currentConfig.thirdPartyWhitelist = data.thirdPartyWhitelist; if (Array.isArray(data.removedBuiltinKeywords)) currentConfig.removedBuiltinKeywords = new Set( data.removedBuiltinKeywords ); if (Array.isArray(data.whitelistDisplayNames)) { try { currentConfig.whitelistDisplayNames = new Map(data.whitelistDisplayNames); } catch (e) { currentConfig.whitelistDisplayNames = new Map(); } } Object.keys(DEFAULT_CONFIG_STATE).forEach((configKey) => { if (data[configKey] !== undefined) { currentConfig[configKey] = data[configKey]; } else if (currentConfig[configKey] === undefined) { currentConfig[configKey] = DEFAULT_CONFIG_STATE[configKey]; } }); } try { var glBlKw = GM_getValue('adblock_gl_blacklistKeywords', null); if (glBlKw !== null) currentConfig.redirectTitleBlacklistKeywords = JSON.parse(glBlKw); var glWlKw = GM_getValue('adblock_gl_whitelistKeywords', null); if (glWlKw !== null) currentConfig.redirectTitleWhitelistKeywords = JSON.parse(glWlKw); var glTp = GM_getValue('adblock_gl_trackingParams', null); if (glTp !== null) { currentConfig.trackingParamKeys = JSON.parse(glTp); rebuildTrackingParamRegex(currentConfig.trackingParamKeys); } } catch(e) { _debugCatch(e, "currentConfig.trackingPar"); } } catch (e) { console.warn( '[广告拦截器] 配置加载失败,已回退默认值:', e.message || e ); try { GM_deleteValue(key); } catch (e2) { } } }, saveConfig() { const hostname = location.hostname; const key = this.getConfigKey(hostname); const toStore = { modules: currentConfig.modules, cspRules: currentConfig.cspRules, whitelist: Array.from(currentConfig.whitelist), keywordWhitelist: Array.from(currentConfig.keywordWhitelist), scriptBlacklist: Array.from(currentConfig.scriptBlacklist), thirdPartyWhitelist: currentConfig.thirdPartyWhitelist, removedBuiltinKeywords: Array.from(currentConfig.removedBuiltinKeywords), whitelistDisplayNames: Array.from(currentConfig.whitelistDisplayNames.entries()), }; Object.keys(DEFAULT_CONFIG_STATE).forEach((k) => { toStore[k] = currentConfig[k]; }); GM_setValue(key, JSON.stringify(toStore)); }, resetAllSettings() { currentConfig.modules = { ...DEFAULT_MODULE_STATE }; currentConfig.cspRules = DEFAULT_CSP_RULES_TEMPLATE.map((rule) => ({ ...rule, })); currentConfig.whitelist.clear(); currentConfig.keywordWhitelist.clear(); currentConfig.scriptBlacklist.clear(); currentConfig.thirdPartyWhitelist = []; currentConfig.removedBuiltinKeywords.clear(); currentConfig.whitelistDisplayNames.clear(); currentConfig.redirectTitleBlacklistKeywords = [...DEFAULT_REDIRECT_TITLE_BLACKLIST]; currentConfig.redirectTitleWhitelistKeywords = [...DEFAULT_REDIRECT_TITLE_WHITELIST]; Object.keys(DEFAULT_CONFIG_STATE).forEach((k) => { currentConfig[k] = DEFAULT_CONFIG_STATE[k]; }); try { GM_deleteValue('adblock_gl_blacklistKeywords'); GM_deleteValue('adblock_gl_whitelistKeywords'); GM_deleteValue('adblock_gl_trackingParams'); } catch(e) { _debugCatch(e, "删除全局黑名单配置失败"); } currentConfig.trackingParamKeys = [...DEFAULT_TRACKING_PARAMS]; rebuildTrackingParamRegex(currentConfig.trackingParamKeys); this.saveConfig(); }, }; StorageManager.loadConfig(); // ======================== 菜单命令注册 ======================== // 【功能】向油猴菜单注册入口:设置面板 / 苹果设备模拟 / 清空白名单 / 重置设置。 // 【机制】面板懒加载——首次使用菜单命令时才初始化 UI;面板初始化失败会弹通知提示刷新重试。 // 【用户体验】菜单命令「1 ⚙️ 广告拦截设置面板」是打开面板的主入口;重置类命令都有二次确认弹窗。 let menuCommandsRegistered = false; try { registerAllMenuCommands(); } catch(e) { _debugCatch(e, "registerAllMenuCommands"); } // ======================== 页面滚动解锁守卫 ======================== // 【功能】对抗"广告锁滚动":广告脚本注入全屏浮层时顺手把 html/body 的 overflow 锁成 hidden, // 隐藏广告后页面仍无法滑动。本守卫统一解除锁定并恢复用户的滚动位置。 // 【机制】任何拦截路径隐藏广告后调用 arm()/armForElement(),在 15 秒窗口内持续检测并解除 // html/body 的 overflow 锁定(内联 !important 覆盖样式表规则,支持反复加锁对抗), // 并恢复一次用户滚动位置。 // 【注意】窗口期外或从未隐藏广告时绝不干预——不会误伤网站自身合法的滚动锁定(弹窗/抽屉等); // 隐藏"覆盖层形态"(fixed/absolute 或占视口一半以上)的元素才触发解锁,普通广告位不触发。 const ScrollLockGuard = { _initialized: false, _lastUserY: 0, _unlockUntil: 0, _unlockY: 0, _sawLockAtArm: false, _restored: false, _driverTimer: null, _prevBodyOverflow: null, _prevBodyOverflowY: null, _prevHtmlOverflow: null, _prevHtmlOverflowY: null, _lastLockCheckAt: 0, _lastLockResult: undefined, _onScroll: null, init: function () { if (this._initialized) return; this._initialized = true; var self = this; // 持续跟踪用户滚动位置: 广告锁定把页面顶回顶部后, 靠这里记录的位置恢复 this._onScroll = function () { try { self._lastUserY = _globals.scrollY || _globals.pageYOffset || 0; } catch (e) {} }; try { _globals.addEventListener('scroll', this._onScroll, { passive: true, capture: true }); } catch (e) { try { _document.addEventListener('scroll', this._onScroll, true); } catch (e2) {} } }, disable: function () { this._initialized = false; // 停掉窗口期自驱动定时器 if (this._driverTimer) { try { _clearTimeout(this._driverTimer); } catch(e) {} this._driverTimer = null; } if (this._onScroll) { try { _globals.removeEventListener('scroll', this._onScroll, { capture: true }); } catch (e) { try { _document.removeEventListener('scroll', this._onScroll, true); } catch (e2) {} } this._onScroll = null; } this._unlockUntil = 0; this._unlockY = 0; this._sawLockAtArm = false; this._restored = false; this._lastLockCheckAt = 0; this._lastLockResult = undefined; this._restoreInlineOverflow(); }, _saveInlineOverflow: function () { try { if (_document.body && this._prevBodyOverflow === null) { var bs = _document.body.style; this._prevBodyOverflow = bs.getPropertyValue('overflow'); this._prevBodyOverflowY = bs.getPropertyValue('overflow-y'); } } catch (e) {} try { if (_document.documentElement && this._prevHtmlOverflow === null) { var hs = _document.documentElement.style; this._prevHtmlOverflow = hs.getPropertyValue('overflow'); this._prevHtmlOverflowY = hs.getPropertyValue('overflow-y'); } } catch (e) {} }, _restoreInlineOverflow: function () { try { if (_document.body && this._prevBodyOverflow !== null) { var bs = _document.body.style; if (this._prevBodyOverflow === '') bs.removeProperty('overflow'); else bs.setProperty('overflow', this._prevBodyOverflow); if (this._prevBodyOverflowY === '') bs.removeProperty('overflow-y'); else bs.setProperty('overflow-y', this._prevBodyOverflowY); } } catch (e) {} try { if (_document.documentElement && this._prevHtmlOverflow !== null) { var hs = _document.documentElement.style; if (this._prevHtmlOverflow === '') hs.removeProperty('overflow'); else hs.setProperty('overflow', this._prevHtmlOverflow); if (this._prevHtmlOverflowY === '') hs.removeProperty('overflow-y'); else hs.setProperty('overflow-y', this._prevHtmlOverflowY); } } catch (e) {} this._prevBodyOverflow = null; this._prevBodyOverflowY = null; this._prevHtmlOverflow = null; this._prevHtmlOverflowY = null; }, _isScrollLocked: function () { try { var b = _document.body ? _getComputedStyle(_document.body) : null; var h = _document.documentElement ? _getComputedStyle(_document.documentElement) : null; if (b && (b.overflow === 'hidden' || b.overflowY === 'hidden')) return true; if (h && (h.overflow === 'hidden' || h.overflowY === 'hidden')) return true; } catch (e) {} return false; }, // 节流的锁定复查: 隐藏大量广告时每次隐藏都会触发一次锁定判定, // 每次都读两次 getComputedStyle 会连续触发回流; 300ms 内复用上次结果, // 复查由周期驱动(800ms)与后续隐藏兜底, 反复加锁的广告脚本仍会被持续解除 _isScrollLockedThrottled: function () { var _n = Date.now(); if (_n - this._lastLockCheckAt < 300) { return this._lastLockResult === undefined ? this._isScrollLocked() : this._lastLockResult; } this._lastLockCheckAt = _n; this._lastLockResult = this._isScrollLocked(); return this._lastLockResult; }, _restoreScrollPosition: function () { try { var dh = (_document.documentElement && _document.documentElement.scrollHeight) || (_document.body && _document.body.scrollHeight) || 0; var vh = _globals.innerHeight || (_document.documentElement && _document.documentElement.clientHeight) || 0; var y = _globals.scrollY || _globals.pageYOffset || 0; if (y <= 0 && this._unlockY > 0 && dh > vh + 50) { this._restored = true; _globals.scrollTo(0, this._unlockY); } } catch (e) {} }, // 隐藏广告后调用(所有拦截路径统一入口): 记录滚动位置并开启 15 秒解锁窗口 arm: function () { this.init(); this._unlockUntil = Date.now() + 15000; try { this._unlockY = _globals.scrollY || _globals.pageYOffset || this._lastUserY || 0; } catch (e) { this._unlockY = this._lastUserY || 0; } this._restored = false; this._sawLockAtArm = this._isScrollLockedThrottled(); this._saveInlineOverflow(); this.apply(); this._startDriver(); }, // 窗口期自驱动: 15 秒内每 800ms 复查一次滚动锁定(广告脚本反复加锁也能持续解除); // 窗口过期或守卫关闭后定时器自停 _startDriver: function () { var self = this; if (this._driverTimer) return; var tick = function () { if (!self._initialized || Date.now() > self._unlockUntil) { self._driverTimer = null; return; } try { self.apply(); } catch (e) {} self._driverTimer = _setTimeout(tick, 800); }; this._driverTimer = _setTimeout(tick, 800); }, // 带元素判定的统一入口: 仅当被隐藏元素确实是"覆盖层形态"(fixed/absolute 或占视口一半以上) // 才解锁滚动 —— 普通内容流里的广告位隐藏不会误触网站自身的滚动锁定 armForElement: function (el) { if (!el) return; var isOverlayLike = false; try { // 内联定位优先(高频路径, 广告元素普遍内联写死 position), 免去计算样式/回流 var _ip = (el.style && el.style.position) || ''; if (_ip === 'fixed' || _ip === 'absolute') { isOverlayLike = true; } else { var _cp = _getComputedStyle(el).position; isOverlayLike = (_cp === 'fixed' || _cp === 'absolute'); if (!isOverlayLike) { isOverlayLike = (el.offsetWidth >= (_globals.innerWidth || 1) * 0.5) || (el.offsetHeight >= (_globals.innerHeight || 1) * 0.5); } } } catch (e) { isOverlayLike = true; // 无法判定时保守触发, 由 apply 内部条件把关 } if (isOverlayLike) this.arm(); }, // 幂等解锁: 窗口期内检测到 overflow 锁定就强制解除(防广告脚本反复加锁), // 并恢复一次滚动位置; 窗口期外或未锁定则不做任何事 apply: function () { if (!this._initialized) return; if (Date.now() > this._unlockUntil) return; if (this._isScrollLockedThrottled()) { try { if (_document.body) { _document.body.style.setProperty('overflow', 'auto', 'important'); _document.body.style.setProperty('overflow-y', 'auto', 'important'); } } catch (e) {} try { if (_document.documentElement) { _document.documentElement.style.setProperty('overflow', 'auto', 'important'); _document.documentElement.style.setProperty('overflow-y', 'auto', 'important'); } } catch (e) {} this._restoreScrollPosition(); return; } // 未锁定但页面被顶回顶部(锁定已被他方移除/样式表未生效) → 窗口期内只恢复一次位置 if (!this._restored && this._sawLockAtArm) this._restoreScrollPosition(); }, }; // ======================== ScriptSyncGuard 中央同步脚本拦截器 ======================== // 【功能】拦截页面动态插入的