// ==UserScript== // @name 手动广告屏蔽器 // @namespace http://tampermonkey.net/ // @version 13.0 // @description 点击隐藏 + 位置记忆 + 网站备注 + 相似检测 + 跨域拦截(三档 + 黑白名单,全入口覆盖) + 广告类型 + 自动识别 // @author You // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant unsafeWindow // @run-at document-start // ==/UserScript== (function() { 'use strict'; var STORAGE_KEY = 'ab_mask_v2'; var REMARK_KEY = 'ab_host_remarks_v1'; var HIDE_MODE_KEY = 'ab_hide_mode_v1'; var AUTO_DETECT_KEY = 'ab_auto_detect_v1'; var JUMP_MODE_KEY = 'ab_jump_mode_v1'; var JUMP_BLACKLIST_KEY = 'ab_jump_blacklist_v1'; var JUMP_WHITELIST_KEY = 'ab_jump_whitelist_v1'; var HOST = location.hostname; var MAX_Z = '2147483647'; var SIMILAR_THRESHOLD = 0.6; var SIMILAR_MIN_GROUPS = 1; var MAX_GROUP_CHILDREN = 30; var DOMAIN_SUFFIXES = [ 'com.cn','net.cn','org.cn','gov.cn','edu.cn','co.uk','org.uk','me.uk','ac.uk', 'co.jp','ne.jp','or.jp','com.au','net.au','org.au','com.hk','org.hk','com.tw','org.tw', 'com','net','org','cn','cc','tv','vip','io','co','xyz','top','info','biz','me','pw', 'live','online','site','club','app','dev','shop','store','work','fun','wang','ren', 'la','link','pro','win','space','website' ]; function showToast(msg) { if (!document.body) return; var t = document.createElement('div'); t.id = '__ab_toast'; t.textContent = msg; t.style.cssText = 'position:fixed;top:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.88);color:#fff;padding:10px 20px;border-radius:20px;font-size:13px;font-family:system-ui;z-index:2147483646;pointer-events:none;max-width:88vw;text-align:center;'; document.body.appendChild(t); setTimeout(function() { if (t.parentNode) t.parentNode.removeChild(t); }, 2600); } function cssEscape(s) { if (typeof CSS !== 'undefined' && CSS.escape) return CSS.escape(s); return String(s).replace(/[^a-zA-Z0-9_-]/g, function(ch) { return '\\' + ch; }); } function getClassName(el) { if (!el || !el.className) return ''; if (typeof el.className === 'string') return el.className; if (el.className.baseVal !== undefined) return el.className.baseVal; return ''; } function generateSelector(el) { if (!el || el.nodeType !== 1) return ''; if (el.id && el.id.indexOf('__ab_') !== 0) { try { if (document.querySelectorAll('#' + cssEscape(el.id)).length === 1) return '#' + cssEscape(el.id); } catch(e) {} } var tag = el.tagName.toLowerCase(); var classes = getClassName(el).split(/\s+/).filter(function(c) { return c && c.indexOf('__ab_') !== 0 && c.length < 40 && !/^\d/.test(c) && !/^[a-f0-9]{8,}$/i.test(c); }); if (classes.length > 0) { var full = tag + '.' + classes.map(cssEscape).join('.'); try { var c1 = document.querySelectorAll(full).length; if (c1 >= 1 && c1 <= 30) return full; } catch(e) {} for (var i = 0; i < classes.length; i++) { var s = tag + '.' + cssEscape(classes[i]); try { var c2 = document.querySelectorAll(s).length; if (c2 >= 1 && c2 <= 30) return s; } catch(e) {} } } var parts = [], node = el, depth = 0; while (node && node !== document.documentElement && depth < 8) { var part = node.tagName.toLowerCase(); if (node.id && node.id.indexOf('__ab_') !== 0) { parts.unshift('#' + cssEscape(node.id)); break; } var p = node.parentElement; if (p) { var sibs = Array.prototype.filter.call(p.children, function(c) { return c.tagName === node.tagName; }); if (sibs.length > 1) part += ':nth-of-type(' + (sibs.indexOf(node) + 1) + ')'; } parts.unshift(part); node = p; depth++; } return parts.join(' > '); } function isOurElement(el) { if (!el || el.nodeType !== 1) return false; if (el.id === '__ab_mask_doc_container' || el.id === '__ab_mask_fixed_container') return true; if (el.id && el.id.indexOf('__ab_') === 0) return true; if (el.closest && el.closest('#__ab_mask_doc_container, #__ab_mask_fixed_container')) return true; if (el.closest && el.closest('[id^="__ab_"]')) return true; return false; } function genId() { return 'r_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); } function genGroupId() { return 'g_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); } function genPosId() { return 'p_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); } function getDocSize() { var de = document.documentElement, b = document.body; return { w: Math.max(de ? de.scrollWidth : 0, b ? b.scrollWidth : 0, window.innerWidth), h: Math.max(de ? de.scrollHeight : 0, b ? b.scrollHeight : 0, window.innerHeight) }; } function getScroll() { return { x: window.scrollX || window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0, y: window.scrollY || window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0 }; } function getDepth(el) { var d = 0; while (el && el !== document.body && el !== document.documentElement) { d++; el = el.parentElement; } return d; } // ==================== 域名列表(黑白名单共用) ==================== var DomainList = { _cache: { black: null, white: null }, _keyOf: function(type) { return type === 'white' ? JUMP_WHITELIST_KEY : JUMP_BLACKLIST_KEY; }, load: function(type) { if (this._cache[type]) return this._cache[type]; var key = this._keyOf(type); try { this._cache[type] = JSON.parse(GM_getValue(key, '[]')); } catch(e) { this._cache[type] = []; } if (!Array.isArray(this._cache[type])) this._cache[type] = []; return this._cache[type]; }, save: function(type) { var key = this._keyOf(type); try { GM_setValue(key, JSON.stringify(this._cache[type])); } catch(e) {} }, stripSuffix: function(input) { var v = String(input || '').trim().toLowerCase(); v = v.replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/[\/?#].*$/, ''); var sorted = DOMAIN_SUFFIXES.slice().sort(function(a, b) { return b.length - a.length; }); for (var i = 0; i < sorted.length; i++) { var suf = '.' + sorted[i]; if (v.length > suf.length && v.slice(-suf.length) === suf) return v.slice(0, -suf.length); } return v; }, addMany: function(type, input) { var parts = String(input || '').split(/[,,\s\n\t]+/); var list = this.load(type); var added = 0; for (var i = 0; i < parts.length; i++) { var key = this.stripSuffix(parts[i]); if (!key) continue; if (list.indexOf(key) !== -1) continue; list.push(key); added++; } if (added > 0) this.save(type); return added; }, remove: function(type, key) { var list = this.load(type); var idx = list.indexOf(key); if (idx === -1) return false; list.splice(idx, 1); this.save(type); return true; }, clear: function(type) { this._cache[type] = []; this.save(type); }, hit: function(type, hostname) { if (!hostname) return null; var h = String(hostname).toLowerCase(); var list = this.load(type); for (var i = 0; i < list.length; i++) { if (list[i] && h.indexOf(list[i]) !== -1) return list[i]; } return null; } }; // ==================== 跨域跳转拦截(全入口覆盖) ==================== var JumpBlocker = { mode: 'off', _installed: false, _W: null, _D: null, _bypass: null, load: function() { try { var m = GM_getValue(JUMP_MODE_KEY, 'off'); if (m === 'off' || m === 'ask' || m === 'block') this.mode = m; else this.mode = 'off'; } catch(e) { this.mode = 'off'; } }, save: function() { try { GM_setValue(JUMP_MODE_KEY, this.mode); } catch(e) {} }, cycle: function() { var next = this.mode === 'off' ? 'ask' : (this.mode === 'ask' ? 'block' : 'off'); this.mode = next; this.save(); registerMenu(); var labels = { off: '跨域跳转拦截已关闭(不拦任何跨域跳转)', ask: '跨域跳转拦截:询问模式(拦下后弹提示条让你选)', block: '跨域跳转拦截:全拦模式(所有跨域跳转直接拦下)' }; showToast(labels[next]); }, getLabel: function() { if (this.mode === 'ask') return '询问'; if (this.mode === 'block') return '全拦'; return '关闭'; }, isCrossOrigin: function(url) { try { if (!url || typeof url !== 'string') return false; if (/^(data|about|blob|#|javascript|mailto|tel|sms|weixin|alipay|intent|market|taobao|tbopen|itms-apps|itms-services|chrome|chrome-extension|moz-extension):/i.test(url)) return false; var u = new URL(url, location.href); if (u.protocol !== 'http:' && u.protocol !== 'https:') return false; return u.host !== location.host; } catch(e) { return false; } }, shouldBlock: function(url) { if (!url || typeof url !== 'string') return false; if (this._bypass) { var b = this._bypass; this._bypass = null; if (String(url) === b) return false; } if (this.mode === 'off') return false; var isCross = false; try { isCross = this.isCrossOrigin(url); } catch(e) {} if (!isCross) return false; var hostname = ''; try { hostname = new URL(url, location.href).hostname; } catch(e) { return true; } var whiteHit = false; try { whiteHit = !!DomainList.hit('white', hostname); } catch(e) {} if (whiteHit) return false; var blackHit = false; try { blackHit = !!DomainList.hit('black', hostname); } catch(e) {} if (blackHit) return true; if (this.mode === 'block') return true; if (this.mode === 'ask') { try { this._showAskBar(url, hostname); } catch(e) {} return true; } return false; }, // 带 try/catch 的封装:抛错时按 mode 兜底 _checkBlock: function(url) { try { return this.shouldBlock(url); } catch(e) { return this.mode !== 'off'; } }, _doNavigate: function(url) { this._bypass = String(url); try { location.href = url; } catch(e) {} }, _askBar: null, _askTimer: null, _showAskBar: function(url, hostname) { var self = this; if (this._askBar) { if (this._askBar.parentNode) this._askBar.parentNode.removeChild(this._askBar); this._askBar = null; } if (this._askTimer) { clearTimeout(this._askTimer); this._askTimer = null; } if (!document.body) return; var bar = document.createElement('div'); bar.id = '__ab_ask_bar'; bar.style.cssText = 'position:fixed;left:50%;bottom:20px;transform:translateX(-50%);' + 'background:rgba(0,0,0,0.88);color:#fff;padding:12px 16px;border-radius:22px;' + 'font-size:13px;font-family:system-ui;z-index:2147483647;' + 'box-shadow:0 4px 20px rgba(0,0,0,0.4);max-width:92vw;box-sizing:border-box;' + 'display:flex;flex-direction:column;gap:10px;align-items:center;'; var line1 = document.createElement('div'); line1.style.cssText = 'font-weight:600;word-break:break-all;text-align:center;font-size:13px;max-width:80vw;'; line1.textContent = '🚫 ' + hostname; bar.appendChild(line1); var btnRow = document.createElement('div'); btnRow.style.cssText = 'display:flex;gap:6px;justify-content:center;align-items:center;flex-wrap:nowrap;'; var whiteBtn = document.createElement('button'); whiteBtn.textContent = '白名'; whiteBtn.style.cssText = 'padding:7px 14px;background:#34C759;color:#fff;border:none;border-radius:18px;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;'; whiteBtn.onclick = function() { var added = DomainList.addMany('white', hostname); var key = DomainList.stripSuffix(hostname); self._closeAskBar(); if (added > 0) showToast('已加入白名单:' + key + '(以后自动放行)'); else showToast('该域名已在白名单中'); self._doNavigate(url); }; btnRow.appendChild(whiteBtn); var blackBtn = document.createElement('button'); blackBtn.textContent = '黑名'; blackBtn.style.cssText = 'padding:7px 14px;background:#FF3B30;color:#fff;border:none;border-radius:18px;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;'; blackBtn.onclick = function() { var added = DomainList.addMany('black', hostname); var key = DomainList.stripSuffix(hostname); self._closeAskBar(); if (added > 0) showToast('已加入黑名单:' + key + '(以后自动拦)'); else showToast('该域名已在黑名单中'); }; btnRow.appendChild(blackBtn); var passBtn = document.createElement('button'); passBtn.textContent = '放行'; passBtn.style.cssText = 'padding:7px 14px;background:#007AFF;color:#fff;border:none;border-radius:18px;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;'; passBtn.onclick = function() { self._closeAskBar(); self._doNavigate(url); }; btnRow.appendChild(passBtn); var closeBtn = document.createElement('button'); closeBtn.textContent = '✕'; closeBtn.style.cssText = 'padding:0;background:#FF9500;color:#fff;border:none;border-radius:50%;font-size:12px;font-weight:700;cursor:pointer;width:30px;height:30px;display:flex;align-items:center;justify-content:center;flex-shrink:0;'; closeBtn.onclick = function() { self._closeAskBar(); }; btnRow.appendChild(closeBtn); bar.appendChild(btnRow); document.body.appendChild(bar); this._askBar = bar; this._askTimer = setTimeout(function() { self._closeAskBar(); }, 5000); }, _closeAskBar: function() { if (this._askTimer) { clearTimeout(this._askTimer); this._askTimer = null; } if (this._askBar) { if (this._askBar.parentNode) this._askBar.parentNode.removeChild(this._askBar); this._askBar = null; } }, _isAnchor: function(node) { if (!node || !node.tagName) return false; var tn = node.tagName; if (tn === 'A' || tn === 'a' || tn === 'AREA' || tn === 'area') return true; return false; }, _getHref: function(a) { if (!a) return ''; return a.getAttribute('href') || a.getAttribute('xlink:href') || a.href || ''; }, _def: function(obj, prop, buildDesc) { try { if (!obj || !prop) return; var desc = Object.getOwnPropertyDescriptor(obj, prop); if (!desc) return; if (desc.configurable === false) return; var nd = buildDesc(desc); if (!nd) return; Object.defineProperty(obj, prop, nd); } catch(e) {} }, install: function() { if (this._installed) return; this._installed = true; var W = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window; var D = W.document; var self = this; this._W = W; this._D = D; // ========== 1. Location.prototype.href setter ========== this._def(W.Location.prototype, 'href', function(d) { if (!d || !d.set) return null; var o = d.set; return { get: d.get, set: function(v) { if (self._checkBlock(String(v))) return; return o.call(this, v); }, enumerable: true, configurable: true }; }); // ========== 2. Location.prototype.assign ========== this._def(W.Location.prototype, 'assign', function(d) { if (!d || !d.value) return null; var o = d.value; return { value: function(u) { if (self._checkBlock(String(u))) return; return o.apply(this, arguments); }, writable: true, configurable: true, enumerable: d.enumerable !== undefined ? d.enumerable : false }; }); // ========== 3. Location.prototype.replace ========== this._def(W.Location.prototype, 'replace', function(d) { if (!d || !d.value) return null; var o = d.value; return { value: function(u) { if (self._checkBlock(String(u))) return; return o.apply(this, arguments); }, writable: true, configurable: true, enumerable: d.enumerable !== undefined ? d.enumerable : false }; }); // ========== 4. Location.prototype.protocol setter ========== this._def(W.Location.prototype, 'protocol', function(d) { if (!d || !d.set) return null; var o = d.set; return { get: d.get, set: function(v) { if (self.mode !== 'off') { var cur = location.protocol; var nv = String(v); if (nv.charAt(nv.length - 1) !== ':') nv += ':'; if (nv !== cur && nv !== 'http:' && nv !== 'https:') return; } return o.call(this, v); }, enumerable: true, configurable: true }; }); // ========== 5. Location.prototype.host setter ========== this._def(W.Location.prototype, 'host', function(d) { if (!d || !d.set) return null; var o = d.set; return { get: d.get, set: function(v) { if (self.mode !== 'off' && String(v) !== location.host) return; return o.call(this, v); }, enumerable: true, configurable: true }; }); // ========== 6. Location.prototype.hostname setter ========== this._def(W.Location.prototype, 'hostname', function(d) { if (!d || !d.set) return null; var o = d.set; return { get: d.get, set: function(v) { if (self.mode !== 'off' && String(v) !== location.hostname) return; return o.call(this, v); }, enumerable: true, configurable: true }; }); // ========== 7. Location.prototype.port setter ========== this._def(W.Location.prototype, 'port', function(d) { if (!d || !d.set) return null; var o = d.set; return { get: d.get, set: function(v) { if (self.mode !== 'off' && String(v) !== location.port) return; return o.call(this, v); }, enumerable: true, configurable: true }; }); // ========== 8. Location.prototype.search setter(原样放行) ========== this._def(W.Location.prototype, 'search', function(d) { if (!d || !d.set) return null; var o = d.set; return { get: d.get, set: function(v) { return o.call(this, v); }, enumerable: true, configurable: true }; }); // ========== 9. Location.prototype.hash setter(原样放行) ========== this._def(W.Location.prototype, 'hash', function(d) { if (!d || !d.set) return null; var o = d.set; return { get: d.get, set: function(v) { return o.call(this, v); }, enumerable: true, configurable: true }; }); // ========== 10. window.open ========== this._def(W, 'open', function(d) { if (!d || !d.value) return null; var o = d.value; return { value: function(url) { if (url && self._checkBlock(String(url))) return null; return o.apply(this, arguments); }, writable: true, configurable: true, enumerable: d.enumerable !== undefined ? d.enumerable : false }; }); // ========== 11. history.pushState ========== try { var origPush = W.history.pushState; W.history.pushState = function() { var url = arguments[2]; if (url && self.mode !== 'off' && typeof url === 'string') { try { var abs = new URL(url, location.href); if (abs.host !== location.host) return; } catch(e) {} } return origPush.apply(this, arguments); }; } catch(e) {} // ========== 12. history.replaceState ========== try { var origReplace = W.history.replaceState; W.history.replaceState = function() { var url = arguments[2]; if (url && self.mode !== 'off' && typeof url === 'string') { try { var abs = new URL(url, location.href); if (abs.host !== location.host) return; } catch(e) {} } return origReplace.apply(this, arguments); }; } catch(e) {} // ========== 13. Document.prototype.domain setter(禁掉) ========== this._def(W.Document.prototype, 'domain', function(d) { if (!d || !d.set) return null; return { get: d.get, set: function() { return; }, enumerable: true, configurable: true }; }); // ========== 14. Document.prototype.location setter ========== this._def(W.Document.prototype, 'location', function(d) { if (!d || !d.set) return null; var o = d.set; return { get: d.get, set: function(v) { if (self._checkBlock(String(v))) return; return o.call(this, v); }, enumerable: true, configurable: true }; }); // ========== 15. HTMLAnchorElement.prototype.click ========== this._def(W.HTMLAnchorElement.prototype, 'click', function(d) { if (!d || !d.value) return null; var o = d.value; return { value: function() { var h = self._getHref(this); if (h && self._checkBlock(h)) return; return o.apply(this, arguments); }, writable: true, configurable: true, enumerable: d.enumerable !== undefined ? d.enumerable : false }; }); // ========== 16. HTMLAreaElement.prototype.href ========== this._def(W.HTMLAreaElement.prototype, 'href', function(d) { if (!d || !d.set) return null; var o = d.set; return { get: d.get, set: function(v) { if (self._checkBlock(String(v))) return; return o.call(this, v); }, enumerable: true, configurable: true }; }); // ========== 17. HTMLEmbedElement.prototype.src ========== try { this._def(W.HTMLEmbedElement.prototype, 'src', function(d) { if (!d || !d.set) return null; var o = d.set; return { get: d.get, set: function(v) { if (self._checkBlock(String(v))) return; return o.call(this, v); }, enumerable: true, configurable: true }; }); } catch(e) {} // ========== 18. HTMLObjectElement.prototype.data ========== try { this._def(W.HTMLObjectElement.prototype, 'data', function(d) { if (!d || !d.set) return null; var o = d.set; return { get: d.get, set: function(v) { if (self._checkBlock(String(v))) return; return o.call(this, v); }, enumerable: true, configurable: true }; }); } catch(e) {} // ========== 19. HTMLFormElement.prototype.submit ========== try { var origFormSubmit = W.HTMLFormElement.prototype.submit; W.HTMLFormElement.prototype.submit = function() { if (self.mode !== 'off') { var action = this.getAttribute('action') || this.action || ''; if (action && self._checkBlock(action)) return; } return origFormSubmit.apply(this, arguments); }; } catch(e) {} // ========== 20. HTMLFormElement.prototype.requestSubmit ========== try { var origReqSubmit = W.HTMLFormElement.prototype.requestSubmit; if (origReqSubmit) { W.HTMLFormElement.prototype.requestSubmit = function() { if (self.mode !== 'off') { var action = this.getAttribute('action') || this.action || ''; if (action && self._checkBlock(action)) return; } return origReqSubmit.apply(this, arguments); }; } } catch(e) {} // ========== 21. / 点击/触摸/鼠标捕获 ========== var blockAnchorEvent = function(e) { if (self.mode === 'off') return; if (e.defaultPrevented) return; var node = e.target; var a = null, guard = 0; while (node && guard < 30) { if (self._isAnchor(node)) { a = node; break; } node = node.parentElement || node.parentNode; guard++; if (!node || node.nodeType !== 1) break; } if (!a) return; var href = self._getHref(a); if (!href || href.charAt(0) === '#') return; if (/^javascript:/i.test(href)) return; if (!self.isCrossOrigin(href)) return; if (self._checkBlock(href)) { e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); } }; try { D.addEventListener('click', blockAnchorEvent, true); } catch(e) {} try { D.addEventListener('mousedown', blockAnchorEvent, true); } catch(e) {} try { D.addEventListener('touchstart', blockAnchorEvent, true); } catch(e) {} try { D.addEventListener('pointerdown', blockAnchorEvent, true); } catch(e) {} // ========== 22. form submit 事件捕获 ========== try { D.addEventListener('submit', function(e) { if (self.mode === 'off') return; if (e.defaultPrevented) return; var form = e.target; if (!form || form.tagName !== 'FORM') return; var submitter = e.submitter; var action = ''; if (submitter && submitter.getAttribute) { var fa = submitter.getAttribute('formaction'); if (fa) action = fa; } if (!action) action = form.getAttribute('action') || form.action || ''; if (!action) return; if (action.charAt(0) === '#' || action.charAt(0) === '/') return; if (/^javascript:/i.test(action)) return; if (self._checkBlock(action)) { e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); } }, true); } catch(e) {} // ========== 23. document.write / writeln hook ========== try { var origWrite = D.write; var origWriteln = D.writeln; D.write = function() { if (self.mode !== 'off') { for (var i = 0; i < arguments.length; i++) { var s = arguments[i]; if (typeof s !== 'string') continue; var m = s.match(/location\s*(?:\.href)?\s*=\s*['"]([^'"]+)['"]/i); if (m && m[1] && self._checkBlock(m[1])) return; m = s.match(/location\.(?:replace|assign)\s*\(\s*['"]([^'"]+)['"]/i); if (m && m[1] && self._checkBlock(m[1])) return; m = s.match(/window\.open\s*\(\s*['"]([^'"]+)['"]/i); if (m && m[1] && self._checkBlock(m[1])) return; m = s.match(/location\.host(?:name)?\s*=\s*['"]([^'"]+)['"]/i); if (m && m[1] && m[1] !== location.hostname) return; } } return origWrite.apply(this, arguments); }; D.writeln = function() { if (self.mode !== 'off') { for (var i = 0; i < arguments.length; i++) { var s = arguments[i]; if (typeof s !== 'string') continue; var m = s.match(/location\s*(?:\.href)?\s*=\s*['"]([^'"]+)['"]/i); if (m && m[1] && self._checkBlock(m[1])) return; } } return origWriteln.apply(this, arguments); }; } catch(e) {} // ========== 24. meta refresh / base / link prefetch 监视 ========== try { var checkNode = function(node) { if (self.mode === 'off' || !node || node.nodeType !== 1) return; var tag = node.tagName; if (tag === 'META') { var eq = (node.getAttribute('http-equiv') || '').toLowerCase(); if (eq === 'refresh') { var c = node.getAttribute('content') || ''; var m = c.match(/url\s*=\s*['"]?([^'"\s>]+)/i); if (m && m[1] && self.isCrossOrigin(m[1])) { var host = ''; try { host = new URL(m[1], location.href).hostname; } catch(e) {} if (DomainList.hit('white', host)) return; if (DomainList.hit('black', host) || self.mode === 'block') node.remove(); } } } else if (tag === 'BASE') { var bh = node.getAttribute('href'); if (bh && self.isCrossOrigin(bh)) node.removeAttribute('href'); } else if (tag === 'LINK') { var rel = (node.getAttribute('rel') || '').toLowerCase(); if (rel === 'prefetch' || rel === 'prerender' || rel === 'preload') { var lh = node.getAttribute('href'); if (lh && self.isCrossOrigin(lh)) node.remove(); } } }; var mo = new MutationObserver(function(muts) { if (self.mode === 'off') return; muts.forEach(function(mu) { mu.addedNodes.forEach(function(n) { if (!n || n.nodeType !== 1) return; checkNode(n); if (n.querySelectorAll) { try { var nodes = n.querySelectorAll('meta[http-equiv="refresh"], base[href], link[rel]'); for (var i = 0; i < nodes.length; i++) checkNode(nodes[i]); } catch(e) {} } }); if (mu.type === 'attributes' && mu.target) checkNode(mu.target); }); }); var observeTarget = function() { var t = D.documentElement || D; if (!t) return; try { mo.observe(t, { childList: true, subtree: true, attributes: true, attributeFilter: ['href', 'content'] }); } catch(e) {} try { var nodes = D.querySelectorAll('meta[http-equiv="refresh"], base[href], link[rel]'); for (var i = 0; i < nodes.length; i++) checkNode(nodes[i]); } catch(e) {} }; if (D.documentElement) observeTarget(); else if (D.addEventListener) D.addEventListener('DOMContentLoaded', observeTarget, { once: true }); } catch(e) {} // ========== 25. 同源 iframe hook ========== try { var hookFrame = function(frame) { if (!frame || frame.tagName !== 'IFRAME') return; try { var cd = frame.contentDocument; if (!cd || cd.__ab_hooked__) return; cd.__ab_hooked__ = true; var frameBlock = function(e) { if (self.mode === 'off') return; if (e.defaultPrevented) return; var node = e.target; var a = null, guard = 0; while (node && guard < 30) { if (self._isAnchor(node)) { a = node; break; } node = node.parentElement || node.parentNode; guard++; if (!node || node.nodeType !== 1) break; } if (!a) return; var target = (a.getAttribute('target') || '').toLowerCase(); if (target !== '_top' && target !== '_parent') return; var href = self._getHref(a); if (!href || href.charAt(0) === '#') return; if (/^javascript:/i.test(href)) return; if (!self.isCrossOrigin(href)) return; if (self._checkBlock(href)) { e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); } }; cd.addEventListener('click', frameBlock, true); cd.addEventListener('mousedown', frameBlock, true); cd.addEventListener('touchstart', frameBlock, true); cd.addEventListener('pointerdown', frameBlock, true); } catch(e) {} }; try { var initialFrames = D.querySelectorAll('iframe'); for (var fi = 0; fi < initialFrames.length; fi++) hookFrame(initialFrames[fi]); } catch(e) {} try { var iframeMo = new MutationObserver(function(muts) { muts.forEach(function(mu) { mu.addedNodes.forEach(function(n) { if (!n || n.nodeType !== 1) return; if (n.tagName === 'IFRAME') hookFrame(n); if (n.querySelectorAll) { try { var iframes = n.querySelectorAll('iframe'); for (var k = 0; k < iframes.length; k++) hookFrame(iframes[k]); } catch(e) {} } }); }); }); var iframeTarget = D.documentElement || D; if (iframeTarget) iframeMo.observe(iframeTarget, { childList: true, subtree: true }); } catch(e) {} try { D.addEventListener('load', function(e) { if (e.target && e.target.tagName === 'IFRAME') hookFrame(e.target); }, true); } catch(e) {} } catch(e) {} // ========== 26. Navigation API ========== try { if (W.navigation && typeof W.navigation.navigate === 'function') { var origNav = W.navigation.navigate; W.navigation.navigate = function(url) { if (self.mode !== 'off' && self._checkBlock(url)) { return { committed: Promise.reject(new Error('blocked')), finished: Promise.reject(new Error('blocked')) }; } return origNav.apply(this, arguments); }; } if (W.navigation && typeof W.navigation.addEventListener === 'function') { W.navigation.addEventListener('navigate', function(e) { if (self.mode === 'off') return; if (e.destination && e.destination.url) { var block = false; try { block = self.shouldBlock(e.destination.url); } catch(ex) { block = self.mode !== 'off'; } if (block) { try { e.preventDefault(); } catch(ex) {} } } }); } } catch(e) {} } }; JumpBlocker.load(); JumpBlocker.install(); var _hideModeCache = null; function getAllHideModes() { if (_hideModeCache) return _hideModeCache; try { _hideModeCache = JSON.parse(GM_getValue(HIDE_MODE_KEY, '{}')); } catch(e) { _hideModeCache = {}; } return _hideModeCache; } function getHideMode() { var m = getAllHideModes()[HOST]; if (m === 'keep' || m === 'clean' || m === 'smart') return m; return 'smart'; } function setHideMode(mode) { if (mode !== 'keep' && mode !== 'clean' && mode !== 'smart') return; var all = getAllHideModes(); all[HOST] = mode; _hideModeCache = all; try { GM_setValue(HIDE_MODE_KEY, JSON.stringify(all)); } catch(e) {} } function getHideModeLabel(m) { if (m === 'keep') return '留白防检测'; if (m === 'clean') return '干净不留白'; return '智能判断'; } function getHideModeHint(m) { if (m === 'keep') return '内容不回流,防检测强'; if (m === 'clean') return '内容回流,页面不留白'; return '自动判断广告类型'; } function smartDecideMode(el) { if (!el || el.nodeType !== 1) return 'clean'; if (el.tagName === 'IFRAME') return 'keep'; try { var cs = window.getComputedStyle(el); if (cs && cs.position === 'fixed') { var r = el.getBoundingClientRect(); if (r.width > 0 && r.height > 0 && r.width < window.innerWidth * 0.85 && r.height < window.innerHeight * 0.6) return 'keep'; } } catch(e) {} var name = (String(el.className || '') + ' ' + String(el.id || '')).toLowerCase(); if (/(^|[\s_\-.])(ad|ads|advert|advertising|banner|sponsor|promo|tuiguang|gg)([\s_\-.]|$)/i.test(name)) return 'keep'; return 'clean'; } function resolveModeForElement(el) { var m = getHideMode(); if (m === 'keep') return 'keep'; if (m === 'clean') return 'clean'; return smartDecideMode(el); } var _autoDetectCache = null; function getAllAutoDetect() { if (_autoDetectCache) return _autoDetectCache; try { _autoDetectCache = JSON.parse(GM_getValue(AUTO_DETECT_KEY, '{}')); } catch(e) { _autoDetectCache = {}; } return _autoDetectCache; } function getAutoDetectLevel() { var m = getAllAutoDetect()[HOST]; if (m === 'off' || m === 'conservative' || m === 'aggressive') return m; return 'off'; } function setAutoDetectLevel(level) { if (level !== 'off' && level !== 'conservative' && level !== 'aggressive') return; var all = getAllAutoDetect(); all[HOST] = level; _autoDetectCache = all; try { GM_setValue(AUTO_DETECT_KEY, JSON.stringify(all)); } catch(e) {} } function getAutoDetectLabel(l) { if (l === 'conservative') return '保守'; if (l === 'aggressive') return '激进'; return '关闭'; } function getAutoDetectHint(l) { if (l === 'conservative') return '只抓确定是广告的,几乎不误伤'; if (l === 'aggressive') return '含赌博广告识别,可能误伤正常内容'; return '不自动扫描,纯手动'; } var AUTO_CONSERVATIVE_SELECTORS = [ 'ins.adsbygoogle','[data-ad-slot]','[data-ad-client]','[data-tuiguang]','[class*="ec-tuiguang"]', 'iframe[src*="doubleclick.net"]','iframe[src*="googlesyndication"]','iframe[src*="cpro.baidu.com"]','iframe[src*="pos.baidu.com"]', '[class*="adsbygoogle"]','[class*="advertisement"]','[class*="ad-container"]','[class*="ad-wrapper"]' ]; var AUTO_AGGRESSIVE_WORD_RE = /(^|[\s_\-.])(ad|ads|banner|promo|sponsor|tuiguang|gg)([\s_\-.]|$)/i; var AUTO_STD_SIZES = [[300,250],[728,90],[320,100],[336,280],[970,90],[970,250],[300,50],[250,250],[200,200]]; var GAMBLING_URL_KEYWORDS = ['dubo','casino','bet','poker','gamble','gambling','22bet','11x','xpj','bwin','1xbet','bet365','/bc/','cp-','qp-','ty-','jy-','ky-','hgw','hg0088','188bet','sbobet','dafa','macau']; var GAMBLING_CN_KEYWORDS = ['赌博','博彩','赌场','娱乐城','真人视讯','真人娱乐','电子游艺','电子游戏','体育博彩','彩票','棋牌','真人荷官','在线娱乐','在线赌场','首存','彩金','注册送','充值送','返水','首充','长期稳定','澳门','新葡京','威尼斯人','太阳城','新澳门','金沙','永利','美高梅','六合','马会','赛马','香港六合','六合彩','时时彩','百家乐','龙虎','牛牛','炸金花','老虎机','轮盘','包赢','稳赚','包赔']; var GAMBLING_DOMAIN_BLACKLIST = ['22bet','11x','1xbet','bet365','bwin','sbobet','dafa','hgw','hg0088','188bet','xpj','macau','dubo','casino','poker','gambling']; function urlHasGamblingKeyword(url) { if (!url || typeof url !== 'string') return false; var lower = url.toLowerCase(); for (var i = 0; i < GAMBLING_URL_KEYWORDS.length; i++) { if (lower.indexOf(GAMBLING_URL_KEYWORDS[i]) !== -1) return true; } return false; } function urlInGamblingBlacklist(url) { if (!url || typeof url !== 'string') return false; try { var u = new URL(url, location.href); var host = u.hostname.toLowerCase(); for (var i = 0; i < GAMBLING_DOMAIN_BLACKLIST.length; i++) { if (host.indexOf(GAMBLING_DOMAIN_BLACKLIST[i]) !== -1) return true; } } catch(e) {} return false; } function textHasGamblingCN(text) { if (!text || typeof text !== 'string') return false; for (var i = 0; i < GAMBLING_CN_KEYWORDS.length; i++) { if (text.indexOf(GAMBLING_CN_KEYWORDS[i]) !== -1) return true; } return false; } function checkGamblingAd(el) { if (!el || el.nodeType !== 1) return null; var signals = 0, strongHit = false; var tag = el.tagName; var img = null; if (tag === 'IMG') img = el; else if (el.querySelector) img = el.querySelector('img'); var link = null; if (tag === 'A') link = el; else if (el.closest) link = el.closest('a'); if (!link && el.querySelector) link = el.querySelector('a'); if (img) { var src = img.getAttribute('src') || img.getAttribute('data-src') || img.src || ''; if (urlHasGamblingKeyword(src)) signals++; if (urlInGamblingBlacklist(src)) { signals += 3; strongHit = true; } var alt = img.getAttribute('alt') || ''; if (textHasGamblingCN(alt)) signals++; } var ownText = ''; try { ownText = (el.textContent || '').slice(0, 500); } catch(e) {} if (textHasGamblingCN(ownText)) signals++; var selfAttrs = String(el.className || '') + ' ' + String(el.id || '') + ' ' + String(el.getAttribute('title') || ''); if (urlHasGamblingKeyword(selfAttrs)) signals++; if (link) { var href = link.getAttribute('href') || link.href || ''; if (urlHasGamblingKeyword(href)) signals++; if (urlInGamblingBlacklist(href)) { signals += 3; strongHit = true; } } if (strongHit) return 'strong'; if (signals >= 2) return 'strong'; if (signals === 1) return 'weak'; return null; } function runAutoDetect() { var level = getAutoDetectLevel(); if (level === 'off') return 0; if (!document.body) return 0; var seen = new WeakSet(); var candidates = []; AUTO_CONSERVATIVE_SELECTORS.forEach(function(sel) { try { var els = document.querySelectorAll(sel); for (var i = 0; i < els.length; i++) { var el = els[i]; if (seen.has(el)) continue; seen.add(el); if (isOurElement(el)) continue; if (el === document.body || el === document.documentElement) continue; candidates.push(el); } } catch(e) {} }); if (level === 'aggressive') { try { var scan = document.querySelectorAll('div, section, aside, iframe, img'); for (var i = 0; i < scan.length; i++) { var el = scan[i]; if (seen.has(el)) continue; seen.add(el); if (isOurElement(el)) continue; if (el === document.body || el === document.documentElement) continue; var name = (String(el.className || '') + ' ' + String(el.id || '')).toLowerCase(); if (AUTO_AGGRESSIVE_WORD_RE.test(name)) { candidates.push(el); continue; } if (el.tagName === 'IFRAME') { var w = el.offsetWidth, h = el.offsetHeight; if (w > 0 && h > 0) { for (var j = 0; j < AUTO_STD_SIZES.length; j++) { if (Math.abs(w - AUTO_STD_SIZES[j][0]) < 8 && Math.abs(h - AUTO_STD_SIZES[j][1]) < 8) { candidates.push(el); break; } } } } } } catch(e) {} try { var gs = document.querySelectorAll('a, img, div'); for (var i = 0; i < gs.length; i++) { var el = gs[i]; if (seen.has(el)) continue; if (isOurElement(el)) continue; if (el === document.body || el === document.documentElement) continue; var verdict = checkGamblingAd(el); if (verdict === 'strong') { seen.add(el); candidates.push(el); } } } catch(e) {} } var valid = []; for (var i = 0; i < candidates.length; i++) { var el = candidates[i]; var r = el.getBoundingClientRect(); if (r.width < 20 || r.height < 20) continue; if (r.width > window.innerWidth * 0.98 && r.height > window.innerHeight * 0.8) continue; valid.push(el); } if (valid.length === 0) return 0; var existing = Storage.list(); var existingSelectors = {}; existing.forEach(function(r) { if (r.type === 'hide' && r.selector) existingSelectors[r.selector] = true; }); var added = 0; var newRules = []; valid.forEach(function(el) { var selector = generateSelector(el); if (!selector) return; if (existingSelectors[selector]) return; var mc = 0; try { mc = document.querySelectorAll(selector).length; } catch(e) { return; } if (mc < 1 || mc > 50) return; existingSelectors[selector] = true; newRules.push({ id: genId(), type: 'hide', selector: selector, enabled: true, auto: true, createdAt: Date.now() }); added++; }); if (newRules.length > 0) { Storage.addMany(newRules); HideManager.apply(); showToast('🤖 自动识别:新增 ' + added + ' 条规则'); } return added; } var StyleManager = { styleEl: null, ensure: function() { if (this.styleEl && this.styleEl.isConnected) return; var s = document.createElement('style'); s.id = '__ab_style'; s.textContent = '[data-ab-hide-rule][data-ab-hide-mode="keep"]{visibility:hidden !important;opacity:0 !important;pointer-events:none !important;}' + '[data-ab-hide-rule][data-ab-hide-mode="keep"] *{visibility:hidden !important;pointer-events:none !important;}' + '[data-ab-hide-rule][data-ab-hide-mode="clean"]{display:none !important;}' + '[data-ab-hide-rule]:not([data-ab-hide-mode]){visibility:hidden !important;opacity:0 !important;pointer-events:none !important;}'; var target = document.head || document.documentElement; if (target) target.appendChild(s); this.styleEl = s; } }; function computeFingerprint(el) { if (!el || el.nodeType !== 1) return null; if (el === document.body || el === document.documentElement) return null; var tag = el.tagName.toLowerCase(); var path = []; var node = el, safety = 0; while (node && node !== document.body && node.parentElement && safety < 30) { var parent = node.parentElement; var idx = -1; for (var i = 0; i < parent.children.length; i++) { if (parent.children[i] === node) { idx = i; break; } } if (idx < 0) return null; path.unshift(idx); node = parent; safety++; } if (safety >= 30) return null; var rect = el.getBoundingClientRect(); var w = Math.round(rect.width), h = Math.round(rect.height); if (w <= 0 || h <= 0) return null; var parentEl = el.parentElement; var siblings = parentEl ? parentEl.children.length : 0; var ratio = h > 0 ? Math.round((w / h) * 100) / 100 : 0; return { tag: tag, depth: path.length, path: path, siblings: siblings, w: w, h: h, ratio: ratio }; } function fingerprintsMatchStrict(fp1, fp2) { if (!fp1 || !fp2) return false; if (fp1.tag !== fp2.tag) return false; if (fp1.depth !== fp2.depth) return false; if (fp1.siblings !== fp2.siblings) return false; if (fp1.w !== fp2.w) return false; if (fp1.h !== fp2.h) return false; if (!fp1.path || !fp2.path) return false; if (fp1.path.length !== fp2.path.length) return false; for (var i = 0; i < fp1.path.length; i++) { if (fp1.path[i] !== fp2.path[i]) return false; } return true; } function fingerprintsMatchLoose(fp1, fp2) { if (!fp1 || !fp2) return false; if (fp1.tag !== fp2.tag) return false; var wRatio = Math.abs(fp1.w - fp2.w) / Math.max(fp1.w, fp2.w, 1); var hRatio = Math.abs(fp1.h - fp2.h) / Math.max(fp1.h, fp2.h, 1); if (wRatio > 0.25 || hRatio > 0.25) return false; if (Math.abs(fp1.depth - fp2.depth) > 2) return false; return true; } function findElementByFingerprint(fp) { if (!fp || !fp.tag) return null; var candidates; try { candidates = document.querySelectorAll(fp.tag); } catch (e) { return null; } for (var i = 0; i < candidates.length; i++) { var el = candidates[i]; if (el === document.body || el === document.documentElement) continue; if (isOurElement(el)) continue; var cur = computeFingerprint(el); if (cur && fingerprintsMatchStrict(cur, fp)) return el; } return null; } function computeStructure(el, maxDepth) { if (!el || el.nodeType !== 1) return null; if (maxDepth === undefined) maxDepth = 2; var rect = el.getBoundingClientRect(); var w = rect.width, h = rect.height; if (w <= 0 || h <= 0) return null; var ratio = h > 0 ? Math.round((w / h) * 100) / 100 : 0; var node = { tag: el.tagName.toLowerCase(), ratio: ratio, childCount: el.children.length, children: [] }; if (maxDepth > 0) { var kids = el.children; var limit = Math.min(kids.length, 20); for (var i = 0; i < limit; i++) { var child = computeStructure(kids[i], maxDepth - 1); if (child) node.children.push(child); } } return node; } function compareStructure(s1, s2) { if (!s1 || !s2) return 0; if (s1.tag !== s2.tag) return 0; var score = 0, weight = 0; weight += 1; if (s1.childCount === s2.childCount) score += 1; else if (Math.abs(s1.childCount - s2.childCount) <= 1) score += 0.5; weight += 1; if (s1.ratio > 0 && s2.ratio > 0) { var r1 = s1.ratio, r2 = s2.ratio; var diff = Math.abs(r1 - r2) / Math.max(r1, r2); if (diff < 0.2) score += 1; else if (diff < 0.5) score += 0.5; } if (s1.children.length > 0 && s2.children.length > 0) { var minLen = Math.min(s1.children.length, s2.children.length); var maxLen = Math.max(s1.children.length, s2.children.length); var childScore = 0; for (var i = 0; i < minLen; i++) childScore += compareStructure(s1.children[i], s2.children[i]); weight += 2; score += (childScore / maxLen) * 2; } if (weight === 0) return 0; return score / weight; } var Storage = { _loadAll: function() { try { return JSON.parse(GM_getValue(STORAGE_KEY, '{}')); } catch(e) { return {}; } }, _saveAll: function(a) { GM_setValue(STORAGE_KEY, JSON.stringify(a)); }, list: function() { var all = this._loadAll(); var arr = all[HOST] || []; return arr.map(function(m) { var it = Object.assign({}, m); if (!it.type) it.type = 'mask'; if (it.type === 'mask' && !it.mode) it.mode = 'doc'; return it; }); }, save: function(list) { var all = this._loadAll(); if (list.length) all[HOST] = list; else delete all[HOST]; this._saveAll(all); }, add: function(item) { var l = this.list(); l.push(item); this.save(l); }, addMany: function(items) { var l = this.list(); items.forEach(function(it) { l.push(it); }); this.save(l); }, remove: function(id) { this.save(this.list().filter(function(m) { return m.id !== id; })); }, removeGroup: function(groupId) { this.save(this.list().filter(function(m) { return m.groupId !== groupId; })); }, removeAllAuto: function() { this.save(this.list().filter(function(m) { return !m.auto; })); }, clear: function() { this.save([]); }, allHosts: function() { var all = this._loadAll(); return Object.keys(all).filter(function(k) { return Array.isArray(all[k]) && all[k].length > 0; }); }, getHostRules: function(host) { var all = this._loadAll(); return all[host] || []; }, getRemarkRaw: function(host) { var map = {}; try { map = JSON.parse(GM_getValue(REMARK_KEY, '{}')); } catch(e) {} return map[host] || null; }, getRemark: function(host) { var data = this.getRemarkRaw(host); if (!data) return ''; if (typeof data === 'string') return data; return data.text || ''; }, setRemark: function(host, remark) { var map = {}; try { map = JSON.parse(GM_getValue(REMARK_KEY, '{}')); } catch(e) {} remark = (remark || '').trim(); if (remark) map[host] = { text: remark, updatedAt: Date.now() }; else delete map[host]; GM_setValue(REMARK_KEY, JSON.stringify(map)); }, getAllRemarks: function() { try { return JSON.parse(GM_getValue(REMARK_KEY, '{}')); } catch(e) { return {}; } } }; var Renderer = { docContainer: null, fixedContainer: null, els: {}, ensure: function() { var p = document.body || document.documentElement; if (!p) return false; if (!this.docContainer || !this.docContainer.isConnected) { var c = document.createElement('div'); c.id = '__ab_mask_doc_container'; c.style.cssText = 'position:absolute;top:0;left:0;width:0;height:0;pointer-events:none;z-index:' + MAX_Z + ';'; p.appendChild(c); this.docContainer = c; } if (!this.fixedContainer || !this.fixedContainer.isConnected) { var c2 = document.createElement('div'); c2.id = '__ab_mask_fixed_container'; c2.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;pointer-events:none;z-index:' + MAX_Z + ';'; p.appendChild(c2); this.fixedContainer = c2; } return true; }, render: function() { if (!this.ensure()) return; var self = this; var list = Storage.list().filter(function(m) { return m.type === 'mask'; }); var ds = getDocSize(), vw = window.innerWidth, vh = window.innerHeight; var ids = {}; list.forEach(function(m) { ids[m.id] = true; }); for (var id in this.els) { if (!ids[id]) { this.els[id].remove(); delete this.els[id]; } } list.forEach(function(m) { var isFixed = m.mode === 'fixed'; var el = self.els[m.id]; if (el && el.dataset.mode !== m.mode) { el.remove(); delete self.els[m.id]; el = null; } if (!el) { el = self._create(m); el.dataset.mode = m.mode; (isFixed ? self.fixedContainer : self.docContainer).appendChild(el); self.els[m.id] = el; } var W = isFixed ? vw : ds.w, H = isFixed ? vh : ds.h; el.style.left = (m.x * W) + 'px'; el.style.top = (m.y * H) + 'px'; el.style.width = (m.w * W) + 'px'; el.style.height = (m.h * H) + 'px'; el.style.display = m.enabled ? 'block' : 'none'; }); }, _create: function(m) { var isFixed = m.mode === 'fixed'; var el = document.createElement('div'); el.dataset.maskId = m.id; el.dataset.mode = m.mode; el.style.cssText = 'position:' + (isFixed ? 'fixed' : 'absolute') + ';background:#ffffff;box-sizing:border-box;pointer-events:auto;z-index:' + MAX_Z + ';touch-action:pan-y;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;'; el.addEventListener('click', function(e) { e.stopPropagation(); }); return el; } }; var HideManager = { _scheduled: false, _timer: null, apply: function() { StyleManager.ensure(); var rules = Storage.list().filter(function(r) { return r.enabled; }); var hideRules = rules.filter(function(r) { return r.type === 'hide'; }); hideRules.forEach(function(rule) { try { var els = document.querySelectorAll(rule.selector); for (var i = 0; i < els.length; i++) { var el = els[i]; if (!el || el.nodeType !== 1) continue; if (el === document.body || el === document.documentElement) continue; if (isOurElement(el)) continue; el.dataset.abHideRule = rule.id; el.dataset.abHideMode = resolveModeForElement(el); } } catch(e) {} }); var posRules = rules.filter(function(r) { return r.type === 'pos' && r.tag; }); if (posRules.length > 0) applyPositionRules(posRules); }, schedule: function() { if (this._scheduled) return; this._scheduled = true; var self = this; var run = function() { self._scheduled = false; self.apply(); }; if (typeof requestAnimationFrame === 'function') requestAnimationFrame(run); else setTimeout(run, 16); }, startPeriodic: function() { if (this._timer) return; var self = this; this._timer = setInterval(function() { try { self.apply(); } catch(e) {} }, 5000); } }; function applyPositionRules(posRules) { var tagSet = {}; posRules.forEach(function(r) { tagSet[r.tag] = true; }); var candidates = []; for (var t in tagSet) { try { var els = document.querySelectorAll(t); for (var i = 0; i < els.length; i++) candidates.push(els[i]); } catch (e) {} } for (var i = 0; i < candidates.length; i++) { var el = candidates[i]; if (el === document.body || el === document.documentElement) continue; if (isOurElement(el)) continue; if (el.dataset.abHideRule) continue; var fp = computeFingerprint(el); if (!fp) continue; for (var j = 0; j < posRules.length; j++) { if (fingerprintsMatchStrict(fp, posRules[j]) || fingerprintsMatchLoose(fp, posRules[j])) { el.dataset.abHideRule = posRules[j].id; el.dataset.abHideMode = resolveModeForElement(el); break; } } } } var ClickSelector = { overlay: null, marks: null, focusEl: null, focusHistory: [], statusBar: null, topTip: null, active: false, updateTimer: null, _touchStartPos: null, _onScroll: null, _onResize: null, _smartRoot: null, _clipRect: null, _clipBorder: null, _cancelMode: false, _colorPool: ['#FF3B30','#007AFF','#34C759','#FF9500','#AF52DE','#5856D6','#FF2D55','#5AC8FA'], _colorIndex: 0, _parentColors: null, start: function() { if (this.active) return; this.active = true; this.marks = new Map(); this.focusEl = null; this.focusHistory = []; this._smartRoot = null; this._clipRect = null; this._clipBorder = null; this._cancelMode = false; this._colorIndex = 0; this._parentColors = new Map(); this._build(); }, _makeDraggable: function(el, handleEl, excludeEls) { var dragging = false, startX = 0, startY = 0, startRect = null; var onStart = function(e) { if (excludeEls) { for (var i = 0; i < excludeEls.length; i++) { var ex = excludeEls[i]; if (!ex) continue; if (ex === e.target) return; if (ex.contains && ex.contains(e.target)) return; } } if (handleEl && e.target !== handleEl && !(handleEl.contains && handleEl.contains(e.target))) return; var touch = e.touches ? e.touches[0] : e; startX = touch.clientX; startY = touch.clientY; startRect = el.getBoundingClientRect(); el.style.left = startRect.left + 'px'; el.style.top = startRect.top + 'px'; el.style.right = 'auto'; el.style.bottom = 'auto'; el.style.transform = 'none'; dragging = true; try { e.preventDefault(); e.stopPropagation(); } catch(ex2) {} }; var onMove = function(e) { if (!dragging) return; var touch = e.touches ? e.touches[0] : e; var dx = touch.clientX - startX, dy = touch.clientY - startY; var newX = Math.max(0, Math.min(window.innerWidth - el.offsetWidth, startRect.left + dx)); var newY = Math.max(0, Math.min(window.innerHeight - el.offsetHeight, startRect.top + dy)); el.style.left = newX + 'px'; el.style.top = newY + 'px'; try { e.preventDefault(); } catch(ex2) {} }; var onEnd = function() { dragging = false; }; el.addEventListener('touchstart', onStart, { passive: false }); el.addEventListener('touchmove', onMove, { passive: false }); el.addEventListener('touchend', onEnd); el.addEventListener('touchcancel', onEnd); el.addEventListener('mousedown', onStart); document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onEnd); }, _build: function() { var self = this; var topTip = document.createElement('div'); topTip.id = '__ab_top_tip'; topTip.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.88);color:#fff;padding:8px 10px 8px 18px;border-radius:20px;font-size:13px;font-family:system-ui;z-index:' + MAX_Z + ';white-space:nowrap;box-shadow:0 4px 16px rgba(0,0,0,0.3);display:flex;align-items:center;gap:10px;cursor:move;user-select:none;'; var tipText = document.createElement('span'); tipText.textContent = '👆 点击广告元素(可多选)'; tipText.style.pointerEvents = 'none'; topTip.appendChild(tipText); var closeX = document.createElement('button'); closeX.textContent = '✕'; closeX.style.cssText = 'background:rgba(255,255,255,0.2);border:none;color:#fff;width:22px;height:22px;border-radius:50%;font-size:12px;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;'; closeX.onclick = function() { self._cancel(); }; topTip.appendChild(closeX); document.body.appendChild(topTip); this.topTip = topTip; var ov = document.createElement('div'); ov.id = '__ab_click_overlay'; ov.style.cssText = 'position:fixed;inset:0;background:transparent;z-index:' + (parseInt(MAX_Z) - 2) + ';touch-action:pan-y;cursor:crosshair;'; document.body.appendChild(ov); this.overlay = ov; var bar = document.createElement('div'); bar.id = '__ab_status_bar'; bar.style.cssText = 'position:fixed;bottom:20px;left:50%;transform:translateX(-50%);display:flex;gap:6px;z-index:' + MAX_Z + ';align-items:center;background:rgba(0,0,0,0.88);padding:8px;border-radius:32px;box-shadow:0 4px 20px rgba(0,0,0,0.4);max-width:96vw;flex-wrap:nowrap;'; this.statusBar = bar; var hint = document.createElement('div'); hint.textContent = '已选 0'; hint.style.cssText = 'color:#fff;font-size:12px;font-family:system-ui;padding:6px 12px;white-space:nowrap;font-weight:600;cursor:move;user-select:none;-webkit-user-select:none;touch-action:none;'; bar.appendChild(hint); this.hintEl = hint; var upBtn = document.createElement('button'); upBtn.textContent = '⬆️父'; upBtn.style.cssText = 'padding:9px 10px;background:#FF9500;color:#fff;border:none;border-radius:18px;font-size:12px;font-weight:600;cursor:pointer;opacity:0.35;'; upBtn.disabled = true; upBtn.onclick = function(e) { e.stopPropagation(); self._expand(); }; bar.appendChild(upBtn); this.upBtn = upBtn; var downBtn = document.createElement('button'); downBtn.textContent = '⬇️子'; downBtn.style.cssText = 'padding:9px 10px;background:#FF9500;color:#fff;border:none;border-radius:18px;font-size:12px;font-weight:600;cursor:pointer;opacity:0.35;'; downBtn.disabled = true; downBtn.onclick = function(e) { e.stopPropagation(); self._shrink(); }; bar.appendChild(downBtn); this.downBtn = downBtn; var actionCol = document.createElement('div'); actionCol.style.cssText = 'display:flex;flex-direction:column;gap:4px;'; var layerBtn = document.createElement('button'); layerBtn.textContent = '✨ 选层'; layerBtn.style.cssText = 'padding:4px 8px;background:#5856D6;color:#fff;border:none;border-radius:12px;font-size:10px;font-weight:600;cursor:pointer;white-space:nowrap;'; layerBtn.onclick = function(e) { e.stopPropagation(); self._smartSelectLayer(); }; actionCol.appendChild(layerBtn); var clipBtn = document.createElement('button'); clipBtn.textContent = '✂️ 框选'; clipBtn.style.cssText = 'padding:4px 8px;background:#007AFF;color:#fff;border:none;border-radius:12px;font-size:10px;font-weight:600;cursor:pointer;white-space:nowrap;'; clipBtn.onclick = function(e) { e.stopPropagation(); self._startClip(); }; actionCol.appendChild(clipBtn); bar.appendChild(actionCol); var rightCol = document.createElement('div'); rightCol.style.cssText = 'display:flex;flex-direction:column;gap:4px;'; var cancelModeBtn = document.createElement('button'); cancelModeBtn.textContent = '✖️ 取消选'; cancelModeBtn.style.cssText = 'padding:5px 10px;background:#8E8E93;color:#fff;border:none;border-radius:12px;font-size:10px;font-weight:600;cursor:pointer;white-space:nowrap;'; cancelModeBtn.onclick = function(e) { e.stopPropagation(); self._cancelMode = !self._cancelMode; self._updateUI(); if (self._cancelMode) showToast('✖️ 取消选模式:点击已选中的容器即可取消'); }; rightCol.appendChild(cancelModeBtn); this.cancelModeBtn = cancelModeBtn; var okBtn = document.createElement('button'); okBtn.textContent = '🚫 隐藏'; okBtn.style.cssText = 'padding:6px 10px;background:#FF3B30;color:#fff;border:none;border-radius:12px;font-size:11px;font-weight:600;cursor:pointer;opacity:0.35;white-space:nowrap;'; okBtn.disabled = true; okBtn.onclick = function(e) { e.stopPropagation(); self._confirm(); }; rightCol.appendChild(okBtn); this.okBtn = okBtn; bar.appendChild(rightCol); document.body.appendChild(bar); this._makeDraggable(topTip, null, [closeX]); this._makeDraggable(bar, hint, [upBtn, downBtn, layerBtn, clipBtn, cancelModeBtn, okBtn]); var onTap = function(e) { e.preventDefault(); e.stopPropagation(); var cx = e.clientX !== undefined ? e.clientX : (e.changedTouches && e.changedTouches[0] ? e.changedTouches[0].clientX : 0); var cy = e.clientY !== undefined ? e.clientY : (e.changedTouches && e.changedTouches[0] ? e.changedTouches[0].clientY : 0); self._pickAt(cx, cy); }; ov.addEventListener('touchstart', function(e) { if (e.touches && e.touches[0]) self._touchStartPos = { x: e.touches[0].clientX, y: e.touches[0].clientY }; }, { passive: true }); ov.addEventListener('touchend', function(e) { if (e.changedTouches && e.changedTouches[0]) { if (self._touchStartPos) { var dx = e.changedTouches[0].clientX - self._touchStartPos.x; var dy = e.changedTouches[0].clientY - self._touchStartPos.y; if (Math.sqrt(dx*dx + dy*dy) > 10) { self._touchStartPos = null; return; } } } onTap(e); self._touchStartPos = null; }, { passive: false }); ov.addEventListener('click', onTap); this._onScroll = function() { self._updateAllMarks(); }; this._onResize = function() { self._updateAllMarks(); }; window.addEventListener('scroll', this._onScroll, { passive: true }); window.addEventListener('resize', this._onResize); this.updateTimer = setInterval(function() { self._updateAllMarks(); }, 200); }, _startClip: function() { var self = this; if (self._clipBorder) { self._clipBorder.remove(); self._clipBorder = null; } self._clipRect = null; self.overlay.style.display = 'none'; self.statusBar.style.display = 'none'; self.topTip.style.display = 'none'; MaskSelector.start(function(rect) { self.overlay.style.display = ''; self.statusBar.style.display = ''; self.topTip.style.display = ''; if (rect.width < 20 || rect.height < 20) { showToast('区域太小,请重新点击框选'); return; } var scroll = getScroll(); self._clipRect = { left: rect.left + scroll.x, top: rect.top + scroll.y, right: rect.right + scroll.x, bottom: rect.bottom + scroll.y }; self._clipBorder = document.createElement('div'); self._clipBorder.id = '__ab_clip_border'; self._clipBorder.style.cssText = 'position:absolute;border:2px solid #FF3B30;background:rgba(255,59,48,0.05);z-index:' + (parseInt(MAX_Z)-1) + ';pointer-events:none;left:' + self._clipRect.left + 'px;top:' + self._clipRect.top + 'px;width:' + rect.width + 'px;height:' + rect.height + 'px;'; document.body.appendChild(self._clipBorder); self._autoAnchorInClip(); showToast('✅ 框选成功,可直接点"✨ 选层"批量选中框内同类元素'); self._updateUI(); }); }, _autoAnchorInClip: function() { if (!this._clipRect) return; var scroll = getScroll(); var c = this._clipRect; var screenLeft = c.left - scroll.x, screenTop = c.top - scroll.y; var screenRight = c.right - scroll.x, screenBottom = c.bottom - scroll.y; var points = [ [(screenLeft + screenRight) / 2, (screenTop + screenBottom) / 2], [screenLeft + 15, screenTop + 15], [screenRight - 15, screenTop + 15], [screenLeft + 15, screenBottom - 15], [screenRight - 15, screenBottom - 15], [(screenLeft + screenRight) / 2, screenTop + 15], [(screenLeft + screenRight) / 2, screenBottom - 15], [screenLeft + 15, (screenTop + screenBottom) / 2], [screenRight - 15, (screenTop + screenBottom) / 2] ]; var ov = this.overlay, bar = this.statusBar, tip = this.topTip; if (ov) ov.style.pointerEvents = 'none'; if (bar) bar.style.pointerEvents = 'none'; if (tip) tip.style.pointerEvents = 'none'; var el = null; for (var i = 0; i < points.length; i++) { var e = null; try { e = document.elementFromPoint(points[i][0], points[i][1]); } catch(ex) {} if (e && !isOurElement(e) && e !== document.documentElement && e !== document.body) { el = e; break; } } if (ov) ov.style.pointerEvents = ''; if (bar) bar.style.pointerEvents = ''; if (tip) tip.style.pointerEvents = ''; if (el) { this.focusEl = el; this.focusHistory = []; this._smartRoot = null; if (!this.marks.has(el)) this._addMark(el); } }, _pickAt: function(x, y) { if (this._clipRect) { var scroll = getScroll(); var docX = x + scroll.x, docY = y + scroll.y; var c = this._clipRect; if (docX < c.left || docX > c.right || docY < c.top || docY > c.bottom) { showToast('⚠️ 请在框选区域内点击'); return; } } var ov = this.overlay, bar = this.statusBar, tip = this.topTip; ov.style.pointerEvents = 'none'; if (bar) bar.style.pointerEvents = 'none'; if (tip) tip.style.pointerEvents = 'none'; var el = null; try { el = document.elementFromPoint(x, y); } catch(e) {} ov.style.pointerEvents = ''; if (bar) bar.style.pointerEvents = ''; if (tip) tip.style.pointerEvents = ''; if (!el) return; if (isOurElement(el)) return; if (el === document.documentElement) return; if (this._cancelMode) { var node = el, found = null; while (node && node !== document.body && node !== document.documentElement) { if (this.marks.has(node)) { found = node; break; } node = node.parentElement; } if (found) { this._removeMark(found); this._updateUI(); showToast('✖️ 已取消一个选中'); this._cancelMode = false; this._updateUI(); } else showToast('⚠️ 这里没有已选中的容器'); return; } this._smartRoot = null; if (this.marks.has(el)) { this._removeMark(el); this.focusEl = el; this.focusHistory = []; this._updateUI(); return; } this._addMark(el); this.focusEl = el; this.focusHistory = []; this._updateUI(); }, _smartSelectLayer: function() { if (!this.focusEl && this._clipRect) this._autoAnchorInClip(); if (!this.focusEl) { showToast('请先点选一个目标元素,或先框选一个区域'); return; } if (!this._smartRoot) this._smartRoot = this.focusEl; else { var parent = this._smartRoot.parentElement; if (parent && parent !== document.body && parent !== document.documentElement) this._smartRoot = parent; else { showToast('已到达最顶层,无法继续向上'); return; } } var tag = this._smartRoot.tagName.toLowerCase(); var targetDepth = getDepth(this._smartRoot); var count = 0; var all = document.querySelectorAll(tag); var scroll = getScroll(); var c = this._clipRect; for (var i = 0; i < all.length; i++) { var el = all[i]; if (el === document.body || el === document.documentElement) continue; if (isOurElement(el)) continue; if (this.marks.has(el)) continue; if (getDepth(el) === targetDepth) { var r = el.getBoundingClientRect(); if (r.width > 0 && r.height > 0) { if (c) { var elLeft = r.left + scroll.x, elTop = r.top + scroll.y; var elRight = elLeft + r.width, elBottom = elTop + r.height; if (elLeft < c.left || elRight > c.right || elTop < c.top || elBottom > c.bottom) continue; } this._addMark(el); count++; } } } this.focusEl = this._smartRoot; this._updateUI(); showToast('已选中 ' + tag + ' 层 (第' + targetDepth + '层, 框内新增 ' + count + ' 个)'); }, _addMark: function(el) { if (this.marks.has(el)) return; var parent = el.parentElement || document.body; var color = this._parentColors.get(parent); if (!color) { color = this._colorPool[this._colorIndex % this._colorPool.length]; this._colorIndex++; this._parentColors.set(parent, color); } el.dataset.abColor = color; var mk = document.createElement('div'); mk.id = '__ab_mark'; mk.style.cssText = 'position:fixed;pointer-events:none;display:none;border:2px dashed ' + color + ';background:' + color + '1A;box-sizing:border-box;z-index:' + (parseInt(MAX_Z) - 3) + ';'; document.body.appendChild(mk); this.marks.set(el, mk); this._positionMark(el, mk); }, _removeMark: function(el) { var mk = this.marks.get(el); if (mk) { mk.remove(); this.marks.delete(el); } }, _positionMark: function(el, mk) { if (!el.isConnected) { mk.style.display = 'none'; return; } var r = el.getBoundingClientRect(); if (r.width === 0 || r.height === 0) { mk.style.display = 'none'; return; } mk.style.display = 'block'; mk.style.left = r.left + 'px'; mk.style.top = r.top + 'px'; mk.style.width = r.width + 'px'; mk.style.height = r.height + 'px'; }, _updateAllMarks: function() { var self = this; var toRemove = []; this.marks.forEach(function(mk, el) { if (!el.isConnected) { toRemove.push(el); return; } self._positionMark(el, mk); }); for (var i = 0; i < toRemove.length; i++) this._removeMark(toRemove[i]); if (toRemove.length > 0) this._updateUI(); }, _updateUI: function() { var n = this.marks.size; this.hintEl.textContent = '已选 ' + n; if (n > 0) { this.okBtn.disabled = false; this.okBtn.style.opacity = '1'; this.okBtn.textContent = '🚫 隐藏 ' + n; } else { this.okBtn.disabled = true; this.okBtn.style.opacity = '0.35'; this.okBtn.textContent = '🚫 隐藏'; } if (this.cancelModeBtn) { if (this._cancelMode) { this.cancelModeBtn.style.background = '#FF9500'; this.cancelModeBtn.textContent = '✖️ 取消中'; } else { this.cancelModeBtn.style.background = '#8E8E93'; this.cancelModeBtn.textContent = '✖️ 取消选'; } } var hasFocus = this.focusEl && this.focusEl.isConnected; this.upBtn.disabled = !hasFocus; this.upBtn.style.opacity = hasFocus ? '1' : '0.35'; var canShrink = this.focusHistory.length > 0; this.downBtn.disabled = !canShrink; this.downBtn.style.opacity = canShrink ? '1' : '0.35'; }, _expand: function() { if (!this.focusEl || !this.focusEl.isConnected) return; this._smartRoot = null; var parent = this.focusEl.parentElement; if (!parent || parent === document.documentElement || isOurElement(parent)) return; if (this.marks.has(parent)) { this.hintEl.textContent = '父已选'; return; } var wasInMarks = this.marks.has(this.focusEl); if (wasInMarks) this._removeMark(this.focusEl); this._addMark(parent); this.focusHistory.push({ el: this.focusEl, wasInMarks: wasInMarks }); this.focusEl = parent; this._updateUI(); }, _shrink: function() { if (this.focusHistory.length === 0) return; this._smartRoot = null; var last = this.focusHistory.pop(); this._removeMark(this.focusEl); if (last.wasInMarks) this._addMark(last.el); this.focusEl = last.el; this._updateUI(); }, _collectGroupRules: function(rootEl) { var groupId = genGroupId(); var rules = []; var count = 0; var walk = function(el, isRoot) { if (count >= MAX_GROUP_CHILDREN) return; if (!el || el.nodeType !== 1) return; var tag = el.tagName.toLowerCase(); if (tag === 'script' || tag === 'style' || tag === 'link' || tag === 'meta' || tag === 'br' || tag === 'hr' || tag === 'noscript') return; var r = el.getBoundingClientRect(); if (r.width <= 0 || r.height <= 0) return; var fp = computeFingerprint(el); if (fp) { rules.push({ id: genPosId(), type: 'pos', groupId: groupId, isRoot: !!isRoot, tag: fp.tag, depth: fp.depth, path: fp.path, siblings: fp.siblings, w: fp.w, h: fp.h, ratio: fp.ratio, enabled: true, createdAt: Date.now() }); count++; } var kids = el.children; for (var i = 0; i < kids.length; i++) { if (count >= MAX_GROUP_CHILDREN) break; walk(kids[i], false); } }; walk(rootEl, true); var struct = computeStructure(rootEl, 2); return { groupId: groupId, rules: rules, struct: struct, rootTag: rootEl.tagName.toLowerCase() }; }, _confirm: function() { if (this.marks.size === 0) return; var addedRules = 0, addedSelector = 0; var allNewRules = []; this.marks.forEach(function(mk, el) { if (!el || !el.isConnected) return; var selector = generateSelector(el); if (selector) { var mc = 0; try { mc = document.querySelectorAll(selector).length; } catch(e) {} if (mc >= 1 && mc <= 100) { allNewRules.push({ id: genId(), type: 'hide', selector: selector, enabled: true, createdAt: Date.now() }); addedSelector++; } } var group = this._collectGroupRules(el); if (group.rules.length > 0) { group.rules[0].structFP = group.struct; group.rules[0].rootTag = group.rootTag; group.rules.forEach(function(r) { allNewRules.push(r); }); addedRules += group.rules.length; } }, this); if (allNewRules.length > 0) Storage.addMany(allNewRules); HideManager.apply(); this._close(); if (addedRules > 0 || addedSelector > 0) { var msg = '已记录:'; if (addedSelector > 0) msg += addedSelector + ' 条选择器 · '; if (addedRules > 0) msg += addedRules + ' 条位置'; showToast(msg); } }, _cancel: function() { this._close(); }, _close: function() { if (this.updateTimer) { clearInterval(this.updateTimer); this.updateTimer = null; } if (this._onScroll) { window.removeEventListener('scroll', this._onScroll); this._onScroll = null; } if (this._onResize) { window.removeEventListener('resize', this._onResize); this._onResize = null; } if (this.overlay) { this.overlay.remove(); this.overlay = null; } if (this.statusBar) { this.statusBar.remove(); this.statusBar = null; } if (this.topTip) { this.topTip.remove(); this.topTip = null; } if (this._clipBorder) { this._clipBorder.remove(); this._clipBorder = null; } if (this.marks) { this.marks.forEach(function(mk) { mk.remove(); }); this.marks.clear(); } this.focusEl = null; this.focusHistory = []; this._smartRoot = null; this._clipRect = null; this._cancelMode = false; this._parentColors.clear(); this._colorIndex = 0; this.active = false; } }; var MaskSelector = { overlay: null, box: null, active: false, drag: null, handles: {}, mode: 'doc', _onConfirm: null, start: function(onConfirm) { if (this.active) return; this.active = true; this._onConfirm = onConfirm || null; this._build(); }, _build: function() { var self = this; var ov = document.createElement('div'); ov.id = '__ab_mask_overlay'; ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.55);z-index:' + MAX_Z + ';touch-action:none;user-select:none;'; this.overlay = ov; var w = Math.min(300, window.innerWidth * 0.8); var h = Math.min(180, window.innerHeight * 0.35); var x = (window.innerWidth - w) / 2, y = (window.innerHeight - h) / 2; var box = document.createElement('div'); box.id = '__ab_mask_box'; box.style.cssText = 'position:absolute;border:2px dashed #007AFF;background:rgba(0,122,255,0.2);box-sizing:border-box;touch-action:none;left:' + x + 'px;top:' + y + 'px;width:' + w + 'px;height:' + h + 'px;'; this.box = box; ['nw','ne','sw','se'].forEach(function(pos) { var hd = document.createElement('div'); hd.dataset.handle = pos; var css = 'position:absolute;width:22px;height:22px;background:#007AFF;border:2px solid #fff;border-radius:50%;box-shadow:0 2px 6px rgba(0,0,0,0.3);z-index:2;'; if (pos === 'nw') css += 'left:-11px;top:-11px;'; if (pos === 'ne') css += 'right:-11px;top:-11px;'; if (pos === 'sw') css += 'left:-11px;bottom:-11px;'; if (pos === 'se') css += 'right:-11px;bottom:-11px;'; hd.style.cssText = css; box.appendChild(hd); self.handles[pos] = hd; }); var topH = document.createElement('div'); topH.dataset.handle = 'top'; topH.style.cssText = 'display:none;position:absolute;left:50%;margin-left:-30px;top:-10px;width:60px;height:20px;background:#007AFF;border:2px solid #fff;border-radius:10px;z-index:3;'; box.appendChild(topH); this.handles.top = topH; var botH = document.createElement('div'); botH.dataset.handle = 'bottom'; botH.style.cssText = 'display:none;position:absolute;left:50%;margin-left:-30px;bottom:-10px;width:60px;height:20px;background:#007AFF;border:2px solid #fff;border-radius:10px;z-index:3;'; box.appendChild(botH); this.handles.bottom = botH; var tip = document.createElement('div'); tip.id = '__ab_mask_tip'; tip.textContent = '拖框移动,拉四角改大小'; tip.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);color:#fff;padding:10px 18px;border-radius:20px;font-size:13px;font-family:system-ui;z-index:' + MAX_Z + ';white-space:nowrap;max-width:90vw;overflow:hidden;text-overflow:ellipsis;'; ov.appendChild(tip); this.tipEl = tip; var btnsWrap = document.createElement('div'); btnsWrap.id = '__ab_mask_btns'; btnsWrap.style.cssText = 'position:fixed;bottom:30px;left:50%;transform:translateX(-50%);display:flex;flex-direction:column;gap:10px;z-index:' + MAX_Z + ';align-items:center;'; var row1 = document.createElement('div'); row1.style.cssText = 'display:flex;gap:10px;justify-content:center;flex-wrap:wrap;'; var modeBtn = document.createElement('button'); modeBtn.textContent = '📌 跟随页面'; modeBtn.style.cssText = 'padding:10px 18px;background:#fff;color:#333;border:2px solid #ddd;border-radius:24px;font-size:13px;font-weight:600;cursor:pointer;'; modeBtn.onclick = function() { self._toggleMode(); }; row1.appendChild(modeBtn); this.modeBtn = modeBtn; var fillBtn = document.createElement('button'); fillBtn.textContent = '↔ 左右铺满'; fillBtn.style.cssText = 'padding:10px 18px;background:#34C759;color:#fff;border:none;border-radius:24px;font-size:13px;font-weight:600;cursor:pointer;'; fillBtn.onclick = function() { self._fillWidth(); }; row1.appendChild(fillBtn); btnsWrap.appendChild(row1); var row2 = document.createElement('div'); row2.style.cssText = 'display:flex;gap:10px;justify-content:center;'; var cancel = document.createElement('button'); cancel.textContent = '取消'; cancel.style.cssText = 'padding:12px 24px;background:#fff;color:#333;border:none;border-radius:24px;font-size:14px;font-weight:600;cursor:pointer;'; cancel.onclick = function() { self._cancel(); }; row2.appendChild(cancel); var ok = document.createElement('button'); ok.textContent = '确认'; ok.style.cssText = 'padding:12px 24px;background:#007AFF;color:#fff;border:none;border-radius:24px;font-size:14px;font-weight:600;cursor:pointer;'; ok.onclick = function() { self._confirm(); }; row2.appendChild(ok); btnsWrap.appendChild(row2); ov.appendChild(btnsWrap); ov.appendChild(box); document.body.appendChild(ov); this._bindDrag(box); this._applyModeStyle(); }, _toggleMode: function() { this.mode = this.mode === 'doc' ? 'fixed' : 'doc'; this._applyModeStyle(); }, _applyModeStyle: function() { var box = this.box, btn = this.modeBtn, tip = this.tipEl; if (!box || !btn) return; var hc = this.mode === 'fixed' ? '#FF9500' : '#007AFF'; if (this.mode === 'fixed') { box.style.borderColor = hc; box.style.background = 'rgba(255,149,0,0.2)'; btn.textContent = '📌 固定在屏幕'; btn.style.background = hc; btn.style.color = '#fff'; btn.style.borderColor = hc; if (tip) tip.textContent = '悬浮广告模式:固定屏幕不随滚动'; } else { box.style.borderColor = hc; box.style.background = 'rgba(0,122,255,0.2)'; btn.textContent = '📌 跟随页面'; btn.style.background = '#fff'; btn.style.color = '#333'; btn.style.borderColor = '#ddd'; if (tip) tip.textContent = '拖框移动,拉四角改大小'; } var self = this; ['nw','ne','sw','se','top','bottom'].forEach(function(p) { if (self.handles[p]) self.handles[p].style.background = hc; }); }, _fillWidth: function() { var rect = this.box.getBoundingClientRect(), vh = window.innerHeight; var top = Math.max(0, Math.min(vh - 40, rect.top)); var height = Math.max(40, Math.min(vh - top, rect.height)); this.box.style.left = '0px'; this.box.style.width = '100vw'; this.box.style.top = top + 'px'; this.box.style.height = height + 'px'; this.handles.top.style.display = 'block'; this.handles.bottom.style.display = 'block'; }, _bindDrag: function(box) { var self = this; var getP = function(e) { var t = e.touches && e.touches.length ? e.touches[0] : e; return { x: t.clientX, y: t.clientY }; }; var onStart = function(e) { var p = getP(e), target = e.target; var handle = target.dataset && target.dataset.handle; var rect = box.getBoundingClientRect(); if (handle === 'top') self.drag = { mode: 'rt', startY: p.y, top: rect.top, bottom: rect.bottom }; else if (handle === 'bottom') self.drag = { mode: 'rb', startY: p.y, top: rect.top, bottom: rect.bottom }; else if (handle && ['nw','ne','sw','se'].indexOf(handle) !== -1) { self.drag = { mode: 'rc', handle: handle, startX: p.x, startY: p.y, rect: { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom } }; } else if (target === box) { var isFull = Math.abs(rect.width - window.innerWidth) < 2; if (isFull) self.drag = { mode: 'mv', startY: p.y, top: rect.top }; else self.drag = { mode: 'm', startX: p.x, startY: p.y, rect: { left: rect.left, top: rect.top } }; } if (self.drag) { e.preventDefault(); e.stopPropagation(); } }; var onMove = function(e) { if (!self.drag) return; e.preventDefault(); var p = getP(e), vh = window.innerHeight, vw = window.innerWidth, minH = 40; if (self.drag.mode === 'mv') { var dy = p.y - self.drag.startY; var nt = Math.max(0, Math.min(vh - box.offsetHeight, self.drag.top + dy)); box.style.top = nt + 'px'; return; } var dx = p.x - self.drag.startX, dy = p.y - self.drag.startY; if (self.drag.mode === 'm') { var nl = Math.max(0, Math.min(vw - box.offsetWidth, self.drag.rect.left + dx)); var nt2 = Math.max(0, Math.min(vh - box.offsetHeight, self.drag.rect.top + dy)); box.style.left = nl + 'px'; box.style.top = nt2 + 'px'; } else if (self.drag.mode === 'rt') { var nt3 = Math.max(0, Math.min(self.drag.bottom - minH, self.drag.top + dy)); box.style.top = nt3 + 'px'; box.style.height = (self.drag.bottom - nt3) + 'px'; } else if (self.drag.mode === 'rb') { var nb = Math.min(vh, Math.max(self.drag.top + minH, self.drag.bottom + dy)); box.style.height = (nb - self.drag.top) + 'px'; } else if (self.drag.mode === 'rc') { var r = self.drag.rect; var left = r.left, top = r.top, right = r.right, bottom = r.bottom; if (self.drag.handle === 'nw') { left = r.left + dx; top = r.top + dy; } else if (self.drag.handle === 'ne') { right = r.right + dx; top = r.top + dy; } else if (self.drag.handle === 'sw') { left = r.left + dx; bottom = r.bottom + dy; } else if (self.drag.handle === 'se') { right = r.right + dx; bottom = r.bottom + dy; } if (right - left < minH) { if (self.drag.handle.indexOf('e') !== -1) right = left + minH; else left = right - minH; } if (bottom - top < minH) { if (self.drag.handle.indexOf('s') !== -1) bottom = top + minH; else top = bottom - minH; } left = Math.max(0, left); top = Math.max(0, top); right = Math.min(vw, right); bottom = Math.min(vh, bottom); box.style.left = left + 'px'; box.style.top = top + 'px'; box.style.width = (right - left) + 'px'; box.style.height = (bottom - top) + 'px'; } }; var onEnd = function() { self.drag = null; }; box.addEventListener('touchstart', onStart, { passive: false }); box.addEventListener('touchmove', onMove, { passive: false }); box.addEventListener('touchend', onEnd); box.addEventListener('touchcancel', onEnd); box.addEventListener('mousedown', onStart); document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onEnd); }, _confirm: function() { var rect = this.box.getBoundingClientRect(); this._close(); if (this._onConfirm) { this._onConfirm(rect); this._onConfirm = null; return; } var id = genId(); var mask; if (this.mode === 'fixed') { var vw = window.innerWidth, vh = window.innerHeight; mask = { id: id, type: 'mask', mode: 'fixed', x: rect.left / vw, y: rect.top / vh, w: rect.width / vw, h: rect.height / vh, enabled: true, createdAt: Date.now() }; } else { var ds = getDocSize(); var sx = window.scrollX || window.pageXOffset || 0; var sy = window.scrollY || window.pageYOffset || 0; mask = { id: id, type: 'mask', mode: 'doc', x: (rect.left + sx) / ds.w, y: (rect.top + sy) / ds.h, w: rect.width / ds.w, h: rect.height / ds.h, enabled: true, createdAt: Date.now() }; } Storage.add(mask); Renderer.render(); }, _cancel: function() { this._onConfirm = null; this._close(); }, _close: function() { if (this.overlay) { this.overlay.remove(); this.overlay = null; } this.box = null; this.handles = {}; this.tipEl = null; this.modeBtn = null; this.active = false; this.drag = null; this.mode = 'doc'; } }; var SimilarFinder = { computeHostMatch: function(sourceHost, pageCandidates) { var sourceRules = Storage.getHostRules(sourceHost); var posRules = sourceRules.filter(function(r) { return r.type === 'pos' && r.enabled !== false; }); if (posRules.length === 0) return null; var groups = {}; posRules.forEach(function(r) { var gid = r.groupId || ('legacy_' + r.id); if (!groups[gid]) groups[gid] = { rules: [], struct: null }; groups[gid].rules.push(r); if (r.structFP) groups[gid].struct = r.structFP; }); var groupResults = []; for (var gid in groups) { var g = groups[gid]; if (!g.struct) { var hits = 0; g.rules.forEach(function(r) { if (findElementByFingerprint(r)) hits++; }); groupResults.push({ groupId: gid, hits: hits, total: g.rules.length, rate: g.rules.length > 0 ? hits / g.rules.length : 0, structMatch: false }); continue; } var bestScore = 0; for (var i = 0; i < pageCandidates.length; i++) { var cand = pageCandidates[i]; var struct = computeStructure(cand, 2); if (!struct) continue; var s = compareStructure(g.struct, struct); if (s > bestScore) bestScore = s; } groupResults.push({ groupId: gid, hits: bestScore >= SIMILAR_THRESHOLD ? 1 : 0, total: 1, rate: bestScore, structMatch: true }); } var totalGroups = groupResults.length; var matchedGroups = groupResults.filter(function(g) { return g.rate >= SIMILAR_THRESHOLD; }).length; var avgRate = 0; if (totalGroups > 0) { var sum = 0; groupResults.forEach(function(g) { sum += g.rate; }); avgRate = sum / totalGroups; } return { host: sourceHost, totalGroups: totalGroups, matchedGroups: matchedGroups, avgRate: avgRate, groups: groupResults }; }, findSimilar: function() { var hosts = Storage.allHosts(); var results = []; var pageCandidates = _collectPageCandidates(); for (var i = 0; i < hosts.length; i++) { var h = hosts[i]; if (h === HOST) continue; var r = this.computeHostMatch(h, pageCandidates); if (r && r.matchedGroups >= SIMILAR_MIN_GROUPS) results.push(r); } results.sort(function(a, b) { return b.avgRate - a.avgRate; }); if (results.length > 0) results = [results[0]]; return results; }, applyFrom: function(sourceHost) { var sourceRules = Storage.getHostRules(sourceHost); if (sourceRules.length === 0) return 0; var current = Storage.list(); var added = 0; sourceRules.forEach(function(r) { var newRule = Object.assign({}, r, { id: (r.type === 'hide' ? genId() : genPosId()), enabled: true, createdAt: Date.now(), fromHost: sourceHost }); if (r.groupId) newRule.groupId = genGroupId(); current.push(newRule); added++; }); Storage.save(current); HideManager.apply(); return added; }, showPicker: function() { var self = this; var hostEl = document.createElement('div'); hostEl.id = '__ab_similar_host'; hostEl.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;z-index:' + MAX_Z + ';'; document.documentElement.appendChild(hostEl); var sh = hostEl.attachShadow({ mode: 'closed' }); var style = document.createElement('style'); style.textContent = '.mask{position:fixed;inset:0;background:rgba(0,0,0,0.45);display:flex;align-items:center;justify-content:center;font-family:system-ui,sans-serif;}' + '.panel{background:#fff;border-radius:16px;width:90vw;max-width:420px;max-height:80vh;padding:18px;box-sizing:border-box;display:flex;flex-direction:column;gap:12px;box-shadow:0 10px 40px rgba(0,0,0,0.25);}' + '.title{font-size:16px;font-weight:700;text-align:center;color:#1a1a1a;}' + '.subtitle{font-size:12px;color:#888;text-align:center;line-height:1.5;}' + '.list{background:#f8f8f8;border-radius:10px;padding:8px;max-height:45vh;overflow-y:auto;}' + '.item{background:#fff;border-radius:8px;padding:10px 12px;margin-bottom:6px;display:flex;align-items:center;gap:10px;cursor:pointer;}' + '.item:hover{background:#f5f5f5;}' + '.item .cb{width:18px;height:18px;flex-shrink:0;cursor:pointer;}' + '.item .info{flex:1;min-width:0;}' + '.item .h{font-size:13px;font-weight:600;color:#333;word-break:break-all;display:flex;align-items:center;gap:6px;flex-wrap:wrap;}' + '.item .h .remark-text{color:#0056b3;}' + '.item .edit-btn{font-size:11px;color:#007AFF;cursor:pointer;background:transparent;border:none;padding:2px 4px;flex-shrink:0;}' + '.item .meta{font-size:11px;color:#999;margin-top:2px;}' + '.empty{text-align:center;color:#bbb;padding:20px 0;font-size:13px;}' + '.btns{display:flex;gap:8px;}' + '.btns button{flex:1;padding:12px;border:none;border-radius:10px;font-size:14px;font-weight:600;cursor:pointer;}' + '.btn-cancel{background:#f0f2f5;color:#444;}' + '.btn-apply{background:#007AFF;color:#fff;}' + '.tip{font-size:11px;color:#aaa;text-align:center;line-height:1.4;}'; sh.appendChild(style); var mask = document.createElement('div'); mask.className = 'mask'; var panel = document.createElement('div'); panel.className = 'panel'; mask.appendChild(panel); sh.appendChild(mask); var title = document.createElement('div'); title.className = 'title'; title.textContent = '📋 选择模板 / 查找相似网站'; panel.appendChild(title); var subtitle = document.createElement('div'); subtitle.className = 'subtitle'; subtitle.textContent = '勾选要套用规则的网站(默认不勾选)'; panel.appendChild(subtitle); var listBox = document.createElement('div'); listBox.className = 'list'; panel.appendChild(listBox); var buildList = function() { var allHosts = Storage.allHosts(); var remarksMap = Storage.getAllRemarks(); var results = []; allHosts.forEach(function(h) { var rd = remarksMap[h]; var rt = '', ua = 0; if (typeof rd === 'string') rt = rd; else if (rd && typeof rd === 'object') { rt = rd.text || ''; ua = rd.updatedAt || 0; } results.push({ host: h, remark: rt, updatedAt: ua, isCurrent: h === HOST, matchedGroups: 0, totalGroups: 0, avgRate: 0, isCalculated: false }); }); results.sort(function(a, b) { if (a.isCurrent && !b.isCurrent) return -1; if (!a.isCurrent && b.isCurrent) return 1; if (a.remark && b.remark) { if (b.updatedAt !== a.updatedAt) return b.updatedAt - a.updatedAt; return a.remark.localeCompare(b.remark); } if (a.remark) return -1; if (b.remark) return 1; return a.host.localeCompare(b.host); }); return results; }; var renderList = function(dataList) { listBox.innerHTML = ''; if (dataList.length === 0) { var em = document.createElement('div'); em.className = 'empty'; em.textContent = '暂无已保存的网站模板'; listBox.appendChild(em); return; } dataList.forEach(function(r) { var item = document.createElement('div'); item.className = 'item'; if (r.isCurrent) { item.style.background = '#f0fbf5'; item.style.border = '1px solid #34C759'; } var cb = document.createElement('input'); cb.type = 'checkbox'; cb.className = 'cb'; cb.dataset.host = r.host; cb.checked = false; item.appendChild(cb); var info = document.createElement('div'); info.className = 'info'; var hDiv = document.createElement('div'); hDiv.className = 'h'; var nameSpan = document.createElement('span'); if (r.remark) { nameSpan.textContent = '📝 ' + r.remark; nameSpan.className = 'remark-text'; } else nameSpan.textContent = r.host; hDiv.appendChild(nameSpan); if (r.isCurrent) { var badge = document.createElement('span'); badge.textContent = '当前网站'; badge.style.cssText = 'font-size:10px;background:#34C759;color:#fff;padding:1px 6px;border-radius:8px;flex-shrink:0;font-weight:500;'; hDiv.appendChild(badge); } var editBtn = document.createElement('button'); editBtn.className = 'edit-btn'; editBtn.textContent = '✏️ 编辑'; editBtn.onclick = function(e) { e.stopPropagation(); e.preventDefault(); var current = r.remark || ''; var newRemark = prompt('为网站 ' + r.host + ' 设置备注名(留空则删除):', current); if (newRemark !== null) { Storage.setRemark(r.host, newRemark); showToast('✅ 备注已更新'); renderList(buildList()); } }; hDiv.appendChild(editBtn); info.appendChild(hDiv); var meta = document.createElement('div'); meta.className = 'meta'; var mt = r.host; if (r.isCalculated) { mt += ' · 相似度 ' + Math.round(r.avgRate * 100) + '%'; if (r.avgRate >= SIMILAR_THRESHOLD) mt += ' ✅'; } meta.textContent = mt; info.appendChild(meta); item.appendChild(info); item.addEventListener('click', function(e) { if (e.target !== cb && e.target !== editBtn) cb.checked = !cb.checked; }); listBox.appendChild(item); }); }; renderList(buildList()); var btns = document.createElement('div'); btns.className = 'btns'; btns.style.marginTop = '4px'; var closeBtn = document.createElement('button'); closeBtn.className = 'btn-cancel'; closeBtn.textContent = '关闭'; var scanBtn = document.createElement('button'); scanBtn.className = 'btn-cancel'; scanBtn.style.background = '#FF9500'; scanBtn.style.color = '#fff'; scanBtn.textContent = '🔍 自动扫描'; var applyBtn = document.createElement('button'); applyBtn.className = 'btn-apply'; applyBtn.textContent = '套用选中'; btns.appendChild(closeBtn); btns.appendChild(scanBtn); btns.appendChild(applyBtn); panel.appendChild(btns); var tip = document.createElement('div'); tip.className = 'tip'; tip.textContent = '提示:修改过备注的网站会自动排在最上面'; panel.appendChild(tip); var close = function() { if (hostEl.parentNode) hostEl.parentNode.removeChild(hostEl); }; closeBtn.onclick = close; scanBtn.onclick = function() { scanBtn.textContent = '⏳ 扫描中...'; scanBtn.disabled = true; setTimeout(function() { var sr = self.findSimilar(); var currentList = buildList(); var map = {}; sr.forEach(function(r) { var rd = Storage.getRemarkRaw(r.host); r.remark = (typeof rd === 'string' ? rd : (rd ? rd.text : '')) || ''; r.updatedAt = (rd && rd.updatedAt) ? rd.updatedAt : 0; r.isCalculated = true; r.isCurrent = false; map[r.host] = r; }); var merged = []; sr.forEach(function(r) { merged.push(r); }); currentList.forEach(function(r) { if (!map[r.host]) merged.push(r); }); merged.sort(function(a, b) { if (a.isCurrent && !b.isCurrent) return -1; if (!a.isCurrent && b.isCurrent) return 1; if (a.isCalculated && !b.isCalculated) return -1; if (!a.isCalculated && b.isCalculated) return 1; if (a.isCalculated && b.isCalculated) { if (b.avgRate !== a.avgRate) return b.avgRate - a.avgRate; } if (a.remark && b.remark) { if (b.updatedAt !== a.updatedAt) return b.updatedAt - a.updatedAt; return a.remark.localeCompare(b.remark); } if (a.remark) return -1; if (b.remark) return 1; return a.host.localeCompare(b.host); }); renderList(merged); scanBtn.style.display = 'none'; subtitle.textContent = '自动扫描完成,已按相似度排序,可直接勾选套用'; }, 50); }; applyBtn.onclick = function() { var checked = listBox.querySelectorAll('input.cb:checked'); if (checked.length === 0) { alert('请至少勾选一个网站'); return; } var total = 0, skippedSelf = false; for (var i = 0; i < checked.length; i++) { var targetHost = checked[i].dataset.host; if (targetHost === HOST) { skippedSelf = true; continue; } total += self.applyFrom(targetHost); } close(); if (skippedSelf) showToast('⚠️ 已跳过当前网站(不能套用自己)'); else if (total > 0) showToast('✅ 已套用 ' + total + ' 条规则'); }; mask.addEventListener('click', function(e) { if (e.target === mask) close(); }); } }; function _collectPageCandidates() { var all = document.querySelectorAll('div, section, article, aside, main'); var cands = []; for (var i = 0; i < all.length; i++) { var el = all[i]; if (el === document.body || el === document.documentElement) continue; if (isOurElement(el)) continue; var r = el.getBoundingClientRect(); if (r.width < 50 || r.height < 30) continue; if (r.width > window.innerWidth * 1.2) continue; cands.push(el); } return cands; } function openListPanel() { var old = document.getElementById('__ab_bl_host'); if (old && old.parentNode) old.parentNode.removeChild(old); var hostEl = document.createElement('div'); hostEl.id = '__ab_bl_host'; hostEl.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;z-index:' + MAX_Z + ';'; document.documentElement.appendChild(hostEl); var sh = hostEl.attachShadow({ mode: 'closed' }); var style = document.createElement('style'); style.textContent = '.mask{position:fixed;inset:0;background:rgba(0,0,0,0.45);display:flex;align-items:center;justify-content:center;font-family:system-ui,sans-serif;}' + '.panel{background:#fff;border-radius:16px;width:90vw;max-width:440px;max-height:85vh;padding:16px;box-sizing:border-box;display:flex;flex-direction:column;gap:10px;box-shadow:0 10px 40px rgba(0,0,0,0.25);overflow-y:auto;}' + '.title{font-size:16px;font-weight:700;text-align:center;color:#1a1a1a;}' + '.subtitle{font-size:12px;color:#888;text-align:center;line-height:1.5;}' + '.section-title{font-size:12px;color:#666;font-weight:600;padding:4px 2px 2px;}' + '.list{background:#f8f8f8;border-radius:10px;padding:6px;max-height:22vh;overflow-y:auto;}' + '.item{background:#fff;border-radius:8px;padding:7px 10px;margin-bottom:4px;display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12px;}' + '.item .key{flex:1;min-width:0;word-break:break-all;color:#333;}' + '.item .del{background:#ff4d4f;color:#fff;border:none;border-radius:6px;padding:3px 8px;font-size:11px;cursor:pointer;flex-shrink:0;}' + '.empty{text-align:center;color:#bbb;padding:14px 0;font-size:12px;}' + '.input-row{display:flex;gap:6px;}' + '.input-row input{flex:1;padding:9px 10px;border:1px solid #ddd;border-radius:8px;font-size:12px;outline:none;min-width:0;}' + '.input-row button{padding:9px 14px;background:#FF3B30;color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;flex-shrink:0;}' + '.input-row button.white-btn{background:#34C759;}' + '.btns{display:flex;gap:8px;}' + '.btns button{flex:1;padding:11px;border:none;border-radius:10px;font-size:13px;font-weight:600;cursor:pointer;}' + '.btn-cancel{background:#f0f2f5;color:#444;}' + '.btn-danger{background:#ff4d4f;color:#fff;}' + '.tip{font-size:11px;color:#aaa;text-align:center;line-height:1.5;}'; sh.appendChild(style); var mask = document.createElement('div'); mask.className = 'mask'; var panel = document.createElement('div'); panel.className = 'panel'; mask.appendChild(panel); sh.appendChild(mask); var title = document.createElement('div'); title.className = 'title'; title.textContent = '🚫 黑白名单管理'; panel.appendChild(title); var subtitle = document.createElement('div'); subtitle.className = 'subtitle'; subtitle.textContent = '白名单命中直接放行,黑名单命中直接拦下'; panel.appendChild(subtitle); var stW = document.createElement('div'); stW.className = 'section-title'; stW.textContent = '✅ 白名单(命中直接放行)'; panel.appendChild(stW); var whiteListBox = document.createElement('div'); whiteListBox.className = 'list'; panel.appendChild(whiteListBox); var whiteInputRow = document.createElement('div'); whiteInputRow.className = 'input-row'; var whiteInput = document.createElement('input'); whiteInput.type = 'text'; whiteInput.placeholder = '输入域名,逗号分隔可批量加'; whiteInputRow.appendChild(whiteInput); var whiteAddBtn = document.createElement('button'); whiteAddBtn.className = 'white-btn'; whiteAddBtn.textContent = '加入白名单'; whiteAddBtn.onclick = function() { var v = whiteInput.value; if (!v.trim()) return; var added = DomainList.addMany('white', v); if (added > 0) { showToast('白名单 +' + added); whiteInput.value = ''; renderWhite(); } else showToast('无新词可加(可能已存在)'); }; whiteInputRow.appendChild(whiteAddBtn); panel.appendChild(whiteInputRow); var stB = document.createElement('div'); stB.className = 'section-title'; stB.textContent = '🚫 黑名单(命中直接拦)'; panel.appendChild(stB); var blackListBox = document.createElement('div'); blackListBox.className = 'list'; panel.appendChild(blackListBox); var blackInputRow = document.createElement('div'); blackInputRow.className = 'input-row'; var blackInput = document.createElement('input'); blackInput.type = 'text'; blackInput.placeholder = '输入域名,逗号分隔可批量加'; blackInputRow.appendChild(blackInput); var blackAddBtn = document.createElement('button'); blackAddBtn.textContent = '加入黑名单'; blackAddBtn.onclick = function() { var v = blackInput.value; if (!v.trim()) return; var added = DomainList.addMany('black', v); if (added > 0) { showToast('黑名单 +' + added); blackInput.value = ''; renderBlack(); } else showToast('无新词可加(可能已存在)'); }; blackInputRow.appendChild(blackAddBtn); panel.appendChild(blackInputRow); var tip = document.createElement('div'); tip.className = 'tip'; tip.textContent = '加 22bet.com 会自动剥成 22bet,能匹配 22bet.net、a22bet.cc 等变体'; panel.appendChild(tip); var renderList = function(box, type) { box.innerHTML = ''; var list = DomainList.load(type); if (list.length === 0) { var em = document.createElement('div'); em.className = 'empty'; em.textContent = type === 'white' ? '暂无白名单' : '暂无黑名单'; box.appendChild(em); return; } list.slice().sort().forEach(function(key) { var item = document.createElement('div'); item.className = 'item'; var keySpan = document.createElement('span'); keySpan.className = 'key'; keySpan.textContent = key; item.appendChild(keySpan); var del = document.createElement('button'); del.className = 'del'; del.textContent = '删除'; del.onclick = function() { DomainList.remove(type, key); if (type === 'white') renderWhite(); else renderBlack(); }; item.appendChild(del); box.appendChild(item); }); }; var renderWhite = function() { renderList(whiteListBox, 'white'); }; var renderBlack = function() { renderList(blackListBox, 'black'); }; renderWhite(); renderBlack(); var btns = document.createElement('div'); btns.className = 'btns'; var clearWhiteBtn = document.createElement('button'); clearWhiteBtn.className = 'btn-cancel'; clearWhiteBtn.textContent = '清空白名单'; clearWhiteBtn.onclick = function() { if (confirm('确定清空所有白名单?')) { DomainList.clear('white'); renderWhite(); } }; btns.appendChild(clearWhiteBtn); var clearBlackBtn = document.createElement('button'); clearBlackBtn.className = 'btn-danger'; clearBlackBtn.textContent = '清空黑名单'; clearBlackBtn.onclick = function() { if (confirm('确定清空所有黑名单?')) { DomainList.clear('black'); renderBlack(); } }; btns.appendChild(clearBlackBtn); panel.appendChild(btns); var closeBtn = document.createElement('button'); closeBtn.className = 'btn-cancel'; closeBtn.style.cssText = 'width:100%;padding:11px;border:none;border-radius:10px;font-size:13px;font-weight:600;cursor:pointer;background:#f0f2f5;color:#444;'; closeBtn.textContent = '关闭'; closeBtn.onclick = function() { if (hostEl.parentNode) hostEl.parentNode.removeChild(hostEl); }; panel.appendChild(closeBtn); mask.addEventListener('click', function(e) { if (e.target === mask) closeBtn.onclick(); }); } var Panel = { host: null, shadow: null, open: function() { if (this.host) { this.host.remove(); this.host = null; this.shadow = null; } var host = document.createElement('div'); host.id = '__ab_panel_host'; host.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;z-index:' + MAX_Z + ';'; document.documentElement.appendChild(host); this.host = host; this.shadow = host.attachShadow({ mode: 'closed' }); var style = document.createElement('style'); style.textContent = '.mask{position:fixed;inset:0;background:rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;font-family:system-ui,sans-serif;}' + '.panel{background:#fff;border-radius:16px;width:92vw;max-width:460px;max-height:85vh;padding:16px;box-sizing:border-box;display:flex;flex-direction:column;gap:10px;box-shadow:0 10px 40px rgba(0,0,0,0.2);overflow-y:auto;}' + '.title{font-size:17px;font-weight:700;text-align:center;color:#1a1a1a;}' + '.info{font-size:12px;color:#888;text-align:center;display:flex;align-items:center;justify-content:center;gap:6px;}' + '.info .host{word-break:break-all;}' + '.info .edit-remark{background:transparent;border:none;color:#007AFF;cursor:pointer;font-size:14px;padding:2px 4px;flex-shrink:0;}' + '.section-title{font-size:12px;color:#666;font-weight:600;padding:6px 4px 2px;}' + '.list{background:#f8f8f8;border-radius:10px;padding:6px;max-height:22vh;overflow-y:auto;}' + '.group{background:#fff;border-radius:8px;padding:8px 10px;margin-bottom:6px;}' + '.group-head{font-size:12px;font-weight:600;color:#333;display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;gap:8px;}' + '.group-meta{font-size:10px;color:#999;font-weight:400;flex:1;min-width:0;word-break:break-all;}' + '.group-acts{display:flex;gap:4px;flex-shrink:0;}' + '.child{font-size:10px;color:#888;padding:2px 0 2px 10px;border-left:2px solid #eee;margin-left:4px;}' + '.item{background:#fff;border-radius:8px;padding:8px 10px;margin-bottom:4px;display:flex;align-items:center;justify-content:space-between;font-size:12px;gap:6px;}' + '.item-desc{color:#666;flex:1;min-width:0;word-break:break-all;font-size:11px;}' + '.item-actions{display:flex;gap:4px;flex-shrink:0;}' + '.btn{border:none;border-radius:8px;padding:8px 12px;font-size:12px;font-weight:600;cursor:pointer;background:#f0f2f5;color:#444;}' + '.btn.primary{background:#007AFF;color:#fff;}' + '.btn.hide-btn{background:#FF3B30;color:#fff;}' + '.btn.find-btn{background:#FF9500;color:#fff;}' + '.btn.danger{background:#ff4d4f;color:#fff;}' + '.btn.small{padding:4px 8px;font-size:10px;}' + '.empty{text-align:center;color:#bbb;padding:10px 0;font-size:11px;}' + '.btn-row{display:flex;gap:6px;}' + '.btn-row .btn{flex:1;padding:9px 4px;font-size:12px;}' + '.auto-tag{color:#007AFF;font-size:9px;background:#e6f0ff;padding:1px 4px;border-radius:4px;margin-right:2px;}'; this.shadow.appendChild(style); var mask = document.createElement('div'); mask.className = 'mask'; var self = this; mask.addEventListener('click', function(e) { if (e.target === mask) self.close(); }); var panel = document.createElement('div'); panel.className = 'panel'; mask.appendChild(panel); this._render(panel); this.shadow.appendChild(mask); }, close: function() { if (this.host) { this.host.remove(); this.host = null; this.shadow = null; } }, _render: function(panel) { var self = this; panel.innerHTML = ''; var list = Storage.list(); var hideRules = list.filter(function(m) { return m.type === 'hide'; }); var posRules = list.filter(function(m) { return m.type === 'pos'; }); var autoCount = hideRules.filter(function(m) { return m.auto; }).length; hideRules.sort(function(a, b) { return (b.createdAt || 0) - (a.createdAt || 0); }); var posGroups = {}; posRules.forEach(function(r) { var gid = r.groupId || ('legacy_' + r.id); if (!posGroups[gid]) posGroups[gid] = []; posGroups[gid].push(r); }); var groupIds = Object.keys(posGroups); groupIds.sort(function(a, b) { var tA = 0, tB = 0; posGroups[a].forEach(function(r) { if (r.createdAt > tA) tA = r.createdAt; }); posGroups[b].forEach(function(r) { if (r.createdAt > tB) tB = r.createdAt; }); return tB - tA; }); var title = document.createElement('div'); title.className = 'title'; title.textContent = '📋 管理广告内容'; panel.appendChild(title); var info = document.createElement('div'); info.className = 'info'; var remark = Storage.getRemark(HOST); var span = document.createElement('span'); span.className = 'host'; span.textContent = remark ? ('📝 ' + remark) : HOST; info.appendChild(span); var editBtn = document.createElement('button'); editBtn.className = 'edit-remark'; editBtn.textContent = '✏️'; editBtn.onclick = function() { var current = Storage.getRemark(HOST); var newRemark = prompt('为当前网站设置备注名(留空则删除):', current); if (newRemark !== null) { Storage.setRemark(HOST, newRemark); showToast('✅ 备注已更新'); self._render(panel); } }; info.appendChild(editBtn); panel.appendChild(info); var st0 = document.createElement('div'); st0.className = 'section-title'; st0.textContent = '📍 位置记忆 (' + groupIds.length + ' 组 / ' + posRules.length + ' 条)'; panel.appendChild(st0); var list0 = document.createElement('div'); list0.className = 'list'; if (groupIds.length === 0) { var em0 = document.createElement('div'); em0.className = 'empty'; em0.textContent = '暂无位置记忆'; list0.appendChild(em0); } else { groupIds.forEach(function(gid) { var rules = posGroups[gid]; var rootRule = rules.find(function(r) { return r.isRoot; }) || rules[0]; var groupEl = document.createElement('div'); groupEl.className = 'group'; var head = document.createElement('div'); head.className = 'group-head'; var meta = document.createElement('span'); meta.className = 'group-meta'; var desc = rootRule.tag + ' · ' + rootRule.w + '×' + rootRule.h; if (rootRule.path) { var p = rootRule.path; desc += ' · 路径[' + (p.length > 6 ? '…' + p.slice(-4).join('/') : p.join('/')) + ']'; } if (rootRule.fromHost) desc += ' · 来自 ' + rootRule.fromHost; meta.textContent = desc; head.appendChild(meta); var acts = document.createElement('div'); acts.className = 'group-acts'; var childBox = document.createElement('div'); childBox.style.display = 'none'; rules.forEach(function(r) { var row = document.createElement('div'); row.className = 'child'; row.textContent = (r.isRoot ? '★ ' : '· ') + r.tag + ' · ' + r.w + '×' + r.h; childBox.appendChild(row); }); var expandBtn = document.createElement('button'); expandBtn.className = 'btn small'; expandBtn.textContent = '展开(' + rules.length + ')'; var expanded = false; expandBtn.onclick = function() { expanded = !expanded; childBox.style.display = expanded ? 'block' : 'none'; expandBtn.textContent = (expanded ? '收起' : '展开') + '(' + rules.length + ')'; }; acts.appendChild(expandBtn); var delBtn = document.createElement('button'); delBtn.className = 'btn small danger'; delBtn.textContent = '删除'; delBtn.onclick = function() { Storage.removeGroup(gid); HideManager.apply(); self._render(panel); }; acts.appendChild(delBtn); head.appendChild(acts); groupEl.appendChild(head); groupEl.appendChild(childBox); list0.appendChild(groupEl); }); } panel.appendChild(list0); var st1 = document.createElement('div'); st1.className = 'section-title'; st1.textContent = '🚫 已隐藏广告 (' + hideRules.length + ',其中自动 ' + autoCount + ')'; panel.appendChild(st1); if (autoCount > 0) { var autoRow = document.createElement('div'); autoRow.style.cssText = 'display:flex;justify-content:flex-end;padding:0 4px;'; var clearAutoBtn = document.createElement('button'); clearAutoBtn.className = 'btn small danger'; clearAutoBtn.textContent = '清除所有自动规则 (' + autoCount + ')'; clearAutoBtn.onclick = function() { if (confirm('确定清除所有自动识别添加的规则?手动添加的不受影响。')) { Storage.removeAllAuto(); HideManager.apply(); self._render(panel); } }; autoRow.appendChild(clearAutoBtn); panel.appendChild(autoRow); } var list1 = document.createElement('div'); list1.className = 'list'; if (hideRules.length === 0) { var em1 = document.createElement('div'); em1.className = 'empty'; em1.textContent = '暂无隐藏规则'; list1.appendChild(em1); } else { hideRules.forEach(function(m) { var item = document.createElement('div'); item.className = 'item'; var desc = document.createElement('div'); desc.className = 'item-desc'; if (m.auto) { var tag = document.createElement('span'); tag.className = 'auto-tag'; tag.textContent = '🤖自动'; desc.appendChild(tag); } desc.appendChild(document.createTextNode(m.selector || '')); item.appendChild(desc); var acts = document.createElement('div'); acts.className = 'item-actions'; var recoverBtn = document.createElement('button'); recoverBtn.className = 'btn small'; recoverBtn.textContent = '恢复'; recoverBtn.style.background = '#34C759'; recoverBtn.style.color = '#fff'; recoverBtn.onclick = function() { Storage.remove(m.id); try { var hid = document.querySelectorAll('[data-ab-hide-rule="' + m.id + '"]'); for (var k = 0; k < hid.length; k++) { delete hid[k].dataset.abHideRule; delete hid[k].dataset.abHideMode; } } catch(e) {} var allRules = Storage.list(); var newRules = allRules.filter(function(r) { return !(r.type === 'pos' && Math.abs((r.createdAt || 0) - (m.createdAt || 0)) < 2000); }); Storage.save(newRules); HideManager.apply(); self._render(panel); }; acts.appendChild(recoverBtn); item.appendChild(acts); list1.appendChild(item); }); } panel.appendChild(list1); var btnRow = document.createElement('div'); btnRow.className = 'btn-row'; var clearBtn = document.createElement('button'); clearBtn.className = 'btn danger'; clearBtn.textContent = '清空全部'; clearBtn.onclick = function() { if (confirm('确定清空本网站所有规则?')) { Storage.clear(); Renderer.render(); try { var hid = document.querySelectorAll('[data-ab-hide-rule]'); for (var k = 0; k < hid.length; k++) { delete hid[k].dataset.abHideRule; delete hid[k].dataset.abHideMode; } } catch(e) {} self._render(panel); } }; btnRow.appendChild(clearBtn); var findBtn = document.createElement('button'); findBtn.className = 'btn find-btn'; findBtn.textContent = '查找相似网站'; findBtn.onclick = function() { self.close(); setTimeout(function() { SimilarFinder.showPicker(); }, 100); }; btnRow.appendChild(findBtn); var hideBtn = document.createElement('button'); hideBtn.className = 'btn hide-btn'; hideBtn.textContent = '点击隐藏'; hideBtn.onclick = function() { self.close(); setTimeout(function() { ClickSelector.start(); }, 100); }; btnRow.appendChild(hideBtn); panel.appendChild(btnRow); var closeBtn = document.createElement('button'); closeBtn.className = 'btn'; closeBtn.style.width = '100%'; closeBtn.textContent = '关闭'; closeBtn.onclick = function() { self.close(); }; panel.appendChild(closeBtn); } }; function startGlobalWatch() { window.addEventListener('resize', function() { Renderer.render(); }); window.addEventListener('orientationchange', function() { setTimeout(function() { Renderer.render(); }, 300); }); if (window.MutationObserver) { var obs = new MutationObserver(function() { HideManager.schedule(); }); obs.observe(document.documentElement, { childList: true, subtree: true }); } HideManager.startPeriodic(); } var _menuIds = {}; function cycleHideMode() { var cur = getHideMode(); var next = cur === 'smart' ? 'keep' : (cur === 'keep' ? 'clean' : 'smart'); setHideMode(next); showToast('广告类型已切换:' + getHideModeLabel(next) + '(' + getHideModeHint(next) + ')'); registerMenu(); HideManager.apply(); } function cycleAutoDetect() { var cur = getAutoDetectLevel(); var next = cur === 'off' ? 'conservative' : (cur === 'conservative' ? 'aggressive' : 'off'); setAutoDetectLevel(next); showToast('自动识别广告:' + getAutoDetectLabel(next) + '(' + getAutoDetectHint(next) + ')'); registerMenu(); if (next !== 'off') setTimeout(function() { runAutoDetect(); }, 200); } function registerMenu() { try { ['jump', 'mode', 'auto', 'list', 'blacklist'].forEach(function(k) { if (_menuIds[k] && typeof GM_unregisterMenuCommand === 'function') { try { GM_unregisterMenuCommand(_menuIds[k]); } catch(e) {} _menuIds[k] = null; } }); GM_registerMenuCommand('1 🚫 点击隐藏广告', function() { ClickSelector.start(); }); GM_registerMenuCommand('2 🔍 查找相似网站', function() { SimilarFinder.showPicker(); }); GM_registerMenuCommand('3 📋 管理广告内容', function() { Panel.open(); }); _menuIds.mode = GM_registerMenuCommand('4 ⚙️ 广告类型:' + getHideModeLabel(getHideMode()), function() { cycleHideMode(); }); _menuIds.auto = GM_registerMenuCommand('5 🤖 自动识别广告:' + getAutoDetectLabel(getAutoDetectLevel()), function() { cycleAutoDetect(); }); _menuIds.jump = GM_registerMenuCommand('6 🎯 跨域跳转拦截:' + JumpBlocker.getLabel(), function() { JumpBlocker.cycle(); }); _menuIds.list = GM_registerMenuCommand('7 🚫 黑白名单管理', function() { openListPanel(); }); } catch(e) {} } registerMenu(); function bootstrap() { StyleManager.ensure(); Renderer.ensure(); Renderer.render(); HideManager.apply(); startGlobalWatch(); setTimeout(function() { try { runAutoDetect(); } catch(e) {} }, 1200); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', bootstrap); } else { bootstrap(); } })();