Greasy Fork is available in English.
集成原生CSS极速注入、Shadow DOM隔离、DOM结构拦截、广告域封杀与正则文本拦截。新增免代码的「积木组合模式」,支持复杂逻辑条件过滤。
// ==UserScript==
// @name 网页元素屏蔽器
// @namespace http://tampermonkey.net/
// @version 0.6
// @description 集成原生CSS极速注入、Shadow DOM隔离、DOM结构拦截、广告域封杀与正则文本拦截。新增免代码的「积木组合模式」,支持复杂逻辑条件过滤。
// @author JerryChiang
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
'use strict';
/**
* 工具函数:防抖,用于优化高频触发事件
*/
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
/**
* 核心数据与配置管理模块
*/
class StorageManager {
constructor() {
this.domain = window.location.hostname;
this.flashList = GM_getValue('pro_blocker_flash_domains', {});
}
getData() {
return {
static: GM_getValue('blocks', {})[this.domain] || [],
dynamic: GM_getValue('dynamicBlocks', {})[this.domain] || [],
regex: GM_getValue('regexBlocks', {})[this.domain] || [],
attribute: GM_getValue('attrBlocks', {})[this.domain] || [],
structural: GM_getValue('structBlocks', {})[this.domain] || [],
complex: GM_getValue('complexBlocks', {})[this.domain] || [], // 新增:积木组合模式数据
config: GM_getValue('config', {})[this.domain] || { mode: 'auto' }
};
}
saveData(type, rules) {
const keyMap = {
'static': 'blocks',
'dynamic': 'dynamicBlocks',
'regex': 'regexBlocks',
'attribute': 'attrBlocks',
'structural': 'structBlocks',
'complex': 'complexBlocks'
};
const key = keyMap[type];
const allData = GM_getValue(key, {});
if (rules.length === 0) {
delete allData[this.domain];
} else {
allData[this.domain] = rules;
}
GM_setValue(key, allData);
if (type !== 'regex' && type !== 'complex') {
BlockEngine.applyCSSRules();
}
}
addRule(type, rule) {
const data = this.getData()[type];
const isDuplicate = data.some(item =>
(type === 'regex' && item.regex === rule.regex) ||
(type === 'static' && item.selector === rule.selector) ||
(type === 'dynamic' && item.className === rule.className) ||
(type === 'attribute' && item.attrSelector === rule.attrSelector) ||
(type === 'structural' && item.structSelector === rule.structSelector) ||
(type === 'complex' && JSON.stringify(item.conditions) === JSON.stringify(rule.conditions))
);
if (!isDuplicate) {
data.push(rule);
this.saveData(type, data);
}
}
removeRule(type, index) {
const data = this.getData()[type];
if (data[index]) {
data.splice(index, 1);
this.saveData(type, data);
}
}
clearDomain() {
['blocks', 'dynamicBlocks', 'regexBlocks', 'attrBlocks', 'structBlocks', 'complexBlocks', 'config'].forEach(key => {
const data = GM_getValue(key, {});
delete data[this.domain];
GM_setValue(key, data);
});
if (this.flashList[this.domain]) {
delete this.flashList[this.domain];
GM_setValue('pro_blocker_flash_domains', this.flashList);
}
}
markAsFlashing() {
if (!this.flashList[this.domain]) {
this.flashList[this.domain] = true;
GM_setValue('pro_blocker_flash_domains', this.flashList);
console.info(`[Pro Blocker] 检测到广告闪现,已将 ${this.domain} 加入极速预判名单。`);
}
}
toggleMode() {
const currentMode = this.getData().config.mode;
const nextMode = currentMode === 'auto' ? 'preemptive' : 'auto';
const allConfig = GM_getValue('config', {});
allConfig[this.domain] = { mode: nextMode };
GM_setValue('config', allConfig);
return nextMode;
}
}
const storage = new StorageManager();
/**
* 拦截引擎:负责底层 DOM 与 CSS 控制
*/
class BlockEngine {
static styleElementId = 'pro-blocker-core-css';
static fastInject() {
const data = storage.getData();
const isFlashingDomain = storage.flashList[storage.domain];
const isPreemptive = data.config.mode === 'preemptive';
if (isFlashingDomain || isPreemptive) {
this.applyCSSRules();
}
}
static applyCSSRules() {
const data = storage.getData();
let cssText = '';
const hideCSS = '{ display: none !important; opacity: 0 !important; visibility: hidden !important; pointer-events: none !important; z-index: -2147483648 !important; height: 0 !important; width: 0 !important; position: absolute !important; }\n';
data.static.forEach(r => r.selector && (cssText += `${r.selector} ${hideCSS}`));
data.dynamic.forEach(r => r.className && (cssText += `.${r.className} ${hideCSS}`));
data.attribute.forEach(r => r.attrSelector && (cssText += `${r.attrSelector} ${hideCSS}`));
data.structural.forEach(r => r.structSelector && (cssText += `${r.structSelector} ${hideCSS}`));
if (!cssText) return;
let styleEl = document.getElementById(this.styleElementId);
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = this.styleElementId;
const target = document.head || document.documentElement;
target.appendChild(styleEl);
}
styleEl.textContent = cssText;
}
static applyRegexRules(targetNode = document.body) {
const data = storage.getData();
if (!data.regex || data.regex.length === 0 || !targetNode) return;
data.regex.forEach(rule => {
try {
const regex = new RegExp(rule.regex);
const walker = document.createTreeWalker(targetNode, NodeFilter.SHOW_TEXT, null, false);
let node;
while ((node = walker.nextNode())) {
if (regex.test(node.textContent)) {
let element = node.parentElement;
for (let i = 0; i < rule.level; i++) {
if (element.parentElement && element.parentElement !== document.body) {
element = element.parentElement;
} else break;
}
if (element && element.style.display !== 'none') {
element.style.setProperty('display', 'none', 'important');
}
}
}
} catch (e) {
console.error('[Pro Blocker] 正则解析异常:', e);
}
});
}
// 新增:解析并应用积木组合模式规则
static applyComplexRules(targetNode = document.body) {
const data = storage.getData();
if (!data.complex || data.complex.length === 0 || !targetNode) return;
const root = targetNode.nodeType === Node.ELEMENT_NODE ? targetNode : targetNode.parentElement;
if (!root) return;
data.complex.forEach(rule => {
try {
// 性能优化:尝试提取前置基础选择器,避免全局扫描
let baseSelector = '*';
if (rule.logic === 'AND') {
let parts = [];
rule.conditions.forEach(c => {
if (c.type === 'class' && c.operator === 'contains' && /^[a-zA-Z0-9\-_]+$/.test(c.value)) parts.push(`.${c.value}`);
if (c.type === 'id' && c.operator === 'equals' && /^[a-zA-Z0-9\-_]+$/.test(c.value)) parts.push(`#${c.value}`);
});
if (parts.length > 0) baseSelector = parts.join('');
}
// 限制扫描的标签类型,排除脚本、样式及大范围的 body
const elements = baseSelector === '*'
? root.querySelectorAll('div, span, a, p, img, li, ul, iframe, section, article, aside')
: root.querySelectorAll(baseSelector);
elements.forEach(el => {
// 兜底机制:跳过文本量过大的结构层容器,防止误杀全站
if (baseSelector === '*' && el.textContent.length > 3000) return;
const results = rule.conditions.map(c => {
let val = '';
if (c.type === 'text') val = el.textContent || '';
else if (c.type === 'class') val = typeof el.className === 'string' ? el.className : '';
else if (c.type === 'id') val = el.id || '';
if (c.operator === 'contains') return val.includes(c.value);
if (c.operator === 'not_contains') return val !== '' && !val.includes(c.value);
if (c.operator === 'equals') return val.trim() === c.value.trim();
return false;
});
const isMatch = rule.logic === 'AND' ? results.every(r => r) : results.some(r => r);
if (isMatch) {
let target = el;
for (let i = 0; i < rule.level; i++) {
if (target.parentElement && target.parentElement !== document.body && target.parentElement !== document.documentElement) {
target = target.parentElement;
} else break;
}
if (target.style.display !== 'none') {
target.style.setProperty('display', 'none', 'important');
target.style.setProperty('opacity', '0', 'important');
}
}
});
} catch(e) {
console.error('[Pro Blocker] 积木规则执行错误:', e);
}
});
}
static startObserver() {
// 合并文本规则与积木规则的防抖更新
const debouncedDynamicApply = debounce(() => {
this.applyRegexRules();
this.applyComplexRules();
}, 300);
const observer = new MutationObserver((mutations) => {
let shouldCheck = false;
for (let mutation of mutations) {
if (mutation.addedNodes.length > 0) {
shouldCheck = true;
const styleEl = document.getElementById(this.styleElementId);
if (styleEl && document.body && styleEl.nextSibling) {
document.body.appendChild(styleEl);
storage.markAsFlashing();
}
break;
}
}
if (shouldCheck) debouncedDynamicApply();
});
window.addEventListener('DOMContentLoaded', () => {
this.applyCSSRules();
this.applyRegexRules();
this.applyComplexRules();
observer.observe(document.body || document.documentElement, { childList: true, subtree: true });
});
}
static generateOptimalSelector(element) {
if (element.id && !/^\d/.test(element.id) && !/[a-zA-Z0-9]{8,}/.test(element.id)) return `#${element.id}`;
let path = [];
while (element && element.nodeType === Node.ELEMENT_NODE && element.tagName.toLowerCase() !== 'body' && element.tagName.toLowerCase() !== 'html') {
let selector = element.tagName.toLowerCase();
if (element.className && typeof element.className === 'string') {
const classes = element.className.trim().split(/\s+/).filter(c => /^[a-zA-Z\-_]+$/.test(c) && !/[a-zA-Z0-9]{10,}/.test(c));
if (classes.length > 0) selector += '.' + classes.join('.');
}
let sibling = element, nth = 1;
while (sibling = sibling.previousElementSibling) {
if (sibling.tagName.toLowerCase() === element.tagName.toLowerCase()) nth++;
}
if (nth > 1) selector += `:nth-of-type(${nth})`;
path.unshift(selector);
element = element.parentElement;
}
return path.join(' > ');
}
static generateStructuralSelector(element) {
let path = [];
let current = element;
while (current && current.nodeType === Node.ELEMENT_NODE && current.tagName.toLowerCase() !== 'html') {
let tagName = current.tagName.toLowerCase();
if (tagName === 'body') {
path.unshift('body');
break;
}
if (current.id && !/^\d/.test(current.id) && !/[a-zA-Z0-9]{8,}/.test(current.id)) {
path.unshift(`#${current.id}`);
break;
}
let nth = 1, sibling = current.previousElementSibling;
while (sibling) { nth++; sibling = sibling.previousElementSibling; }
path.unshift(`${tagName}:nth-child(${nth})`);
current = current.parentElement;
}
return path.join(' > ');
}
static extractResourceDomain(element) {
let urls = [];
if (element.src) urls.push(element.src);
if (element.href) urls.push(element.href);
const mediaChild = element.querySelector('img, iframe, video, a');
if (mediaChild) {
if (mediaChild.src) urls.push(mediaChild.src);
if (mediaChild.href) urls.push(mediaChild.href);
}
for (let url of urls) {
try {
const urlObj = new URL(url);
if (urlObj.hostname && !urlObj.hostname.includes(window.location.hostname) && !url.startsWith('data:')) {
return { full: url, domain: urlObj.hostname };
}
} catch(e) {}
}
return null;
}
}
/**
* 用户交互界面:基于 Shadow DOM 隔离
*/
class UIManager {
constructor() {
const existingHost = document.getElementById('pro-blocker-ui-host');
if (existingHost) existingHost.remove();
this.shadowHost = document.createElement('div');
this.shadowHost.id = 'pro-blocker-ui-host';
this.shadowHost.style.cssText = 'position: fixed; z-index: 2147483647; top: 0; left: 0; width: 0; height: 0; overflow: visible;';
this.shadowRoot = this.shadowHost.attachShadow({ mode: 'closed' });
this.injectStyles();
document.documentElement.appendChild(this.shadowHost);
this.highlightEl = null;
this._previewAffectedElements = []; // 新增:用于存储规则预览时受影响的元素状态
this._handleMouseOver = this._handleMouseOver.bind(this);
this._handleClick = this._handleClick.bind(this);
}
injectStyles() {
const style = document.createElement('style');
style.textContent = `
:host { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
.panel {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.98); backdrop-filter: blur(12px);
border: 1px solid rgba(0,0,0,0.1); padding: 24px; border-radius: 12px;
box-shadow: 0 12px 48px rgba(0,0,0,0.25); width: 480px; max-height: 85vh; overflow-y: auto; color: #333;
}
h3 { margin-top: 0; font-size: 18px; font-weight: 600; color: #111; margin-bottom: 16px; border-bottom: 1px solid #eee; padding-bottom: 8px;}
p { font-size: 13px; margin: 0 0 12px 0; color: #555; line-height: 1.5; word-break: break-all; }
.code-box { background: #f4f4f4; border: 1px solid #ddd; padding: 6px; border-radius: 4px; font-family: monospace; font-size: 11px; margin-top: 4px; display: block;}
.btn-group { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
button {
padding: 8px 12px; border: none; border-radius: 6px; cursor: pointer;
font-size: 13px; font-weight: 500; transition: all 0.2s; flex: 1;
display: flex; align-items: center; justify-content: center;
}
button:hover { filter: brightness(0.9); transform: translateY(-1px); }
button:active { transform: translateY(0); }
.btn-primary { background: #007AFF; color: #fff; }
.btn-success { background: #34C759; color: #fff; }
.btn-danger { background: #FF3B30; color: #fff; }
.btn-warning { background: #FF9500; color: #fff; }
.btn-dark { background: #212529; color: #fff; }
.btn-info { background: #17a2b8; color: #fff; }
.btn-outline { background: transparent; border: 1px solid #ccc; color: #333; }
label { font-size: 13px; font-weight: 600; display: block; margin-bottom: 6px; color: #444; }
input[type="text"], input[type="number"], select {
width: 100%; padding: 10px; margin-bottom: 15px; border: 1px solid #ddd;
border-radius: 6px; box-sizing: border-box; outline: none; font-size: 14px;
transition: border-color 0.2s;
}
input[type="text"]:focus, input[type="number"]:focus, select:focus { border-color: #007AFF; }
.rule-list { list-style: none; padding: 0; margin: 0; }
.rule-item {
display: flex; justify-content: space-between; align-items: center;
padding: 10px; border-bottom: 1px solid #f0f0f0; background: #fafafa;
border-radius: 6px; margin-bottom: 8px; font-size: 12px; word-break: break-all;
}
.rule-content { flex: 1; padding-right: 10px; }
.tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: #eee; margin-right: 6px; font-weight: bold; }
.tag.attr { background: #E1BEE7; color: #4A148C; }
.tag.struct { background: #FFE082; color: #FF6F00; }
.tag.complex { background: #E3F2FD; color: #1565C0; }
.status-bar { padding: 10px; background: #f8f9fa; border-radius: 6px; margin-bottom: 15px; font-size: 12px; border: 1px solid #e9ecef; }
`;
this.shadowRoot.appendChild(style);
}
injectHighlightStyle() {
let style = document.getElementById('pro-blocker-highlight-style');
if (!style) {
style = document.createElement('style');
style.id = 'pro-blocker-highlight-style';
style.textContent = `
.pro-blocker-highlight {
outline: 3px solid #FF3B30 !important; outline-offset: -3px !important;
background-color: rgba(255, 59, 48, 0.15) !important; cursor: crosshair !important;
transition: outline 0.1s ease-in-out !important; box-shadow: 0 0 10px rgba(255,59,48,0.5) !important;
}
`;
document.head.appendChild(style);
}
}
makeDraggable(panel) {
const header = panel.querySelector('h3');
if (!header) return;
header.style.cursor = 'grab';
header.style.userSelect = 'none';
let isDragging = false;
let startX, startY, initialLeft, initialTop;
const onMouseDown = (e) => {
if (e.target !== header && !header.contains(e.target)) return;
isDragging = true;
header.style.cursor = 'grabbing';
const rect = panel.getBoundingClientRect();
panel.style.transform = 'none';
panel.style.left = rect.left + 'px';
panel.style.top = rect.top + 'px';
startX = e.clientX;
startY = e.clientY;
initialLeft = rect.left;
initialTop = rect.top;
e.preventDefault();
};
const onMouseMove = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
panel.style.left = `${initialLeft + dx}px`;
panel.style.top = `${initialTop + dy}px`;
};
const onMouseUp = () => {
if (isDragging) {
isDragging = false;
header.style.cursor = 'grab';
}
};
header.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
panel._cleanupDrag = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
}
startSelection() {
this.injectHighlightStyle();
document.body.addEventListener('mouseover', this._handleMouseOver, true);
document.body.addEventListener('click', this._handleClick, true);
document.body.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.stopSelection();
}, { once: true });
}
stopSelection() {
document.body.removeEventListener('mouseover', this._handleMouseOver, true);
document.body.removeEventListener('click', this._handleClick, true);
if (this.highlightEl) {
this.highlightEl.classList.remove('pro-blocker-highlight');
this.highlightEl = null;
}
}
_handleMouseOver(e) {
if (e.target.closest('#pro-blocker-ui-host')) return;
if (this.highlightEl) this.highlightEl.classList.remove('pro-blocker-highlight');
this.highlightEl = e.target;
this.highlightEl.classList.add('pro-blocker-highlight');
}
_handleClick(e) {
e.preventDefault(); e.stopPropagation();
if (e.target.closest('#pro-blocker-ui-host')) return;
this.stopSelection();
this.showActionPanel(e.target);
}
showActionPanel(element) {
this.clearPanel();
const panel = document.createElement('div');
panel.className = 'panel';
const selector = BlockEngine.generateOptimalSelector(element);
const structSelector = BlockEngine.generateStructuralSelector(element);
const resourceInfo = BlockEngine.extractResourceDomain(element);
const domainHtml = resourceInfo ? `<p style="color:#D32F2F; border-left: 3px solid #F44336; padding-left: 8px;"><strong>发现第三方资源域:</strong><br>${resourceInfo.domain}</p>` : '';
panel.innerHTML = `
<h3 title="按住可拖动窗口">确认屏蔽策略</h3>
<p><strong>常规语义路径:</strong><br><span class="code-box">${selector}</span></p>
<p><strong>物理结构路径:</strong><br><span class="code-box">${structSelector}</span></p>
${domainHtml}
<div class="btn-group">
<button class="btn-primary" id="btn-static">静态路径拦截</button>
<button class="btn-success" id="btn-dynamic">动态类名拦截</button>
</div>
<div class="btn-group">
<button class="btn-dark" id="btn-struct" style="flex:100%;">🎯 按物理结构拦截 (无视ID/类名随机化)</button>
</div>
${resourceInfo ? `
<div class="btn-group">
<button class="btn-danger" id="btn-domain" style="flex:100%; font-weight:bold;">🔥 彻底封杀该广告来源域名 (推荐)</button>
</div>` : ''}
<div class="btn-group" style="margin-top: 15px; border-top: 1px solid #eee; padding-top: 15px;">
<button class="btn-warning" id="btn-preview">🔍 预览效果</button>
<button class="btn-outline" id="btn-cancel">取消配置</button>
</div>
`;
this.makeDraggable(panel);
this.shadowRoot.appendChild(panel);
let isPreviewing = false;
panel.querySelector('#btn-static').addEventListener('click', () => {
storage.addRule('static', { selector: selector, type: 'static' });
this.clearPanel();
});
panel.querySelector('#btn-dynamic').addEventListener('click', () => {
const primaryClass = element.className.split(/\s+/)[0];
if (!primaryClass) return alert('当前元素无有效类名,请选择其他拦截方式。');
storage.addRule('dynamic', { className: primaryClass, type: 'dynamic' });
this.clearPanel();
});
panel.querySelector('#btn-struct').addEventListener('click', () => {
storage.addRule('structural', { structSelector: structSelector, type: 'structural' });
this.clearPanel();
});
if (resourceInfo) {
panel.querySelector('#btn-domain').addEventListener('click', () => {
const attrSel = `[src*="${resourceInfo.domain}"], [href*="${resourceInfo.domain}"], [data-src*="${resourceInfo.domain}"]`;
storage.addRule('attribute', { attrSelector: attrSel, type: 'attribute', domain: resourceInfo.domain });
this.clearPanel();
});
}
panel.querySelector('#btn-preview').addEventListener('click', (e) => {
isPreviewing = !isPreviewing;
element.style.setProperty('display', isPreviewing ? 'none' : '', 'important');
e.target.textContent = isPreviewing ? '👁 恢复显示' : '🔍 预览效果';
});
panel.querySelector('#btn-cancel').addEventListener('click', () => {
if (isPreviewing) element.style.display = '';
this.clearPanel();
});
}
showRegexPanel() {
this.clearPanel();
const panel = document.createElement('div');
panel.className = 'panel';
panel.innerHTML = `
<h3 title="按住可拖动窗口">添加拦截规则</h3>
<p>通过组合条件或正则表达式,实现对复杂动态广告的精准拦截。</p>
<label for="regex-mode">匹配模式</label>
<select id="regex-mode">
<option value="contains">基础文本模式 (包含指定字符即隐藏)</option>
<option value="builder" selected>积木组合模式 (免代码,支持多条件逻辑运算) ✨</option>
<option value="regex">正则表达式模式 (适合高级用户)</option>
</select>
<!-- 基础/正则模式 UI -->
<div id="standard-ui" style="display: none;">
<label for="regex-input">拦截内容</label>
<input type="text" id="regex-input" placeholder="输入要屏蔽的关键词或正则表达式片段..." />
</div>
<!-- 积木组合模式 UI -->
<div id="builder-ui" style="background: #f8f9fa; padding: 12px; border-radius: 8px; border: 1px solid #e9ecef; margin-bottom: 15px;">
<label style="margin-bottom: 8px;">总体逻辑网关</label>
<select id="builder-logic" style="margin-bottom: 12px;">
<option value="AND">满足以下【全部】条件才拦截 (AND)</option>
<option value="OR">满足以下【任意】条件即拦截 (OR)</option>
</select>
<label>详细拦截条件</label>
<div id="builder-conditions"></div>
<button id="btn-add-condition" class="btn-outline" style="width: 100%; margin-top: 8px; border-style: dashed; padding: 6px;">+ 添加新条件块</button>
</div>
<label for="regex-level">向上隐藏层级 (0为仅隐藏自身,1为隐藏直接父级节点)</label>
<input type="number" id="regex-level" value="0" min="0" max="10" />
<div class="btn-group" style="margin-top: 15px;">
<button class="btn-warning" id="btn-preview-regex">🔍 预览效果</button>
<button class="btn-primary" id="btn-save-regex">保存并应用</button>
<button class="btn-outline" id="btn-close-regex">取消</button>
</div>
`;
this.makeDraggable(panel);
this.shadowRoot.appendChild(panel);
const modeSelect = panel.querySelector('#regex-mode');
const standardUI = panel.querySelector('#standard-ui');
const builderUI = panel.querySelector('#builder-ui');
const conditionsContainer = panel.querySelector('#builder-conditions');
// 模式切换联动
modeSelect.addEventListener('change', (e) => {
if (e.target.value === 'builder') {
standardUI.style.display = 'none';
builderUI.style.display = 'block';
if (conditionsContainer.children.length === 0) addConditionRow();
} else {
standardUI.style.display = 'block';
builderUI.style.display = 'none';
}
});
// 添加单个积木条件
const addConditionRow = () => {
const row = document.createElement('div');
row.className = 'condition-row';
row.style.cssText = 'display: flex; gap: 6px; margin-bottom: 8px; align-items: center;';
row.innerHTML = `
<select class="cond-type" style="width: 35%; margin: 0; padding: 8px;">
<option value="text">元素文本</option>
<option value="class">类名(Class)</option>
<option value="id">标识符(ID)</option>
</select>
<select class="cond-op" style="width: 25%; margin: 0; padding: 8px;">
<option value="contains">包含</option>
<option value="equals">等于</option>
<option value="not_contains">不包含</option>
</select>
<input type="text" class="cond-val" style="flex: 1; margin: 0; padding: 8px;" placeholder="设定值..." />
<button class="btn-danger btn-remove-cond" style="flex: none; width: 32px; padding: 0; min-width: 32px; height: 35px;">✕</button>
`;
row.querySelector('.btn-remove-cond').addEventListener('click', () => {
row.remove();
if (conditionsContainer.children.length === 0) addConditionRow(); // 保证至少有一个条件
});
conditionsContainer.appendChild(row);
};
// 初始化呈现一行默认积木
addConditionRow();
panel.querySelector('#btn-add-condition').addEventListener('click', addConditionRow);
let isPreviewing = false;
const resetPreview = () => {
if (isPreviewing) {
this._previewAffectedElements.forEach(item => {
if (item.el) {
item.el.style.display = item.origDisplay;
item.el.style.opacity = item.origOpacity;
}
});
this._previewAffectedElements = [];
isPreviewing = false;
const previewBtn = panel.querySelector('#btn-preview-regex');
if (previewBtn) previewBtn.textContent = '🔍 预览效果';
}
};
// 监听面板内输入变化,自动取消已失效的预览状态
panel.addEventListener('input', resetPreview);
panel.addEventListener('change', resetPreview);
panel.querySelector('#btn-preview-regex').addEventListener('click', (e) => {
if (isPreviewing) {
resetPreview();
return;
}
const mode = modeSelect.value;
const level = parseInt(panel.querySelector('#regex-level').value, 10);
this._previewAffectedElements = [];
if (mode === 'builder') {
const logic = panel.querySelector('#builder-logic').value;
const rows = conditionsContainer.querySelectorAll('.condition-row');
const conditions = [];
let isValueMissing = false;
rows.forEach(row => {
const type = row.querySelector('.cond-type').value;
const op = row.querySelector('.cond-op').value;
const val = row.querySelector('.cond-val').value.trim();
if (!val) isValueMissing = true;
conditions.push({ type, operator: op, value: val });
});
if (isValueMissing || conditions.length === 0) {
alert('规则校验失败:请完整填写所有积木条件的值再进行预览。');
return;
}
// 性能优化前置:尝试提取基础选择器
let baseSelector = '*';
if (logic === 'AND') {
let parts = [];
conditions.forEach(c => {
if (c.type === 'class' && c.operator === 'contains' && /^[a-zA-Z0-9\-_]+$/.test(c.value)) parts.push(`.${c.value}`);
if (c.type === 'id' && c.operator === 'equals' && /^[a-zA-Z0-9\-_]+$/.test(c.value)) parts.push(`#${c.value}`);
});
if (parts.length > 0) baseSelector = parts.join('');
}
const root = document.body;
const elements = baseSelector === '*'
? root.querySelectorAll('div, span, a, p, img, li, ul, iframe, section, article, aside')
: root.querySelectorAll(baseSelector);
elements.forEach(el => {
if (baseSelector === '*' && el.textContent.length > 3000) return;
const results = conditions.map(c => {
let val = '';
if (c.type === 'text') val = el.textContent || '';
else if (c.type === 'class') val = typeof el.className === 'string' ? el.className : '';
else if (c.type === 'id') val = el.id || '';
if (c.operator === 'contains') return val.includes(c.value);
if (c.operator === 'not_contains') return val !== '' && !val.includes(c.value);
if (c.operator === 'equals') return val.trim() === c.value.trim();
return false;
});
const isMatch = logic === 'AND' ? results.every(r => r) : results.some(r => r);
if (isMatch) {
let target = el;
for (let i = 0; i < level; i++) {
if (target.parentElement && target.parentElement !== document.body && target.parentElement !== document.documentElement) {
target = target.parentElement;
} else break;
}
if (target.style.display !== 'none') {
this._previewAffectedElements.push({ el: target, origDisplay: target.style.display, origOpacity: target.style.opacity });
target.style.setProperty('display', 'none', 'important');
target.style.setProperty('opacity', '0', 'important');
}
}
});
} else {
const text = panel.querySelector('#regex-input').value.trim();
if (!text) {
alert('规则校验失败:请输入有效的匹配内容再进行预览。');
return;
}
let regexRule = text;
if (mode === 'contains') {
const escapedText = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
regexRule = `.*${escapedText}.*`;
}
try {
const regex = new RegExp(regexRule);
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
let node;
while ((node = walker.nextNode())) {
if (regex.test(node.textContent)) {
let target = node.parentElement;
for (let i = 0; i < level; i++) {
if (target.parentElement && target.parentElement !== document.body) {
target = target.parentElement;
} else break;
}
if (target && target.style.display !== 'none') {
this._previewAffectedElements.push({ el: target, origDisplay: target.style.display, origOpacity: target.style.opacity });
target.style.setProperty('display', 'none', 'important');
target.style.setProperty('opacity', '0', 'important');
}
}
}
} catch (err) {
alert('规则校验失败:正则表达式存在语法错误。');
return;
}
}
isPreviewing = true;
e.target.textContent = '👁 恢复显示';
});
panel.querySelector('#btn-save-regex').addEventListener('click', () => {
const mode = modeSelect.value;
const level = parseInt(panel.querySelector('#regex-level').value, 10);
if (mode === 'builder') {
const logic = panel.querySelector('#builder-logic').value;
const rows = conditionsContainer.querySelectorAll('.condition-row');
const conditions = [];
let isValueMissing = false;
rows.forEach(row => {
const type = row.querySelector('.cond-type').value;
const op = row.querySelector('.cond-op').value;
const val = row.querySelector('.cond-val').value.trim();
if (!val) isValueMissing = true;
conditions.push({ type, operator: op, value: val });
});
if (isValueMissing || conditions.length === 0) {
alert('校验失败:请完整填写所有积木条件的值。');
return;
}
storage.addRule('complex', { logic, conditions, level, type: 'complex' });
BlockEngine.applyComplexRules();
} else {
const text = panel.querySelector('#regex-input').value.trim();
if (!text) {
alert('校验失败:请输入有效的匹配内容。');
return;
}
let regexRule = text;
if (mode === 'contains') {
const escapedText = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
regexRule = `.*${escapedText}.*`;
}
storage.addRule('regex', { regex: regexRule, level: level, type: 'regex' });
BlockEngine.applyRegexRules();
}
this.clearPanel();
});
panel.querySelector('#btn-close-regex').addEventListener('click', () => this.clearPanel());
}
showManager() {
this.clearPanel();
const panel = document.createElement('div');
panel.className = 'panel';
const data = storage.getData();
let rulesHTML = '<ul class="rule-list">';
const renderItem = (type, content, index, tagClass = '') => `
<li class="rule-item">
<div class="rule-content">
<span class="tag ${tagClass}">${type}</span> ${content}
</div>
<button class="btn-danger btn-delete" style="flex:none; width:60px; padding: 4px;" data-type="${type}" data-index="${index}">删除</button>
</li>
`;
data.static.forEach((r, i) => rulesHTML += renderItem('静态', r.selector, i));
data.dynamic.forEach((r, i) => rulesHTML += renderItem('动态', `类名: ${r.className}`, i));
data.regex.forEach((r, i) => rulesHTML += renderItem('正则', `匹配: ${r.regex} (层级: ${r.level})`, i));
data.attribute.forEach((r, i) => rulesHTML += renderItem('域封杀', `阻断: ${r.domain}`, i, 'attr'));
data.structural.forEach((r, i) => rulesHTML += renderItem('位置', r.structSelector, i, 'struct'));
// 渲染积木组合规则
data.complex.forEach((r, i) => {
const formatOp = (op) => op === 'contains' ? '包含' : (op === 'equals' ? '等于' : '不包含');
const formatType = (t) => t === 'text' ? '文本' : (t === 'class' ? '类名' : 'ID');
const condText = r.conditions.map(c => `[${formatType(c.type)} ${formatOp(c.operator)} "${c.value}"]`).join(` <span style="color:#007AFF; font-weight:bold;">${r.logic}</span> `);
rulesHTML += renderItem('积木', `${condText} (层级: ${r.level})`, i, 'complex');
});
if (rulesHTML === '<ul class="rule-list">') {
rulesHTML = '<p style="text-align:center; color:#999; margin: 20px 0;">当前域名暂无屏蔽规则</p>';
} else { rulesHTML += '</ul>'; }
const modeText = data.config.mode === 'preemptive' ? '强制极速预判 (防闪现)' : '智能自动';
const flashStatus = storage.flashList[storage.domain] ? '<span style="color:red; font-weight:bold;">已记录闪现特征,系统采用极速注入</span>' : '运行良好';
panel.innerHTML = `
<h3 title="按住可拖动窗口">规则与防御管理 (${storage.domain})</h3>
<div class="status-bar">
<div><strong>防御策略:</strong> ${modeText}</div>
<div style="margin-top:4px;"><strong>系统评估:</strong> ${flashStatus}</div>
</div>
<div style="max-height: 250px; overflow-y: auto; margin-bottom: 15px; border: 1px solid #eee; border-radius: 6px;">
${rulesHTML}
</div>
<div class="btn-group">
<button class="btn-info" id="btn-toggle-mode" style="flex:100%;">🚀 切换防御策略</button>
</div>
<div class="btn-group" style="margin-top: 10px;">
<button class="btn-outline" id="btn-clear-all">清除所有规则</button>
<button class="btn-primary" id="btn-close-manager">完成</button>
</div>
`;
this.makeDraggable(panel);
this.shadowRoot.appendChild(panel);
panel.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', (e) => {
const typeMap = { '静态': 'static', '动态': 'dynamic', '正则': 'regex', '域封杀': 'attribute', '位置': 'structural', '积木': 'complex' };
const type = typeMap[e.target.getAttribute('data-type')];
const index = parseInt(e.target.getAttribute('data-index'), 10);
storage.removeRule(type, index);
this.showManager();
if (type !== 'regex' && type !== 'complex') BlockEngine.applyCSSRules();
else window.location.reload();
});
});
panel.querySelector('#btn-toggle-mode').addEventListener('click', () => {
const newMode = storage.toggleMode();
alert(`策略已调整为:${newMode === 'preemptive' ? '极速预判模式' : '智能自动模式'}\n页面即将刷新以应用变更配置。`);
window.location.reload();
});
panel.querySelector('#btn-clear-all').addEventListener('click', () => {
if (confirm('警告:此操作将清空当前域名下的所有拦截规则和配置,且不可恢复。确认继续?')) {
storage.clearDomain();
window.location.reload();
}
});
panel.querySelector('#btn-close-manager').addEventListener('click', () => this.clearPanel());
}
clearPanel() {
if (this._previewAffectedElements && this._previewAffectedElements.length > 0) {
this._previewAffectedElements.forEach(item => {
if (item.el) {
item.el.style.display = item.origDisplay;
item.el.style.opacity = item.origOpacity;
}
});
this._previewAffectedElements = [];
}
const oldPanel = this.shadowRoot.querySelector('.panel');
if (oldPanel && typeof oldPanel._cleanupDrag === 'function') {
oldPanel._cleanupDrag();
}
this.shadowRoot.innerHTML = '';
this.injectStyles();
}
}
// ================= 初始化与执行流 =================
BlockEngine.fastInject();
BlockEngine.startObserver();
if (window.self === window.top) {
let uiInstance = null;
function getUI() {
if (!uiInstance) uiInstance = new UIManager();
return uiInstance;
}
GM_registerMenuCommand('🖱 手动选择屏蔽元素', () => getUI().startSelection());
GM_registerMenuCommand('📝 添加文本/正则/积木规则', () => getUI().showRegexPanel());
GM_registerMenuCommand('⚙️ 管理规则与防御策略', () => getUI().showManager());
}
})();