Porn Blocker | 色情内容过滤器

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.2
// @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 that may harm your well-being.',
            redirect: 'You will be redirected in <span class="countdown">4</span> seconds…',
            footer: 'Cherish your mind · Stay away from harmful sites'
        },
        '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: '<span class="countdown">4</span>초 후 자동으로 이동됩니다……',
            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
        ],
    };

    // 预编译 domainKeywords、whitelist、falsePositiveRegexList
    const compiledDomainRegexes = Object.keys(config.domainKeywords).map(k => new RegExp(`\\b${k}\\b`, 'gi'));
    const compiledWhitelistRegexes = Object.keys(config.whitelist).map(k => {
        if (k.startsWith('/') && k.endsWith('/')) {
            return new RegExp(k.slice(1, -1), 'i');
        } else {
            return new RegExp(`\\b${k}\\b`, 'i');
        }
    });

    // ----------------- 工具函数 -----------------
    function isWhitelisted(text) {
        let i = 0;
        for (const [w, wv] of Object.entries(config.whitelist)) {
            const reg = compiledWhitelistRegexes[i++];
            if (reg.test(text)) 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) {
            let i = 0;
            for (const [k, v] of Object.entries(config.domainKeywords)) {
                const reg = compiledDomainRegexes[i++];
                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() {
            if (detectAdultContent()) {
                blacklistManager.addToBlacklist(hostname, 'dynamic-content');
                handleBlockedContent();
                return;
            }
            checkCount++;
            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];
        document.title = text.title;
        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) { }
    })();
})();