广告终结者
// ==UserScript==
// @name 广告终结者
// @namespace http://tampermonkey.net/
// @version 3.6
// @description 优化文本广告检测(仍存在bug,无法与第三方拦截同时开启)
// @author DeepSeek
// @match *://*/*
// @license MIT
// @grant GM_registerMenuCommand
// @grant GM_notification
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const HOSTNAME = location.hostname.replace(/\./g, '\\.');
const REGEX = {
dynamicId: /^(?:[0-9a-f]{16,}|[\dA-F-]{20,})$/i,
adAttributes: /(?:ad(?:s|vert|vertisement|box|frame|container|wrap|content|label)?|推广|广告|gg|sponsor|推荐|guanggao|syad|bfad|弹窗|悬浮|葡京|banner|pop(?:up|under)|track(?:ing)?)[_-]?/i,
thirdParty: new RegExp(`^(https?:\\/\\/(.*\\.)?${HOSTNAME}(\\/|$)|data:|about:blank)`, 'i'),
textAdKeywords: /限时(?:优惠|免费)|立即(?:下载|注册|咨询)|微信(?:号|客服)?[::]?|vx[::]\w+|telegram[::]\w+|免广告|偷拍|黑料|解锁(?:高清在线直播)|点击下载/
};
const FIRST_SCRIPT_DEFAULT_KEYWORDS = [
'.substr(10)', '.substr(22)', 'htmlAds', 'ads_codes', '{return void 0!==b[a]?b[a]:a}).join("")}', '-${scripts[randomIndex]}', '/image/202${scripts[Math.random()', '"https://"+Date.parse(new Date())+', '"https://"+(new Date().getDate())+', 'new Function(t)()',
'new Function(b)()', 'new Function(\'d\',e)', 'Math.floor(2147483648 * Math.random());', 'Math.floor(Math.random()*url.length)', '&&navigator[', '=navigator;', 'navigator.platform){setTimeout(function', 'disableDebugger', 'sojson.v', '<\\/\'+\'s\'+\'c\'+\'ri\'+\'pt\'+\'>\');', '</\'+\'s\'+\'c\'+\'ri\'+\'pt\'+\'>\');',
];
const FIRST_SCRIPT_REGEX = {
obfuscatedAds: {
randomPathScript: /<script\s+src\s*=\s*["']\/[A-Za-z0-9]{8,}\/[A-Za-z0-9]{8,}\.js["']/i,
dynamicCode: /(?:eval|(?:new\s+Function\s*\(\s*(["'])\s*)\1|set(?:Timeout|Interval|Immediate)\s*\(\s*function\s*\(\)\s*{)/i,
hexEscape: /\\x[0-9a-f]{2}(?:\\x[0-9a-f]{2}){5,}/gi,
unicodeEscape: /\\u[0-9a-f]{4}(?:\\u[0-9a-f]{4}){3,20}/gi,
arrayObfuscation: /\[(?:0x[0-9a-f]+|\d+),?(?:0x[0-9a-f]+|\d+)\]\.(?:join|reverse|map)/i,
mathObfuscation: /^Math\[\s*["']\w{5,}["']\s*\]/im,
base64Data: /(?:data:.*?(?:ad|banner)|(?:[A-Za-z0-9+/]{4}){10,}(?:ad|banner)|data:image\/(?:png|jpeg|gif);base64,|%[0-9A-Fa-f]{2,})/i
}
};
const DEFAULT_MODULES = {
main: false,
dynamicSystem: false,
layoutSystem: false,
frameSystem: false,
mediaSystem: false,
textSystem: false,
thirdPartyBlock: false,
obfuscatedAdSystem: false,
floating: false
};
const DEFAULT_CSP_RULES = [
{ id: 1, name: '阻止外部脚本加载', rule: "script-src 'self'", enabled: true },
{ id: 2, name: '阻止内联脚本执行', rule: "script-src 'unsafe-inline'", enabled: false },
{ id: 3, name: '阻止动态脚本执行', rule: "script-src 'unsafe-eval'", enabled: false },
{ id: 4, name: '阻止外部样式加载', rule: "style-src 'self'", enabled: false },
{ id: 5, name: '阻止内联样式执行', rule: "style-src 'unsafe-inline'", enabled: false },
{ id: 6, name: '阻止外部图片加载', rule: "img-src 'self'", enabled: true },
{ id: 7, name: '禁止所有框架加载', rule: "frame-src 'none'", enabled: false },
{ id: 8, name: '禁止媒体资源加载', rule: "media-src 'none'", enabled: false },
{ id: 9, name: '禁止对象嵌入', rule: "object-src 'none'", enabled: false }
];
const CONFIG = {
modules: { ...DEFAULT_MODULES },
protectionRules: {
dynamicIdLength: 30,
zIndexThreshold: 100,
maxFrameDepth: 4,
textAdKeywords: ['限时优惠', '立即下载', '微信', 'vx:', 'telegram', '偷拍', '黑料', '扫码关注', '点击下载']
},
csp: {
enabled: false,
rules: DEFAULT_CSP_RULES.map(rule => ({ ...rule }))
},
mobileOptimizations: {
lazyLoadImages: true,
removeImagePlaceholders: true
},
moduleNames: {
dynamicSystem: '动态检测系统',
layoutSystem: '布局检测系统',
frameSystem: '框架过滤系统',
mediaSystem: '媒体检测系统',
textSystem: '文本广告检测',
thirdPartyBlock: '第三方拦截',
obfuscatedAdSystem: '混淆广告检测',
floating: '浮动广告检测'
},
modulePriority: [
'thirdPartyBlock',
'frameSystem',
'obfuscatedAdSystem',
'dynamicSystem',
'mediaSystem',
'layoutSystem',
'textSystem',
'floating'
],
keywordConfig: {
useDefault: true,
custom: [],
blockThreshold: 2
},
performance: {
highRiskModules: ['thirdPartyBlock', 'frameSystem', 'obfuscatedAdSystem'],
visibleAreaPriority: true,
styleCacheTimeout: 600000,
mutationProcessLimit: 40,
mutationProcessTimeLimit: 10,
idleCallbackTimeout: 2500,
throttleScrollDelay: 200,
debounceMutationDelay: 300,
longTaskThreshold: 500,
degradeThreshold: 3
},
obfuscatedAdConfig: {
useDefault: true,
customKeywords: [],
customRegex: [],
blockThreshold: 2
}
};
const Logs = {
logs: [],
maxLogs: 30,
add(moduleKey, element, reason) {
const moduleName = CONFIG.moduleNames[moduleKey] || '未知模块';
this.logs.push({
module: moduleName,
element: `${element.tagName}#${element.id || ''}.${element.className || ''}`,
content: this.getContent(element),
reason: reason || {},
timestamp: new Date().toISOString()
});
if (this.logs.length > this.maxLogs) this.logs.shift();
},
getContent(element) {
return element.outerHTML.slice(0, 100) + (element.outerHTML.length > 100 ? '...' : '');
},
clear() {
this.logs = [];
GM_notification('日志已清空');
}
};
const perf = {
pendingTasks: [],
isProcessing: false,
processed: new WeakSet(),
adElements: new Set(),
styleCache: new Map(),
lastStyleCacheClear: Date.now(),
longTaskCount: 0,
degraded: false
};
const ThirdPartyInterceptor = {
originals: {
createElement: document.createElement,
setAttribute: Element.prototype.setAttribute,
appendChild: Node.prototype.appendChild,
fetch: window.fetch,
xhrOpen: XMLHttpRequest.prototype.open,
documentWrite: document.write,
documentWriteln: document.writeln,
insertAdjacentHTML: Element.prototype.insertAdjacentHTML
},
blockedUrls: new Set(),
enabled: false,
init() {
if (!CONFIG.modules.thirdPartyBlock) {
this.disable();
return;
}
this.enable();
},
enable() {
if (this.enabled) return;
this.enabled = true;
document.createElement = this.wrapElementCreation;
Element.prototype.setAttribute = this.wrapSetAttribute;
Node.prototype.appendChild = this.wrapAppendChild;
window.fetch = this.wrapFetch;
XMLHttpRequest.prototype.open = this.wrapXHROpen;
document.write = this.wrapDocumentWrite;
document.writeln = this.wrapDocumentWriteln;
Element.prototype.insertAdjacentHTML = this.wrapInsertAdjacentHTML;
document.addEventListener('beforescriptexecute', this.handleBeforeScriptExecute);
},
disable() {
if (!this.enabled) return;
this.enabled = false;
document.createElement = this.originals.createElement;
Element.prototype.setAttribute = this.originals.setAttribute;
Node.prototype.appendChild = this.originals.appendChild;
window.fetch = this.originals.fetch;
XMLHttpRequest.prototype.open = this.originals.xhrOpen;
document.write = this.originals.documentWrite;
document.writeln = this.originals.documentWriteln;
Element.prototype.insertAdjacentHTML = this.originals.insertAdjacentHTML;
document.removeEventListener('beforescriptexecute', this.handleBeforeScriptExecute);
},
wrapElementCreation(tagName) {
const element = ThirdPartyInterceptor.originals.createElement.call(document, tagName);
if (['script', 'iframe', 'img'].includes(tagName.toLowerCase())) {
return new Proxy(element, {
set(target, prop, value) {
if (prop === 'src' && ThirdPartyInterceptor.shouldBlock(target, value)) {
ThirdPartyInterceptor.blockedUrls.add(value);
Logs.add('thirdPartyBlock', target, {
type: '代理对象属性设置',
detail: value
});
return true;
}
target[prop] = value;
return true;
}
});
}
return element;
},
wrapSetAttribute: function(name, value) {
const _this = this;
if (name === 'src' && ThirdPartyInterceptor.shouldBlock(_this, value)) {
ThirdPartyInterceptor.blockedUrls.add(value);
Logs.add('thirdPartyBlock', _this, {
type: '第三方资源',
detail: value
});
return;
}
return ThirdPartyInterceptor.originals.setAttribute.call(_this, name, value);
}.bind(Element.prototype.setAttribute),
wrapAppendChild: function(node) {
const _this = this;
if (node.nodeType === 1 && ThirdPartyInterceptor.shouldBlock(node, node.src)) {
ThirdPartyInterceptor.blockedUrls.add(node.src);
Logs.add('thirdPartyBlock', node, {
type: '动态添加第三方元素',
detail: node.src
});
return null;
}
return ThirdPartyInterceptor.originals.appendChild.call(_this, node);
}.bind(Node.prototype.appendChild),
wrapFetch: function(input, init) {
const url = typeof input === 'string' ? input : (input && input.url) ? input.url : '';
if (ThirdPartyInterceptor.isThirdParty(url)) {
ThirdPartyInterceptor.blockedUrls.add(url);
Logs.add('thirdPartyBlock', {tagName: 'FETCH', id: '', className: ''}, {
type: '第三方资源',
detail: url
});
return Promise.reject(new Error('第三方资源请求被拦截'));
}
return ThirdPartyInterceptor.originals.fetch(input, init);
}.bind(window.fetch),
wrapXHROpen: function(method, url) {
const _this = this;
if (ThirdPartyInterceptor.isThirdParty(url)) {
_this._blocked = true;
ThirdPartyInterceptor.blockedUrls.add(url);
Logs.add('thirdPartyBlock', {tagName: 'XHR', id: '', className: ''}, {
type: '第三方资源',
detail: url
});
return;
}
return ThirdPartyInterceptor.originals.xhrOpen.apply(_this, arguments);
}.bind(XMLHttpRequest.prototype.open),
wrapDocumentWrite: function(content) {
return ThirdPartyInterceptor.handleHTMLInsertion(content, 'document.write', ThirdPartyInterceptor.originals.documentWrite.bind(document));
}.bind(document.write),
wrapDocumentWriteln: function(content) {
return ThirdPartyInterceptor.handleHTMLInsertion(content, 'document.writeln', ThirdPartyInterceptor.originals.documentWriteln.bind(document));
}.bind(document.writeln),
wrapInsertAdjacentHTML: function(position, html) {
return ThirdPartyInterceptor.handleHTMLInsertion(html, 'insertAdjacentHTML', ThirdPartyInterceptor.originals.insertAdjacentHTML.bind(this, position));
}.bind(Element.prototype.insertAdjacentHTML),
handleBeforeScriptExecute: function(e) {
if (!ThirdPartyInterceptor.enabled) return;
const script = e.target;
if (ThirdPartyInterceptor.isThirdParty(script.src)) {
e.preventDefault();
e.stopImmediatePropagation();
Logs.add('thirdPartyBlock', script, {
type: 'beforescriptexecute',
detail: script.src
});
AdUtils.safeRemove(script, 'thirdPartyBlock', {
type: '脚本执行前拦截',
detail: script.src
});
}
},
shouldBlock(element, url) {
if (!this.enabled) return false;
if (!url) return false;
const tagName = element.tagName?.toLowerCase();
const shouldCheck = ['script', 'iframe', 'img'].includes(tagName);
return shouldCheck && this.isThirdParty(url);
},
isThirdParty(url) {
try {
if (!url || url.startsWith('data:') || url.startsWith('blob:')) return false;
const currentHost = new URL(location.href).hostname;
const resourceHost = new URL(url, location.href).hostname;
return !resourceHost.endsWith('.' + currentHost) && resourceHost !== currentHost;
} catch (e) {
return true;
}
},
handleHTMLInsertion(html, insertionType, originalFunction) {
if (ThirdPartyInterceptor.enabled) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
const elements = tempDiv.querySelectorAll('script, iframe, img');
let modifiedHtml = html;
elements.forEach(el => {
const src = el.src || el.getAttribute('src') || '';
if (ThirdPartyInterceptor.isThirdParty(src)) {
modifiedHtml = modifiedHtml.replace(el.outerHTML, '');
Logs.add('thirdPartyBlock', el, {
type: insertionType,
detail: src
});
}
});
html = modifiedHtml;
}
return originalFunction(html);
}
};
const AdUtils = {
safeRemove(element, moduleKey, reason) {
if (!element?.parentNode) return false;
try {
Logs.add(moduleKey, element, reason);
element.style.display = 'none';
element.style.visibility = 'hidden';
element.style.opacity = '0';
element.setAttribute('data-ad-removed', 'true');
this.removeEventListeners(element);
return true;
} catch (e) {
console.warn('元素隐藏失败:', e);
return false;
}
},
removeEventListeners(element) {
element.removeAttribute('onload');
element.removeAttribute('onerror');
}
};
const StyleManager = {
styleElement: null,
inject() {
if (this.styleElement) return;
this.styleElement = document.createElement('style');
this.styleElement.id = 'ad-blocker-styles';
let cssRules = [];
cssRules.push(`
[data-ad-removed] {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
position: absolute !important;
z-index: -9999 !important;
width: 0 !important;
height: 0 !important;
}
iframe[src*="ad"], .ad-container {
display: none !important;
height: 0 !important;
width: 0 !important;
opacity: 0 !important;
}
`);
this.styleElement.textContent = cssRules.join('\n');
document.head.appendChild(this.styleElement);
},
remove() {
if (this.styleElement?.parentNode) {
this.styleElement.parentNode.removeChild(this.styleElement);
this.styleElement = null;
}
},
toggle(enable) {
enable ? this.inject() : this.remove();
}
};
const CSPManager = {
currentPolicy: null,
generatePolicy() {
return CONFIG.csp.rules
.filter(rule => rule.enabled)
.map(rule => rule.rule)
.join('; ');
},
inject() {
this.remove();
if (!CONFIG.csp.enabled) return;
const policy = this.generatePolicy();
this.currentPolicy = policy;
const meta = document.createElement('meta');
meta.httpEquiv = "Content-Security-Policy";
meta.content = policy;
document.head.appendChild(meta);
},
remove() {
const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
if (meta) meta.remove();
},
toggleRule(ruleId, enable) {
const rule = CONFIG.csp.rules.find(r => r.id === ruleId);
if (rule) rule.enabled = enable;
},
injectInitialCSP() {
if (!CONFIG.csp.enabled) return;
const initialRules = CONFIG.csp.rules
.filter(rule => [1, 6].includes(rule.id) && rule.enabled)
.map(rule => rule.rule)
.join('; ');
if (initialRules) {
const meta = document.createElement('meta');
meta.httpEquiv = "Content-Security-Policy";
meta.content = initialRules;
document.head.appendChild(meta);
}
}
};
const Detector = {
checkModule(moduleKey, element) {
if (!CONFIG.modules.main || !CONFIG.modules[moduleKey]) return false;
return this[`check${moduleKey.charAt(0).toUpperCase() + moduleKey.slice(1)}`](element);
},
checkThirdPartyBlock(el) {
if (!CONFIG.modules.thirdPartyBlock) return false;
if (el.tagName === 'SCRIPT') {
const src = el.src || el.getAttribute('data-src') || '';
if (!REGEX.thirdParty.test(src)) {
FirstScriptAdUtils.safeRemove(el, {
type: '第三方资源拦截',
detail: `源: ${src.slice(0, 80)}`,
moduleKey: 'thirdPartyBlock'
});
return true;
}
} else if (el.tagName === 'IFRAME') {
const src = el.src || el.getAttribute('data-src') || '';
if (!REGEX.thirdParty.test(src)) {
return AdUtils.safeRemove(el, 'thirdPartyBlock', {
type: '第三方资源拦截',
detail: `源: ${src.slice(0, 80)}`
});
}
}
return false;
},
checkFrameSystem(el) {
if (!CONFIG.modules.frameSystem) return false;
if (el.tagName === 'IFRAME') {
let depth = 0, parent = el;
while ((parent = parent.parentElement) && depth <= CONFIG.protectionRules.maxFrameDepth) {
if (parent.tagName === 'IFRAME') depth++;
}
if (depth > CONFIG.protectionRules.maxFrameDepth) {
return AdUtils.safeRemove(el, 'frameSystem', {
type: '深层嵌套框架',
detail: `嵌套层级: ${depth}`
});
}
}
return false;
},
checkDynamicSystem(el) {
if (!CONFIG.modules.dynamicSystem) return false;
const attrs = el.getAttributeNames().map(name =>
`${name}=${el.getAttribute(name)}`
).join(' ');
if (REGEX.adAttributes.test(attrs)) {
return AdUtils.safeRemove(el, 'dynamicSystem', {
type: '广告属性检测',
detail: `属性: ${attrs.slice(0, 100)}`
});
}
if (el.id && (
el.id.length > CONFIG.protectionRules.dynamicIdLength ||
REGEX.dynamicId.test(el.id)
)) {
return AdUtils.safeRemove(el, 'dynamicSystem', {
type: '动态ID检测',
detail: `异常ID: ${el.id}`
});
}
return false;
},
checkMediaSystem(el) {
if (!CONFIG.modules.mediaSystem) return false;
if (el.tagName === 'IMG' && (el.src.includes('ad') || el.src.match(/\.(gif|webp)(\?|$)/))) {
return AdUtils.safeRemove(el, 'mediaSystem', {
type: '图片广告',
detail: `图片源: ${el.src.slice(0, 50)}`
});
}
const style = this.getCachedStyle(el);
const rect = el.getBoundingClientRect();
if (['fixed', 'sticky'].includes(style.position)) {
const isTopBanner = rect.top < 50;
const isBottomBanner = rect.bottom > window.innerHeight - 50;
if (isTopBanner || isBottomBanner) {
return AdUtils.safeRemove(el, 'mediaSystem', {
type: '浮动广告',
detail: `位置: ${rect.top}px`
});
}
}
return false;
},
checkLayoutSystem(el) {
if (!CONFIG.modules.layoutSystem) return false;
const style = this.getCachedStyle(el);
const zIndex = parseInt(style.zIndex, 10);
if (!isNaN(zIndex) && zIndex > CONFIG.protectionRules.zIndexThreshold) {
return AdUtils.safeRemove(el, 'layoutSystem', {
type: '高Z轴元素',
detail: `z-index: ${zIndex}`
});
}
if (style.overflow === 'hidden' && el.scrollHeight > el.clientHeight) {
return AdUtils.safeRemove(el, 'layoutSystem', {
type: '隐藏溢出内容的容器'
});
}
if (style.visibility === 'hidden' || style.display === 'none') {
return AdUtils.safeRemove(el, 'layoutSystem', {
type: '不可见容器'
});
}
if (el.children.length === 0 && el.textContent.trim() === '' && style.width === '0px' && style.height === '0px') {
return AdUtils.safeRemove(el, 'layoutSystem', {
type: '空的不可见容器'
});
}
if (el.children.length > 0 && Array.from(el.children).every(child => child.style.display === 'none' || child.style.visibility === 'hidden')) {
return AdUtils.safeRemove(el, 'layoutSystem', {
type: '子元素的不可见容器'
});
}
return false;
},
checkTextSystem(el) {
if (!CONFIG.modules.textSystem) return false;
const text = el.textContent?.toLowerCase() || '';
const match = text.match(REGEX.textAdKeywords);
if (match) {
const span = document.createElement('span');
span.style.display = 'none';
span.setAttribute('data-ad-removed', 'true');
const keyword = match[0];
const textNodes = Array.from(el.childNodes).filter(node => node.nodeType === Node.TEXT_NODE && node.textContent.toLowerCase().includes(keyword));
textNodes.forEach(textNode => {
const originalText = textNode.textContent;
let newHTML = originalText.replace(new RegExp(keyword, 'gi'), `<span style="display:none;" data-ad-removed="true">${keyword}</span>`);
const tempDiv = document.createElement('div');
tempDiv.innerHTML = newHTML;
Array.from(tempDiv.childNodes).forEach(newNode => {
el.insertBefore(newNode, textNode);
});
el.removeChild(textNode);
});
Logs.add('textSystem', el, {
type: '文本广告',
detail: `触发规则: ${match}`,
keywords: [match]
});
}
return false;
},
checkFloating(el) {
if (!CONFIG.modules.floating) return false;
const style = this.getCachedStyle(el);
if (['fixed', 'sticky'].includes(style.position)) {
return AdUtils.safeRemove(el, 'floating', {
type: '浮动元素',
detail: `定位方式: ${style.position}`
});
}
return false;
},
getCachedStyle(el) {
if (Date.now() - perf.lastStyleCacheClear > CONFIG.performance.styleCacheTimeout) {
perf.styleCache.clear();
perf.lastStyleCacheClear = Date.now();
}
const key = el.nodeName + Array.from(el.classList).join('');
if (!perf.styleCache.has(key)) {
try {
perf.styleCache.set(key, getComputedStyle(el));
} catch (e) {
console.warn("Error getting computed style:", e);
return {};
}
}
return perf.styleCache.get(key);
},
isVisible(el) {
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
};
const Processor = {
collectAds() {
const elements = [...document.getElementsByTagName('*')];
perf.adElements.clear();
elements.forEach(element => {
if (perf.processed.has(element)) return;
let modulePriority = CONFIG.modulePriority;
if (CONFIG.performance.visibleAreaPriority && Detector.isVisible(element)) {
modulePriority = [...CONFIG.performance.highRiskModules, ...modulePriority.filter(m => !CONFIG.performance.highRiskModules.includes(m))];
}
for (const moduleKey of modulePriority) {
if (moduleKey === 'obfuscatedAdSystem') continue;
if (Detector.checkModule(moduleKey, element)) {
perf.adElements.add(element);
perf.processed.add(element);
break;
}
}
});
},
processElements() {
if (perf.degraded) return;
let startTime = performance.now();
let elementCount = 0;
for (let element of perf.adElements) {
if (elementCount >= CONFIG.performance.mutationProcessLimit ||
performance.now() - startTime > CONFIG.performance.mutationProcessTimeLimit) {
perf.pendingTasks.push(() => Processor.processElements());
break;
}
if (perf.processed.has(element)) continue;
let modulePriority = CONFIG.modulePriority;
if (CONFIG.performance.visibleAreaPriority && Detector.isVisible(element)) {
modulePriority = [...CONFIG.performance.highRiskModules, ...modulePriority.filter(m => !CONFIG.performance.highRiskModules.includes(m))];
}
for (const moduleKey of modulePriority) {
if (Detector.checkModule(moduleKey, element)) {
perf.processed.add(element);
break;
}
}
elementCount++;
}
perf.isProcessing = false;
if (perf.pendingTasks.length > 0) {
scheduleProcess();
}
}
};
const observer = new MutationObserver(mutations => {
let startTime = performance.now();
let mutationCount = 0;
for (let mutation of mutations) {
if (mutationCount >= CONFIG.performance.mutationProcessLimit ||
performance.now() - startTime > CONFIG.performance.mutationProcessTimeLimit) {
debouncedScheduleProcess();
return;
}
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) {
if (!perf.processed.has(node)) {
perf.adElements.add(node);
}
}
});
} else if (mutation.type === 'attributes') {
if (!perf.processed.has(mutation.target)) {
perf.adElements.add(mutation.target);
}
}
mutationCount++;
}
scheduleProcess();
});
let scheduledProcess = null;
const debouncedScheduleProcess = debounce(scheduleProcess, CONFIG.performance.debounceMutationDelay);
const FirstScriptAdUtils = {
safeRemove(element, reason) {
if (!element?.parentNode || element.dataset.firstScriptAdRemoved) return false;
try {
const moduleKey = reason.moduleKey || 'obfuscatedAdSystem';
Logs.add(moduleKey, element, reason);
element.parentNode.removeChild(element);
element.dataset.firstScriptAdRemoved = true;
return true;
} catch (e) {
console.warn('元素移除失败:', e);
return false;
}
},
checkKeywords(content) {
const keywords = [
...(CONFIG.obfuscatedAdConfig.useDefault ? FIRST_SCRIPT_DEFAULT_KEYWORDS : []),
...CONFIG.obfuscatedAdConfig.customKeywords
];
return keywords.filter(k => content.includes(k));
},
checkRegex(content) {
const regexList = [
...(CONFIG.obfuscatedAdConfig.useDefault ? Object.values(FIRST_SCRIPT_REGEX.obfuscatedAds) : []),
...CONFIG.obfuscatedAdConfig.customRegex.map(r => new RegExp(r.pattern, r.flags))
];
return regexList.filter(r => r.test(content));
}
};
const FirstScriptObfuscatedAdDetector = {
observer: null,
init() {
this.observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) this.processElement(node);
});
});
});
this.observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['id', 'class', 'style', 'src']
});
this.scanExistingElements();
},
scanExistingElements() {
document.querySelectorAll('script, iframe, div, span, img').forEach(el => {
this.processElement(el);
});
},
processElement(element) {
if (element.dataset.firstScriptProcessed) return;
element.dataset.firstScriptProcessed = true;
const content = element.innerHTML || element.src || '';
const matchedKeywords = FirstScriptAdUtils.checkKeywords(content);
const matchedRegex = FirstScriptAdUtils.checkRegex(content);
if (matchedKeywords.length + matchedRegex.length >= CONFIG.obfuscatedAdConfig.blockThreshold) {
FirstScriptAdUtils.safeRemove(element, {
type: '规则匹配',
keywords: matchedKeywords,
regex: matchedRegex.map(r => r.toString())
});
}
}
};
const UIController = {
init() {
this.loadConfig();
this.registerMainSwitch();
this.registerModuleSwitches();
this.registerLogCommands();
this.registerCSPCommands();
this.registerResetCommand();
StyleManager.toggle(CONFIG.modules.main);
},
registerMainSwitch() {
GM_registerMenuCommand(
`🔘 主开关 [${CONFIG.modules.main ? '✅' : '❌'}]`,
() => this.toggleMain()
);
},
registerModuleSwitches() {
Object.entries(CONFIG.moduleNames).forEach(([key, name]) => {
GM_registerMenuCommand(
`${name} [${CONFIG.modules[key] ? '✅' : '❌'}]`,
() => this.toggleModule(key, name)
);
});
},
registerLogCommands() {
GM_registerMenuCommand('📜 查看拦截日志', () => this.showLogs());
GM_registerMenuCommand('🧹 清空日志', () => Logs.clear());
},
registerCSPCommands() {
GM_registerMenuCommand('🛡️ CSP策略管理', () => this.manageCSP());
},
registerResetCommand() {
GM_registerMenuCommand('🔄 重置设置', () => this.resetSettings());
},
toggleMain() {
const newState = !CONFIG.modules.main;
Object.keys(CONFIG.modules).forEach(key => {
if (key !== 'main') CONFIG.modules[key] = newState;
});
CONFIG.modules.main = newState;
this.saveConfig();
StyleManager.toggle(newState);
GM_notification(`主开关已${newState ? '启用' : '禁用'}`, `所有模块${newState ? '✅ 已激活' : '❌ 已停用'}`);
location.reload();
},
toggleModule(key, name) {
CONFIG.modules[key] = !CONFIG.modules[key];
CONFIG.modules.main = Object.values(CONFIG.modules)
.slice(1).some(v => v);
this.saveConfig();
StyleManager.toggle(CONFIG.modules.main);
GM_notification(`${name} ${CONFIG.modules[key] ? '✅ 已启用' : '❌ 已禁用'}`);
location.reload();
},
showLogs() {
const logText = Logs.logs.map(log => {
const parts = [];
if (log.reason.type) parts.push(`类型: ${log.reason.type}`);
if (log.reason.detail) parts.push(`详细: ${log.reason.detail}`);
if (log.reason.keywords) parts.push(`关键词: ${log.reason.keywords.join(', ')}`);
if (log.reason.regex) parts.push(`正则式: ${log.reason.regex.join(', ')}`);
return `${log.module}\n元素: ${log.element}\n内容: ${log.content}\n${parts.join('\n')}`;
}).join('\n\n');
alert(`广告拦截日志(最近${Logs.maxLogs}条):\n\n${logText || '暂无日志'}`);
},
manageCSP() {
const rulesDisplay = CONFIG.csp.rules
.map(r => `${r.id}. ${r.name} (${r.enabled ? '✅' : '❌'})`)
.join('\n');
let input = prompt(
`当前CSP策略状态: ${CONFIG.csp.enabled ? '✅ 已启用' : '❌ 已禁用'}\n\n` +
"当前策略规则:\n" + rulesDisplay + "\n\n" +
"输入选项:\n1. 开启策略 (输入 'enable')\n2. 关闭策略 (输入 'disable')\n" +
"修改规则示例:输入 '1on' 启用规则1\n输入 '23off' 禁用规则2和规则3\n\n"+
"请输入你的选择:",
""
);
if (input === null) return;
input = input.trim().toLowerCase();
switch (input) {
case 'enable':
CONFIG.csp.enabled = true;
this.saveConfig();
CSPManager.inject();
alert("CSP策略已启用");
location.reload();
break;
case 'disable':
CONFIG.csp.enabled = false;
this.saveConfig();
CSPManager.remove();
alert("CSP策略已禁用");
location.reload();
break;
default:
if (/^\d+(?:on|off)$/i.test(input)) {
this.modifyCSPRules(input);
} else {
alert("无效输入");
}
}
},
modifyCSPRules(actionInput) {
const matches = actionInput.match(/^(\d+)(on|off)$/i);
if (matches) {
const ruleIds = matches[1].split('').map(Number);
const enable = matches[2].toLowerCase() === 'on';
let updatedRules = [];
ruleIds.forEach(id => {
const rule = CONFIG.csp.rules.find(r => r.id === id);
if (rule) {
rule.enabled = enable;
updatedRules.push(id);
}
});
this.saveConfig();
CSPManager.inject();
alert(`规则 ${updatedRules.join(',')} 已更新为 ${enable ? '启用' : '禁用'}`);
location.reload();
} else {
alert("输入格式错误,请按示例输入如'1on'或'123off'");
}
},
resetSettings() {
if (confirm("确定要重置所有设置吗?")) {
Object.assign(CONFIG.modules, DEFAULT_MODULES);
CONFIG.csp.enabled = false;
CONFIG.csp.rules = DEFAULT_CSP_RULES.map(rule => ({ ...rule }));
this.saveConfig();
CSPManager.remove();
alert("设置已重置为默认值");
location.reload();
}
},
saveConfig() {
GM_setValue(`adblockConfig_${location.hostname}`, JSON.stringify({
modules: CONFIG.modules,
csp: CONFIG.csp,
obfuscatedAdConfig: {
...CONFIG.obfuscatedAdConfig,
customRegex: CONFIG.obfuscatedAdConfig.customRegex.map(r => ({
pattern: r.pattern,
flags: r.flags
}))
}
}));
},
loadConfig() {
try {
const savedConfig = JSON.parse(GM_getValue(`adblockConfig_${location.hostname}`, '{}'));
if (savedConfig.modules) {
Object.keys(CONFIG.modules).forEach(key => {
if (savedConfig.modules.hasOwnProperty(key)) {
CONFIG.modules[key] = savedConfig.modules[key];
}
});
}
if (savedConfig.csp) {
Object.assign(CONFIG.csp, savedConfig.csp);
}
if (savedConfig.obfuscatedAdConfig) {
CONFIG.obfuscatedAdConfig.useDefault = savedConfig.obfuscatedAdConfig.useDefault;
CONFIG.obfuscatedAdConfig.customKeywords = savedConfig.obfuscatedAdConfig.customKeywords;
CONFIG.obfuscatedAdConfig.customRegex = savedConfig.obfuscatedAdConfig.customRegex.map(r => new RegExp(r.pattern, r.flags));
CONFIG.obfuscatedAdConfig.blockThreshold = savedConfig.obfuscatedAdConfig.blockThreshold;
}
} catch (e) {
console.error("加载配置失败:", e);
}
}
};
function init() {
UIController.init();
CSPManager.injectInitialCSP();
ThirdPartyInterceptor.init();
if (CONFIG.modules.obfuscatedAdSystem) {
FirstScriptObfuscatedAdDetector.init();
}
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['id', 'class', 'style', 'src']
});
Processor.collectAds();
scheduleProcess();
window.addEventListener('scroll', throttle(() => {
Processor.collectAds();
scheduleProcess();
}, CONFIG.performance.throttleScrollDelay));
window.addEventListener('resize', debounce(() => {
Processor.collectAds();
scheduleProcess();
}, 150));
if (CONFIG.mobileOptimizations.lazyLoadImages) {
document.addEventListener('DOMContentLoaded', () => {
const images = document.querySelectorAll('img[data-src]');
images.forEach(img => {
img.src = img.dataset.src;
img.removeAttribute('data-src');
});
});
}
if (CONFIG.mobileOptimizations.removeImagePlaceholders) {
document.addEventListener('DOMContentLoaded', () => {
const placeholders = document.querySelectorAll('.image-placeholder');
placeholders.forEach(placeholder => {
placeholder.parentNode.removeChild(placeholder);
});
});
}
}
function throttle(fn, delay) {
let last = 0, timer = null;
return function(...args) {
const now = Date.now();
if (now - last >= delay) {
fn.apply(this, args);
last = now;
} else {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
last = now;
}, delay - (now - last));
}
};
}
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
try {
fn.apply(this, args);
} catch(e) {
Logs.add('error', this, {
type: 'debounceError',
detail: e.message
});
}
}, delay);
};
}
function isElementInViewport(el) {
if (!el || !el.getBoundingClientRect) return false;
const rect = el.getBoundingClientRect();
return (
rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.left <= (window.innerWidth || document.documentElement.clientWidth) &&
rect.bottom >= 0 &&
rect.right >= 0
);
}
function scheduleProcess() {
const tasks = [];
let isScheduled = false;
function runTasks(deadline) {
while (tasks.length && (deadline.timeRemaining() > 2 || deadline.didTimeout)) {
const task = tasks.shift();
try {
task();
} catch(e) {
console.error('Task error:', e);
perf.record('longTask', performance.now());
}
}
if (tasks.length) {
requestIdleCallback(runTasks, { timeout: CONFIG.performance.idleCallbackTimeout });
} else {
isScheduled = false;
}
}
return function(task) {
tasks.push(task);
if (!isScheduled) {
isScheduled = true;
if (typeof requestIdleCallback === 'function') {
requestIdleCallback(runTasks, { timeout: CONFIG.performance.idleCallbackTimeout });
} else {
setTimeout(runTasks.bind(null, {
timeRemaining: () => 50,
didTimeout: true
}), 50);
}
}
};
}
init();
})();