您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
A powerful content blocker that helps protect you from inappropriate websites. Features: Auto-detection of adult content, Multi-language support, Smart scoring system, Safe browsing protection.
当前为
// ==UserScript== // @name Porn Blocker | 色情内容过滤器 // @name:en Porn Blocker // @name:zh-CN 色情内容过滤器 // @name:zh-TW 色情內容過濾器 // @name:zh-HK 色情內容過濾器 // @name:ja アダルトコンテンツブロッカー // @name:ko 성인 컨텐츠 차단기 // @name:ru Блокировщик порнографии // @namespace https://noctiro.moe // @version 2.1.1 // @description A powerful content blocker that helps protect you from inappropriate websites. Features: Auto-detection of adult content, Multi-language support, Smart scoring system, Safe browsing protection. // @description:en A powerful content blocker that helps protect you from inappropriate websites. Features: Auto-detection of adult content, Multi-language support, Smart scoring system, Safe browsing protection. // @description:zh-CN 强大的网页过滤工具,帮助你远离不良网站。功能特点:智能检测色情内容,多语言支持,评分系统,安全浏览保护,支持自定义过滤规则。为了更好的网络环境,从我做起。 // @description:zh-TW 強大的網頁過濾工具,幫助你遠離不良網站。功能特點:智能檢測色情內容,多語言支持,評分系統,安全瀏覽保護,支持自定義過濾規則。為了更好的網絡環境,從我做起。 // @description:zh-HK 強大的網頁過濾工具,幫助你遠離不良網站。功能特點:智能檢測色情內容,多語言支持,評分系統,安全瀏覽保護,支持自定義過濾規則。為了更好的網絡環境,從我做起。 // @description:ja アダルトコンテンツを自動的にブロックする強力なツールです。機能:アダルトコンテンツの自動検出、多言語対応、スコアリングシステム、カスタマイズ可能なフィルタリング。より良いインターネット環境のために。 // @description:ko 성인 컨텐츠를 자동으로 차단하는 강력한 도구입니다. 기능: 성인 컨텐츠 자동 감지, 다국어 지원, 점수 시스템, 안전 브라우징 보호, 맞춤형 필터링 규칙。 // @description:ru Мощный инструмент для блокировки неприемлемого контента. Функции: автоматическое определение, многоязычная поддержка, система оценки, настраиваемые правила фильтрации。 // @license Apache-2.0 // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCI+CiA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZCIgeDE9IjAlIiB5MT0iMCUiIHgyPSIxMDAlIiB5Mj0iMTAwJSI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiNGRjhBNjUiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIxMDAiIHN0b3AtY29sb3I9IiNGRkQ1NEYiLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiA8L2RlZnM+CiA8cmVjdCB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIGZpbGw9InVybCgjZ3JhZCkiIHJ4PSI4IiByeT0iOCI+PC9yZWN0PgogPHRleHQgeD0iMzIiIHk9IjQ2IiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMzYiIGZvbnQtd2VpZ2h0PSJib2xkIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjRkZGRkZGIj5SMTg8L3RleHQ+CiA8bGluZSB4MT0iMTIiIHkxPSIxMiIgeDI9IjUyIiB5Mj0iNTIiIHN0cm9rZT0iI0QzMkYyRiIgc3Ryb2tlLXdpZHRoPSI2IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPC9zdmc+ // @match *://*/* // @run-at document-start // @run-at document-end // @run-at document-idle // @grant none // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function () { 'use strict'; // 多语言支持 const i18n = { 'en': { title: '🚫 Access Denied', message: 'This page contains content harmful to your well-being.', redirect: 'You will be guided away in <span class="countdown">4</span> seconds…', footer: 'Protect your mind · Steer clear of harmful sights' }, 'zh-CN': { title: '🚫 访问受限', message: '此页面含有不良信息,损害身心健康。', redirect: '<span class="countdown">4</span> 秒后为您引导离开…', footer: '悦享健康 · 远离不良诱惑' }, 'zh-TW': { title: '🚫 存取受限', message: '此頁面內容有礙身心健康。', redirect: '<span class="countdown">4</span> 秒後跳轉離開…', footer: '珍愛生命 · 遠離不良資訊' }, 'zh-HK': { title: '🚫 存取受限', message: '此網頁含有不良資訊,恐損身心健康。', redirect: '<span class="countdown">4</span> 秒後自動跳轉…', footer: '強健身心 · 遠離有害內容' }, 'ja': { title: '🚫 アクセス制限', message: 'このページには心身に有害な情報が含まれています。', redirect: '<span class="countdown">4</span> 秒後、自動的に別のページへ移動します…', footer: '身心を大切に · 有害サイトは遠ざけよう' }, 'ko': { title: '🚫 접근 제한', message: '이 페이지는 신체와 정신에 해로운 정보를 포함하고 있습니다.', redirect: '4초 후에 자동으로 다른 페이지로 이동합니다…', footer: '건강을 지키세요 · 유해 콘텐츠는 멀리' }, 'ru': { title: '🚫 Доступ ограничен', message: 'На этой странице содержатся материалы, вредные для вашего здоровья.', redirect: 'Через <span class="countdown">4</span> секунды вы будете перенаправлены…', footer: 'Берегите здоровье · Избегайте вредного' } }; // 获取用户语言 const getUserLanguage = () => { const lang = navigator.language || navigator.userLanguage; // 检查完整语言代码 if (i18n[lang]) return lang; // 处理中文的特殊情况 if (lang.startsWith('zh')) { const region = lang.toLowerCase(); if (region.includes('tw') || region.includes('hant')) return 'zh-TW'; if (region.includes('hk')) return 'zh-HK'; return 'zh-CN'; } // 检查简单语言代码 const shortLang = lang.split('-')[0]; if (i18n[shortLang]) return shortLang; return 'en'; }; // 浏览器检测函数 const getBrowserType = () => { const ua = navigator.userAgent.toLowerCase(); // 1. User-Agent Client Hints (modern Chromium-based browsers) if (navigator.userAgentData && Array.isArray(navigator.userAgentData.brands)) { const brands = navigator.userAgentData.brands.map(b => b.brand.toLowerCase()); if (brands.includes('microsoft edge')) return 'edge'; if (brands.includes('google chrome')) return 'chrome'; if (brands.includes('brave')) return 'brave'; if (brands.includes('vivaldi')) return 'vivaldi'; if (brands.includes('opera') || brands.includes('opr')) return 'opera'; if (brands.includes('arc')) return 'arc'; // If none of the above, it's some other Chromium variant if (brands.includes('chromium')) return 'chromium'; } // 2. Arc-specific CSS variable detection (Arc adds --arc-palette-background) if (window.getComputedStyle(document.documentElement) .getPropertyValue('--arc-palette-background')) { return 'arc'; } // 3. Traditional UA substring checks for non-Chromium or unhinted cases if (ua.includes('ucbrowser')) return 'uc'; if (ua.includes('qqbrowser')) return 'qq'; if (ua.includes('2345explorer')) return '2345'; if (ua.includes('360') || ua.includes('qihu')) return '360'; if (ua.includes('maxthon')) return 'maxthon'; if (ua.includes('via')) return 'via'; if (ua.includes('waterfox')) return 'waterfox'; if (ua.includes('palemoon')) return 'palemoon'; if (ua.includes('torbrowser') || (ua.includes('firefox') && ua.includes('tor'))) return 'tor'; if (ua.includes('focus')) return 'firefox-focus'; if (ua.includes('firefox')) return 'firefox'; if (ua.includes('edg/')) return 'edge'; // Edge Chromium if (ua.includes('opr/') || ua.includes('opera')) return 'opera'; if (ua.includes('brave')) return 'brave'; if (ua.includes('vivaldi')) return 'vivaldi'; if (ua.includes('yabrowser')) return 'yandex'; if (ua.includes('chrome')) return 'chrome'; if (ua.includes('safari') && !ua.includes('chrome')) return 'safari'; return 'other'; }; // 获取浏览器主页URL const getHomePageUrl = () => { switch (getBrowserType()) { case 'firefox': return 'about:home'; case 'tor': return 'about:home'; // Tor uses Firefox's UI case 'waterfox': return 'about:home'; // Waterfox mirrors Firefox case 'palemoon': return 'about:home'; // Pale Moon custom but similar case 'chrome': return 'chrome://newtab'; case 'edge': return 'edge://newtab'; case 'safari': return 'topsites://'; case 'opera': return 'opera://startpage'; case 'brave': return 'brave://newtab'; case 'vivaldi': return 'vivaldi://newtab'; case 'yandex': return 'yandex://newtab'; case 'arc': return 'arc://start'; // Arc’s default start page case 'via': return 'via://home'; // Fallbacks for lesser-known or legacy browsers case 'uc': return 'ucenterhome://'; case 'qq': return 'qbrowser://home'; case '360': return 'se://newtab'; case 'maxthon': return 'mx://newtab'; case '2345': return '2345explorer://newtab'; default: return 'about:blank'; } }; // ----------------- 配置项 ----------------- const config = { // ================== 域名专用黑名单词汇 ================== domainKeywords: { // 常见成人网站域名关键词(权重4) 'pornhub': 4, 'xvideo': 4, 'redtube': 4, 'xnxx': 4, 'xhamster': 4, '4tube': 4, 'youporn': 4, 'spankbang': 4, 'myfreecams': 4, 'missav': 4, 'rule34': 4, 'youjizz': 4, 'onlyfans': 4, 'paidaa': 4, 'haijiao': 4, // 核心违规词(权重3-4) 'porn': 3, 'nsfw': 3, 'hentai': 3, 'incest': 4, 'rape': 4, 'childporn': 4, // 身体部位关键词(权重2) 'pussy': 2, 'cock': 2, 'dick': 2, 'boobs': 2, 'tits': 2, 'ass': 2, 'beaver': 1, // 特定群体(权重2-3) 'cuckold': 3, 'virgin': 2, 'luoli': 2, 'gay': 2, // 具体违规行为(权重2-3) 'blowjob': 3, 'creampie': 2, 'bdsm': 2, 'masturbat': 2, 'handjob': 3, 'footjob': 3, 'rimjob': 3, // 其他相关词汇(权重1-2) 'camgirl': 2, 'nude': 3, 'naked': 3, 'upskirt': 2, // 特定地区成人站点域名特征(权重4) 'jav': 4, // 域名变体检测(权重3) 'p0rn': 3, 'pr0n': 3, 'pron': 3, 's3x': 3, 'sexx': 3, }, // ================== 内容检测关键词 ================== contentKeywords: { // 核心违规词(权重3-4)- 严格边界检测 '\\b(?:po*r*n|pr[o0]n)\\b': 3, // porn及其变体 'nsfw': 3, '\\bhentai\\b': 3, '\\binces*t\\b': 4, '\\br[a@]pe\\b': 4, '(?:child|kid|teen)(?:po*r*n)': 4, '海角社区': 4, // 身体部位关键词(权重2)- 优化边界和上下文检测 'puss(?:y|ies)\\b': 2, '\\bco*ck(?:s)?(?!tail|roach|pit|er)\\b': 2, // 排除cocktail等 '\\bdick(?:s)?(?!ens|tionary|tate)\\b': 2, // 排除dickens等 '\\bb[o0]{2,}bs?\\b': 2, '\\btits?\\b': 2, '(?<!cl|gl|gr|br|m|b|h)ass(?:es)?(?!ign|et|ist|ume|ess|ert|embl|oci|ault|essment|emble|ume|uming|ured)\\b': 2, // 优化ass检测 '\\bbeaver(?!s\\s+dam)\\b': 1, // 排除海狸相关 // 特定群体(权重2-3)- 上下文敏感 '\\bteen(?!age\\s+mutant)\\b': 3, // 排除 Teenage Mutant '\\bsis(?!ter|temp)\\b': 2, // 排除 sister, system '\\bmilfs?\\b': 2, '\\bcuck[o0]ld\\b': 3, '\\bvirgins?(?!ia|\\s+islands?)\\b': 2, // 排除地名 'lu[o0]li': 2, '\\bg[a@]y(?!lord|le|le\\s+storm)\\b': 2, // 排除人名 // 具体违规行为(权重2-3)- 严格检测 '\\banal(?!ys[it]|og)\\b': 3, // 排除analysis等 '\\bbl[o0]w\\s*j[o0]b\\b': 3, 'cream\\s*pie(?!\\s+recipe)\\b': 2, // 排除食物相关 '\\bbdsm\\b': 2, 'masturba?t(?:ion|e|ing)\\b': 2, '\\bhand\\s*j[o0]b\\b': 3, '\\bf[o0]{2}t\\s*j[o0]b\\b': 3, '\\brim\\s*j[o0]b\\b': 3, // 新增违规行为(权重2-3) '\\bstr[i1]p(?:p(?:er|ing)|tease)\\b': 3, '\\bh[o0]{2}ker(?:s)?\\b': 3, 'pr[o0]st[i1]tut(?:e|ion)\\b': 3, 'b[o0]{2}ty(?!\\s+call)\\b': 2, // 排除 booty call 'sp[a@]nk(?:ing)?\\b': 2, 'deepthroat': 3, 'bukk[a@]ke': 3, 'org(?:y|ies)\\b': 3, 'gangbang': 3, 'thr[e3]{2}s[o0]me': 2, 'c[u|v]msh[o0]t': 3, 'f[e3]tish': 2, // 其他相关词汇(权重1-2)- 上下文敏感 '\\bcamgirls?\\b': 2, '\\bwebcam(?!era)\\b': 2, // 排除webcamera '\\ble[a@]ked(?!\\s+(?:pipe|gas|oil))\\b': 2, // 排除工程相关 '\\bf[a@]p(?:p(?:ing)?)?\\b': 2, '\\ber[o0]tic(?!a\\s+books?)\\b': 1, // 排除文学相关 '\\besc[o0]rt(?!\\s+mission)\\b': 3, // 排除游戏相关 '\\bnude(?!\\s+color)\\b': 3, // 排除色彩相关 'n[a@]ked(?!\\s+juice)\\b': 3, // 排除品牌 '\\bupskirt\\b': 2, '\\b[o0]nlyfans\\b': 3, // 多语言支持 (按原有配置) '情色': 3, '成人': 3, '做爱': 4, 'セックス': 3, 'エロ': 3, '淫': 4, 'секс': 3, 'порн': 3, '性爱': 3, '無修正': 3, 'ポルノ': 3, 'порно': 3, '色情': 3, '骚': 1, '啪啪': 2, '自慰': 3, '口交': 3, '肛交': 3, '吞精': 3, '诱惑': 1, '全裸': 3, '内射': 3, '乳交': 3, '射精': 3, '反差': 0.5, '调教': 1.5, '性交': 3, '性奴': 3, '高潮': 0.3, '白虎': 0.8, '少女': 0.1, '女友': 0.1, '狂操': 3, '捆绑': 0.1, '约炮': 3, '鸡吧': 3, '鸡巴': 3, '阴茎': 1, '阴道': 1, '女优': 3, '裸体': 3, '男优': 3, '乱伦': 3, '偷情': 2, '母狗': 3, '内射': 4, '喷水': 0.8, '潮吹': 3, '轮奸': 2, '少妇': 2, '熟女': 2, // 新增中文词汇(更细致的分级) '色情': 3, '情色': 3, '黄色': 2, '淫(?:秽|荡|乱|贱|液|穴|水)': 4, '肉(?:棒|根|穴|缝|臀|奶|体|欲)': 3, '(?:巨|大|小|翘|白|圆|肥)(?:乳|臀|胸)': 2, '(?:舔|添|吸|吮|插|干|操|草|日|艹)(?:穴|逼|屄|阴|蜜|菊|屌|鸡|肉)': 4, '(?:销|骚|浪|淫)(?:魂|女|货|逼|贱|荡)': 3, // 新增日语词汇 'オナニー': 3, // 自慰 '手コキ': 3, // 手淫 'パイズリ': 3, // 乳交 '中出し': 4, // 中出 '素人': 2, // 素人 'アヘ顔': 3, // 阿黑颜 '痴女': 3, // 痴女 '処女': 2, // 处女 // 新增韩语词汇 '섹스': 3, // 性 '야동': 3, // 成人视频 '자위': 2, // 自慰 '음란': 3, // 淫乱 '성인': 2, // 成人 '누드': 2, // 裸体 // 新兴词汇、变体、谐音、emoji(权重2-4) // 英文新兴变体 'lewd': 2, 'fap': 2, 'simp': 2, 'thicc': 2, 'bussy': 2, 'sloot': 2, 'nut': 2, 'noods': 2, 'lewdies': 2, 'camwhore': 3, 'onlyfams': 3, 'fansly': 3, 'sugardaddy': 2, 'sugarbaby': 2, 'egirl': 2, 'eboy': 2, // 谐音与变体 'pron': 3, 'prawn': 2, 'p0rn': 3, 'p*rn': 3, 's3x': 3, 'shex': 2, 'seggs': 2, 's3ggs': 2, 'sx': 2, 'lo1i': 3, 'l0li': 3, 'loli': 3, 'shota': 3, 'sh0ta': 3, 'sh0t4': 3, '萝莉': 3, '正太': 3, // emoji '🍆': 0.5, '👅': 0.5, '👙': 0.5, '👠': 0.5, '👄': 0.5, '🔞': 2, // 新兴中文网络词 '涩涩': 2, '涩图': 2, '涩气': 2, '涩女': 2, '涩男': 2, '涩会': 2, '涩图群': 2, '涩图包': 2, '涩图控': 2, '色批': 2, '色图': 2, '色气': 2, '色女': 2, '色男': 2, '色会': 2, '色图群': 2, '色图包': 2, '色图控': 2, '约p': 3, '约啪': 3, '约炮': 3, '约x': 3, '约会炮': 3, '约会啪': 3, '约会p': 3, '约会x': 3, // 日语新兴词 'エッチ': 2, 'えっち': 2, 'えちえち': 2, 'えち': 2, 'エロい': 2, 'エロ画像': 2, 'エロ動画': 2, // 韩语新兴词 '야짤': 2, '야사': 2, '야한': 2, '야동': 3, '야설': 2, }, // ================== 白名单减分规则 ================== whitelist: { // 强豁免词(权重-30) 'edu': -30, 'health': -30, 'medical': -30, 'science': -30, 'gov': -30, 'org': -30, 'official': -30, // 常用场景豁免(权重-15) 'academy': -15, 'clinic': -15, 'therapy': -15, 'university': -4, 'research': -15, 'news': -15, 'dictionary': -15, 'library': -15, 'museum': -15, // 动物/自然相关(权重-1) 'animal': -4, 'zoo': -1, 'cat': -1, 'dog': -1, 'pet': -6, 'bird': -1, 'vet': -1, // 科技类(权重-5) 'tech': -5, 'cloud': -5, 'software': -5, 'cyber': -3, // 支持正则和后缀 '/\\.edu$/': -30, '/\\.gov$/': -30, '/\\.org$/': -30, '/\\.ac\\./': -20, // 在线聊天/论坛豁免 'telegram': -20, 'discord': -20, 'slack': -20, 'line': -20, 'whatsapp': -20, 'skype': -20, 'teams': -20, 'twitter': -20, 'facebook': -20, 'forum': -10, 'bbs': -10, 'reddit': -10, 'tieba': -10, '知乎': -10, '豆瓣': -10, 'quora': -10, 'stack': -10, 'stackoverflow': -10, }, // ================== 阈值配置 ================== thresholds: { // 总分触发阈值(建议3~4) block: 3, // URL路径加分阈值 path: 2, // 进行白名单减分的最低阈值 whitelist: 2 }, // ================== 域名正则表达式规则 ================== domainPatterns: [ /^mogu\d+\.[a-z]{2,3}$/i, /^mg\d+\.app$/i, ], // ================== 内容检测规则 ================== // 需要内容检测的域名规则 contentCheckDomains: [ /\d{3}[a-z]{2,3}/i, /[a-z]{2,3}\d{2,3}/i, /[a-z]{1,3}\d{1,3}[a-z]{1,3}/i, // 海角社区 /^[a-z0-9]{0,5}(\.[a-z0-5]{0,5})?h[a-z0-5]{0,5}j[a-z0-5]{0,5}(\.[a-z]{0,5})?/i, ], // 内容检测相关配置 contentCheck: { // 成人内容分数 adultContentThreshold: 25, suspiciousTagNames: [ // 主要内容区域 'article', 'main', 'section', 'content', // 文本块 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', // 列表和表格 'li', 'td', 'th', 'figcaption', // 链接和按钮文本 'a', 'button', // 通用容器 'div.content', 'div.text', 'div.description', 'span.text', 'span.content' ], // 文本节点最小长度 textNodeMinLength: 5, // 防抖等待时间(毫秒) debounceWait: 1000, // 观察者最大运行时间(毫秒) observerTimeout: 30000, // 添加局部内容检测配置 localizedCheck: { // 单个元素的内容阈值,超过此值才会影响整体评分 elementThreshold: 8, // 需要触发的违规元素数量 minViolationCount: 3, // 违规内容占总内容的比例阈值 violationRatio: 0.3, // 排除检测的元素 excludeSelectors: [ '.comment', '.reply', '.user-content', '[id*="comment"]', '[class*="comment"]', '[id*="reply"]', '[class*="reply"]', '.social-feed', '.user-post' ], // 高风险元素选择器(权重更高) highRiskSelectors: [ 'article', 'main', '.main-content', '.article-content', '.post-content' ] } }, // ================== 搜索引擎白名单 ================== searchEngines: [ 'google.com', 'bing.com', 'baidu.com', 'yahoo.com', 'duckduckgo.com', 'yandex.com', 'so.com', 'sogou.com', 'sm.cn', 'search.brave.com', 'ecosia.org', 'qwant.com', 'searx', 'startpage.com', 'you.com', 'naver.com', 'daum.net', 'ask.com', 'aol.com', 'dogpile.com', 'gibiru.com', 'mojeek.com', 'metager.org', 'swisscows.com', 'search.com', 'search.yahoo.com', 'search.aol.com', 'search.naver.com', 'search.daum.net', 'search.sogou.com', 'search.sm.cn', 'search.yandex.com', 'search.ecosia.org', 'search.qwant.com', 'search.searx', 'search.startpage.com', 'search.you.com', 'search.brave.com', 'search.metager.org', 'search.swisscows.com' ], // ================== 误判正则白名单 ================== falsePositiveRegexList: [ /cocktail/i, /class/i, /classic/i, /associate/i, /assignment/i, /passage/i, /passion/i, /pass/i, /mass/i, /massive/i, /dickens/i, /dickinson/i, /analysis/i, /analogy/i, /webcamera/i, /booty call/i, /virginia/i, /virgin islands/i, /teenage mutant/i, /system/i, /sister/i, /mission/i, /juice/i, /color/i, /pipe/i, /gas/i, /oil/i, /roach/i, /pit/i, /er/i, /tate/i, /ens/i, /dictionary/i, /museum/i, /library/i, /academy/i, /clinic/i, /therapy/i, /research/i, /news/i, /animal/i, /zoo/i, /cat/i, /dog/i, /pet/i, /bird/i, /vet/i, /tech/i, /cloud/i, /software/i, /cyber/i, /gov/i, /org/i, /official/i, /edu/i, /health/i, /medical/i, /science/i ], }; // ----------------- 工具函数 ----------------- function isWhitelisted(text) { for (const [w, wv] of Object.entries(config.whitelist)) { if (w.startsWith('/') && w.endsWith('/')) { if (new RegExp(w.slice(1, -1), 'i').test(text)) return wv; } else if (text.endsWith(w)) { return wv; } else if (text.match(new RegExp(`\\b${w}\\b`, 'i'))) { return wv; } } return 0; } const compiledContentRegexes = Object.entries(config.contentKeywords).map(([k, v]) => ({ regex: new RegExp(k, 'i'), weight: v, raw: k })); // 只检测可见文本 function getAllVisibleText(element) { if (!element) return ""; const textSet = new Set(); try { const walker = document.createTreeWalker( element, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { const parent = node.parentElement; if (!parent || /^(SCRIPT|STYLE|NOSCRIPT|IFRAME|META|LINK)$/i.test(parent.tagName) || parent.hidden || getComputedStyle(parent).display === 'none' || getComputedStyle(parent).visibility === 'hidden' || getComputedStyle(parent).opacity === '0') { return NodeFilter.FILTER_REJECT; } const text = node.textContent.trim(); if (!text || text.length < config.contentCheck.textNodeMinLength) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } } ); let node; while (node = walker.nextNode()) { textSet.add(node.textContent.trim()); } } catch (e) { } return Array.from(textSet).join(' '); } // 评分函数 function calculateScore(text, isDomain = false) { if (!text) return 0; const white = isWhitelisted(text); if (white) return white; for (const fp of config.falsePositiveRegexList) { if (fp.test(text)) return 0; } let score = 0; if (isDomain) { for (const [k, v] of Object.entries(config.domainKeywords)) { const reg = new RegExp(`\\b${k}\\b`, 'gi'); const matches = text.match(reg); if (matches) score += v * matches.length; } } else { for (const { regex, weight, raw } of compiledContentRegexes) { const matches = text.match(regex); if (matches) { let contextSafe = false; for (const w of Object.keys(config.whitelist)) { if (text.match(new RegExp(`.{0,10}${w}.{0,10}${raw}|${raw}.{0,10}${w}.{0,10}`, 'i'))) { contextSafe = true; break; } } if (!contextSafe) score += weight * matches.length; } } } return score; } // 主内容检测 function detectAdultContent() { let totalScore = 0; let violationCount = 0; const mainSelectors = ['main', 'article', '.main-content', '.article-content', '.post-content', '#main', '#content']; let mainElements = []; for (const sel of mainSelectors) { mainElements = mainElements.concat(Array.from(document.querySelectorAll(sel))); } if (mainElements.length === 0) mainElements = [document.body]; let mainRisk = false; for (const el of mainElements) { const text = getAllVisibleText(el).slice(0, 2000); const score = calculateScore(text); if (score >= config.contentCheck.localizedCheck.elementThreshold) mainRisk = true; totalScore += score; } if (!mainRisk) { const globalText = getAllVisibleText(document.body).slice(0, 2000); const globalScore = calculateScore(globalText); if (globalScore >= config.contentCheck.localizedCheck.elementThreshold) violationCount++; totalScore += globalScore; } // 图片alt/title检测 const images = document.querySelectorAll('img[alt], img[title]'); for (const img of images) { const imgText = `${img.alt} ${img.title}`.trim(); if (imgText) { const score = calculateScore(imgText); if (score >= 3) violationCount++; totalScore += score * 0.3; } } // 元数据检测 const metaTags = document.querySelectorAll('meta[name="description"], meta[name="keywords"]'); for (const meta of metaTags) { const content = meta.content; if (content) { const score = calculateScore(content); if (score >= 3) violationCount++; totalScore += score * 0.2; } } if (violationCount > 0 || mainRisk) return true; return totalScore >= config.contentCheck.adultContentThreshold; } // 动态内容检测增强 function enhancedDynamicContentCheck() { let checkCount = 0; const maxChecks = 10; const interval = 2000; const hostname = window.location.hostname; function tryCheck() { checkCount++; if (detectAdultContent()) { blacklistManager.addToBlacklist(hostname, 'dynamic-content'); handleBlockedContent(); return; } if (checkCount < maxChecks) { setTimeout(tryCheck, interval); } } setTimeout(tryCheck, 1000); } // GM_value 封装 async function gmGet(key, def) { if (typeof GM_getValue === 'function') { const v = await GM_getValue(key); return v === undefined ? def : v; } return def; } async function gmSet(key, value) { if (typeof GM_setValue === 'function') { await GM_setValue(key, value); } } // 黑名单管理器(GM_setValue/GM_getValue实现) const blacklistManager = { BLACKLIST_KEY: 'pornblocker-blacklist', BLACKLIST_VERSION_KEY: 'pornblocker-blacklist-version', CURRENT_VERSION: '3.0', // 升级数据库版本,弃用旧数据 // 只在版本号不一致时清空旧数据 async checkAndUpgradeVersion() { const storedVersion = await gmGet(this.BLACKLIST_VERSION_KEY, null); if (storedVersion !== this.CURRENT_VERSION) { await gmSet(this.BLACKLIST_VERSION_KEY, this.CURRENT_VERSION); await gmSet(this.BLACKLIST_KEY, []); } }, // 获取黑名单 async getBlacklist() { // 确保版本检查已完成 await this.checkAndUpgradeVersion(); let data = await gmGet(this.BLACKLIST_KEY, []); // 自动清理过期和升级结构 const now = Date.now(); let changed = false; const valid = (Array.isArray(data) ? data : []).filter(item => { if (typeof item === 'string') return true; // 兼容老数据 if (item && item.host && item.expire && item.expire > now) return true; changed = true; return false; }).map(item => { if (typeof item === 'string') { changed = true; return createBlacklistEntry(item, 'legacy', '自动升级'); } // 结构升级:补全缺失字段 if (!item.version) item.version = this.CURRENT_VERSION; if (!item.added) item.added = now; if (!item.reason) item.reason = ''; if (!item.note) item.note = ''; return item; }); if (changed) { this.saveBlacklist(valid); } return valid; }, async saveBlacklist(list) { await gmSet(this.BLACKLIST_KEY, list); }, async addToBlacklist(hostname, reason = '', note = '') { if (!hostname) return false; // 搜索引擎白名单,禁止加入黑名单 if (config.searchEngines.some(domain => hostname === domain || hostname.endsWith('.' + domain))) { return false; } let list = await this.getBlacklist(); if (list.some(item => (typeof item === 'string' ? item : item.host) === hostname)) return true; list.push(createBlacklistEntry(hostname, reason, note)); await this.saveBlacklist(list); return true; }, async isBlacklisted(hostname) { let list = await this.getBlacklist(); return list.some(item => (typeof item === 'string' ? item : item.host) === hostname); }, async removeFromBlacklist(hostname) { let list = await this.getBlacklist(); list = list.filter(item => (typeof item === 'string' ? item : item.host) !== hostname); await this.saveBlacklist(list); return true; }, // 新增批量清理过期条目方法 async cleanExpired() { let list = await this.getBlacklist(); const now = Date.now(); const valid = list.filter(item => (typeof item === 'string') || (item && item.expire && item.expire > now)); await this.saveBlacklist(valid); return valid.length; } }; // 数据库结构优化:黑名单支持更多元数据,未来可扩展 // 支持来源、拦截原因、添加时间、过期时间、用户备注等 // 统一黑名单条目结构 function createBlacklistEntry(host, reason = '', note = '') { return { host, reason, note, added: Date.now(), expire: getExpireTimestamp(), version: blacklistManager.CURRENT_VERSION }; } // 立即执行版本检查 (async function initBlacklist() { await blacklistManager.checkAndUpgradeVersion(); })(); function getExpireTimestamp() { const BLACKLIST_EXPIRE_DAYS = 30; return Date.now() + BLACKLIST_EXPIRE_DAYS * 24 * 60 * 60 * 1000; } const regexCache = { domainRegex: new RegExp(Object.keys(config.domainKeywords).join('|'), 'gi'), whitelistRegex: new RegExp(Object.keys(config.whitelist).join('|'), 'gi'), xxxRegex: /\.xxx$/i }; function checkDomainPatterns(hostname) { return config.domainPatterns.some(pattern => pattern.test(hostname)); } function shouldCheckContent(hostname) { return config.contentCheckDomains.some(pattern => pattern.test(hostname)); } // ----------------- 主检测逻辑 ----------------- async function checkUrl() { const url = new URL(window.location.href); const hostname = url.hostname; if (await blacklistManager.isBlacklisted(hostname)) { return { shouldBlock: true, url, reason: 'blacklist' }; } if (checkDomainPatterns(url.hostname)) { await blacklistManager.addToBlacklist(hostname, 'domain-pattern'); return { shouldBlock: true, url, reason: 'domain-pattern' }; } for (const w of Object.keys(config.whitelist)) { if (hostname.match(new RegExp(`\\b${w}\\b`, 'i')) || (document.title || '').match(new RegExp(`\\b${w}\\b`, 'i'))) { return { shouldBlock: false, url }; } } if (shouldCheckContent(url.hostname)) { if (document.body) { if (detectAdultContent()) { await blacklistManager.addToBlacklist(hostname, 'content'); return { shouldBlock: true, url, reason: 'content' }; } enhancedDynamicContentCheck(); } else { document.addEventListener('DOMContentLoaded', () => { if (detectAdultContent()) { blacklistManager.addToBlacklist(hostname, 'content'); handleBlockedContent(); } enhancedDynamicContentCheck(); }); } } let score = 0; const pornMatches = url.hostname.match(regexCache.domainRegex) || []; pornMatches.forEach(match => { const keyword = match.toLowerCase(); const domainScore = config.domainKeywords[keyword] || 0; if (domainScore !== 0) score += domainScore; }); const path = url.pathname + url.search; score += calculateScore(path) * 0.4; score += calculateScore(document.title || ""); if (score >= config.thresholds.whitelist) { const hostMatches = url.hostname.match(regexCache.whitelistRegex) || []; const titleMatches = (document.title || "").match(regexCache.whitelistRegex) || []; let whitelistScore = 0; const whitelistMatchCount = (matches) => { matches.forEach(match => { const term = match.toLowerCase(); const reduction = config.whitelist[term] || 0; whitelistScore += reduction; }); }; whitelistMatchCount(hostMatches); whitelistMatchCount(titleMatches); score += whitelistScore; } return { shouldBlock: score >= config.thresholds.block, url }; } // 检测结果处理函数 const handleBlockedContent = () => { const lang = getUserLanguage(); const text = i18n[lang]; window.stop(); document.documentElement.innerHTML = ` <body> <div class="container"> <div class="card"> <div class="icon-wrapper"> <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/> </svg> </div> <h1>${text.title}</h1> <p>${text.message}<br>${text.redirect}</p> <div class="footer">${text.footer}</div> </div> </div> <style> :root { --bg-light: #f0f2f5; --card-light: #ffffff; --text-light: #2d3436; --text-secondary-light: #636e72; --text-muted-light: #b2bec3; --accent-light: #ff4757; --bg-dark: #1a1a1a; --card-dark: #2d2d2d; --text-dark: #ffffff; --text-secondary-dark: #a0a0a0; --text-muted-dark: #808080; --accent-dark: #ff6b6b; } @media (prefers-color-scheme: dark) { body { background: var(--bg-dark) !important; } .card { background: var(--card-dark) !important; box-shadow: 0 4px 12px rgba(0,0,0,0.3) !important; } h1 { color: var(--text-dark) !important; } p { color: var(--text-secondary-dark) !important; } .footer { color: var(--text-muted-dark) !important; } .icon-wrapper { background: var(--accent-dark) !important; } .countdown { color: var(--accent-dark); } } body { background: var(--bg-light); margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; min-height: 100vh; display: flex; align-items: center; justify-content: center; } .container { max-width: 500px; width: 100%; } .card { background: var(--card-light); border-radius: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); padding: 32px; text-align: center; animation: slideIn 0.5s ease-out; } .icon-wrapper { width: 64px; height: 64px; background: var(--accent-light); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 24px; animation: pulse 2s infinite; } .icon-wrapper svg { stroke: white; } h1 { color: var(--text-light); margin: 0 0 16px; font-size: 24px; font-weight: 600; } p { color: var(--text-secondary-light); margin: 0 0 24px; line-height: 1.6; font-size: 16px; } .footer { color: var(--text-muted-light); font-size: 14px; animation: fadeIn 1s ease-out; } .countdown { font-weight: bold; color: var(--accent-light); } @keyframes slideIn { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } </style> </body> `; let timeLeft = 4; const countdownEl = document.querySelector('.countdown'); const countdownInterval = setInterval(() => { timeLeft--; if (countdownEl) countdownEl.textContent = timeLeft; if (timeLeft <= 0) { clearInterval(countdownInterval); try { const homeUrl = getHomePageUrl(); if (window.history.length > 1) { const iframe = document.createElement('iframe'); iframe.style.display = 'none'; document.body.appendChild(iframe); iframe.onload = () => { try { const prevUrl = iframe.contentWindow.location.href; const prevScore = calculateScore(new URL(prevUrl).hostname, true); if (prevScore >= config.thresholds.block) { window.location.href = homeUrl; } else { window.history.back(); } } catch (e) { window.location.href = homeUrl; } document.body.removeChild(iframe); }; iframe.src = 'about:blank'; } else { window.location.href = homeUrl; } } catch (e) { window.location.href = getHomePageUrl(); } } }, 1000); }; // setupDynamicContentCheck 函数之前添加新函数 const setupTitleObserver = () => { let titleObserver = null; try { // 监听 title 标签变化 const titleElement = document.querySelector('title'); if (titleElement) { titleObserver = new MutationObserver(async (mutations) => { for (const mutation of mutations) { const newTitle = mutation.target.textContent; console.log(`[Title Change] New title: "${newTitle}"`); // 计算新标题的分数 const titleScore = calculateScore(newTitle || ""); if (titleScore >= config.thresholds.block) { console.log(`[Title Score] ${titleScore} exceeds threshold`); const hostname = window.location.hostname; await blacklistManager.addToBlacklist(hostname); titleObserver.disconnect(); handleBlockedContent(); return; } } }); titleObserver.observe(titleElement, { subtree: true, characterData: true, childList: true }); } // 监听 title 标签的添加 const headObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeName === 'TITLE') { setupTitleObserver(); headObserver.disconnect(); return; } } } }); headObserver.observe(document.head, { childList: true, subtree: true }); // 设置超时清理 setTimeout(() => { titleObserver?.disconnect(); headObserver?.disconnect(); }, config.contentCheck.observerTimeout); } catch (e) { console.error('Error in setupTitleObserver:', e); } return titleObserver; }; // ----------------- 主检测逻辑 ----------------- (async function () { const { shouldBlock, url: currentUrl } = await checkUrl(); if (shouldBlock || regexCache.xxxRegex.test(currentUrl.hostname)) { handleBlockedContent(); } else { // 添加标题监听 setupTitleObserver(); } })(); // 可选:定期自动清理过期黑名单(每天一次) (function autoCleanBlacklist() { try { const key = 'pornblocker-last-clean'; const now = Date.now(); let last = 0; try { last = parseInt(localStorage.getItem(key) || '0', 10); } catch (e) { } if (!last || now - last > 86400000) { blacklistManager.cleanExpired(); localStorage.setItem(key, now.toString()); } } catch (e) { } })(); })();