Greasy Fork

Greasy Fork is available in English.

bangumi 敏感词检测+替换

检测bangumi发布/修改内容中含有的敏感词,并进行单个或批量替换,同时支持自定义预设,可自动/手动更新词库

当前为 2025-08-16 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         bangumi 敏感词检测+替换
// @namespace    http://greasyfork.icu/zh-CN/users/1386262-zintop
// @version      1.2.0
// @description  检测bangumi发布/修改内容中含有的敏感词,并进行单个或批量替换,同时支持自定义预设,可自动/手动更新词库
// @author       zintop
// @license      MIT
// @include      /^https?:\/\/(bgm\.tv|bangumi\.tv|chii\.in)\/.*(group\/topic\/.+\/edit|group\/.+\/settings|group\/.+\/new_topic|blog\/create|blog\/.+\/edit|subject\/.+\/topic\/new|subject\/topic\/.+\/edit|user\/.+\/timeline|subject\/.+|settings|index\/create|index\/.+\/edit|anime\/list\/.+).*/
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const STORAGE_KEY = 'sensitive_panel_settings';
    const REMOTE_JSON = 'https://raw.githubusercontent.com/zintop/bangumi-sensitive-words/refs/heads/main/bangumi-sensitive-words.json';

    let SENSITIVE_WORDS = []; // 词库
    let lastUpdate = '';      // 更新时间

    let detectedWords = new Set();
    let regexPresets = JSON.parse(localStorage.getItem('sensitive_regex_presets') || '[]');
    let panelFirstShowDone = false;

    function $(s) { return document.querySelector(s); }

    function savePanelSettings(panel) {
        const s = {
            left: panel.style.left,
            top: panel.style.top,
            width: panel.style.width,
            height: panel.style.height,
            opacity: panel.style.opacity
        };
        localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
    }

    function loadPanelSettings(panel) {
        const s = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
        if (s.left) panel.style.left = s.left;
        if (s.top) panel.style.top = s.top;
        if (s.width) panel.style.width = s.width;
        if (s.height) panel.style.height = s.height;
        if (s.opacity) panel.style.opacity = s.opacity;
    }

    async function fetchRemoteWords() {
        try {
            const res = await fetch(REMOTE_JSON + '?_=' + Date.now());
            const json = await res.json();
            if (Array.isArray(json)) {
                SENSITIVE_WORDS = json;
                lastUpdate = new Date().toLocaleString();
                const el = $('#sensitive-last-update');
                if(el) el.textContent = `词库更新时间:${lastUpdate}`;
                runDetection();
            }
        } catch (e) {
            console.error('敏感词库更新失败', e);
        }
    }

    function createUI() {
        const panel = document.createElement('div');
        panel.id = 'sensitive-panel';
        panel.style.cssText = `
            position: fixed; top:80px; left:320px; width:280px; max-height:80vh;
            overflow-y:auto; z-index:99999; background:#E9E8E8; border:1px solid #f99;
            font-size:13px; font-family:sans-serif; border-radius:8px;
            box-shadow:0 2px 6px rgba(0,0,0,0.15); resize:both; overflow:hidden auto;
            opacity:1; display:none;
        `;
        loadPanelSettings(panel);

        panel.innerHTML = `
            <div id="sensitive-header" style="background:#f99;color:#fff;padding:5px;cursor:move;">敏感词检测</div>
            <div id="sensitive-status" style="padding:5px;"><strong>✅ 没有检测到敏感词</strong></div>
            <div id="sensitive-last-update" style="padding:5px; font-size:11px; color:#666;">词库更新时间:${lastUpdate}</div>
            <div id="sensitive-word-list" style="padding:5px;"></div>
            <div style="padding:5px;">
                <button id="replace-all">全部替换</button>
                <button id="replace-stars">全部替换为**</button>
                <button id="add-preset" style="margin-left:4px;">添加预设</button>
                <button id="update-words" style="margin-left:4px;">手动更新词库</button>
            </div>
            <div id="preset-list" style="padding:5px;"></div>
        `;
        document.body.appendChild(panel);

        // 拖动
        const header = $('#sensitive-header');
        let offsetX=0, offsetY=0, isDown=false;
        header.addEventListener('mousedown', e => { isDown=true; offsetX=e.clientX-panel.offsetLeft; offsetY=e.clientY-panel.offsetTop; e.preventDefault(); });
        document.addEventListener('mouseup', ()=>{isDown=false;});
        document.addEventListener('mousemove', e=>{ if(!isDown) return; panel.style.left=`${e.clientX-offsetX}px`; panel.style.top=`${e.clientY-offsetY}px`; savePanelSettings(panel); });

        // 插入按钮
        const uname = document.querySelector('.avatar')?.getAttribute('href')?.split('/').pop();
        if(!uname) return;
        const dock = document.querySelector('#dock ul>li.first');
        if(dock){
            const li = document.createElement('li');
            li.innerHTML = `<a href="javascript:void(0);" id="toggleSensitiveBtn">敏感词🔍</a><p></p>`;
            dock.after(li);
            $('#toggleSensitiveBtn').addEventListener('click', ()=>{
                panel.style.display = panel.style.display==='none'?'block':'none';
            });
        }

        // 替换按钮
        $('#replace-all').onclick = () => {
            Array.from(detectedWords).forEach(w=>{
                const r=prompt(`将 "${w}" 替换为:`);
                if(r!=null) replaceWordInInputs(w,r);
            });
            runDetection();
        };
        $('#replace-stars').onclick = () => {
            detectedWords.forEach(w=>replaceWordInInputs(w,'*'.repeat(w.length)));
            runDetection();
        };

        $('#add-preset').onclick = showPresetDialog;
        $('#update-words').onclick = fetchRemoteWords;

        renderPresets();
    }

    function updateToggleButtonText(){
        const btn = $('#toggleSensitiveBtn');
        if(!btn) return;
        btn.textContent = detectedWords.size>0 ? '敏感词⚠️' : '敏感词🔍';
        const panel = $('#sensitive-panel');
        if(detectedWords.size>0 && !panelFirstShowDone){
            panel.style.display='block';
            panelFirstShowDone=true;
        }
    }

    function showPresetDialog(editIdx){
        const isEdit = typeof editIdx==='number';
        const existing = isEdit ? regexPresets[editIdx] : null;
        const dialog = document.createElement('div');
        dialog.style.cssText = `position: fixed; top: 20%; left: 50%; transform: translateX(-50%);
            background: #E9E8E8; padding: 20px; z-index: 100000; border: 1px solid #ccc;
            box-shadow: 0 2px 8px rgba(0,0,0,0.3); max-height: 70vh; overflow-y: auto;`;
        dialog.innerHTML = `
            <h3>${isEdit?'编辑':'添加'}预设</h3>
            <div id="preset-items">
                ${existing?existing.rules.map(r=>`<div><input placeholder="指定内容" value="${r.pattern}"> → <input placeholder="替换为" value="${r.replace}"></div>`).join(''):'<div><input placeholder="指定内容"> → <input placeholder="替换为"></div>'}
            </div>
            <button id="add-rule">添加规则</button>
            <br><br>
            <input id="preset-name" placeholder="预设名称(可选)" value="${existing?existing.name:''}"><br><br>
            <button id="save-preset">保存</button>
            <button id="cancel-preset">取消</button>
        `;
        document.body.appendChild(dialog);
        $('#add-rule').onclick=()=>{$('#preset-items').appendChild(document.createElement('div')).innerHTML='<input placeholder="指定内容"> → <input placeholder="替换为">';};
        $('#cancel-preset').onclick=()=>dialog.remove();
        $('#save-preset').onclick=()=>{
            const name=$('#preset-name').value.trim()||`预设${regexPresets.length+1}`;
            const rules=Array.from(dialog.querySelectorAll('#preset-items > div')).map(div=>{
                const inputs=div.querySelectorAll('input');
                return {pattern:inputs[0].value.trim(),replace:inputs[1].value};
            }).filter(r=>r.pattern.length>0);
            if(rules.length===0){alert('请至少添加一个有效的预设规则');return;}
            if(isEdit) regexPresets[editIdx]={name,rules};
            else regexPresets.push({name,rules});
            localStorage.setItem('sensitive_regex_presets',JSON.stringify(regexPresets));
            dialog.remove(); renderPresets(); runDetection();
        };
    }

    function renderPresets(){
        const container=$('#preset-list');
        container.innerHTML='';
        regexPresets.forEach((preset,i)=>{
            const div=document.createElement('div');
            div.style.marginBottom='8px'; div.style.border='1px solid #ddd';
            div.style.padding='6px'; div.style.borderRadius='4px';
            div.innerHTML=`<b>${preset.name}</b>
                <button class="btn-load" data-i="${i}">加载</button>
                <button class="btn-edit" data-i="${i}">编辑</button>
                <button class="btn-delete" data-i="${i}">删除</button>`;
            container.appendChild(div);
        });

        container.querySelectorAll('.btn-load').forEach(btn=>{
            btn.onclick=()=>{
                const preset=regexPresets[btn.dataset.i];
                preset.rules.forEach(rule=>replaceWordInInputs(rule.pattern,rule.replace));
                runDetection();
            };
        });
        container.querySelectorAll('.btn-edit').forEach(btn=>{
            btn.onclick=()=>showPresetDialog(Number(btn.dataset.i));
        });
        container.querySelectorAll('.btn-delete').forEach(btn=>{
            btn.onclick=()=>{
                if(confirm('确定删除此预设?')){
                    regexPresets.splice(Number(btn.dataset.i),1);
                    localStorage.setItem('sensitive_regex_presets',JSON.stringify(regexPresets));
                    renderPresets(); runDetection();
                }
            };
        });
    }

    function replaceWordInInputs(word,replacement){
        const inputs=Array.from(document.querySelectorAll('textarea,input[type=text],input[type=search],input:not([type])')).filter(el=>el.offsetParent!==null);
        inputs.forEach(input=>{
            if(input.value.includes(word)){
                input.value=input.value.split(word).join(replacement);
                input.dispatchEvent(new Event('input',{bubbles:true}));
            }
        });
    }

    function hookInputEvents(){
        const inputs=Array.from(document.querySelectorAll('textarea,input[type=text],input[type=search],input:not([type])')).filter(el=>el.offsetParent!==null);
        inputs.forEach(input=>input.addEventListener('input',()=>runDetection()));
    }

    function runDetection(customRules){
        const list=$('#sensitive-word-list');
        const status=$('#sensitive-status');
        detectedWords.clear(); list.innerHTML='';
        const inputs=Array.from(document.querySelectorAll('textarea,input[type=text],input[type=search],input:not([type])')).filter(el=>el.offsetParent!==null);
        const text=inputs.map(i=>i.value).join('\n');

        SENSITIVE_WORDS.forEach(w=>{if(text.includes(w)) detectedWords.add(w);});
        const rules=customRules||regexPresets.flatMap(p=>p.rules);
        rules.forEach(({pattern})=>{
            let reg; try{reg=new RegExp(pattern,'gi');}catch{return;}
            let match; while((match=reg.exec(text))!==null) detectedWords.add(match[0]);
        });

        if(detectedWords.size===0) status.innerHTML='<strong>✅ 没有检测到敏感词</strong>';
        else status.innerHTML=`<strong style="color:red">⚠️ 检测到${detectedWords.size}个敏感词</strong>`;

        detectedWords.forEach(w=>{
            const div=document.createElement('div'); div.style.marginBottom='4px'; div.style.wordBreak='break-word';
            div.innerHTML=`<strong>${w}</strong> <button class="btn-replace">替换</button>`;
            const btn=div.querySelector('.btn-replace');
            btn.onclick=()=>{
                const r=prompt(`将“${w}”替换为:`);
                if(r!=null){replaceWordInInputs(w,r); runDetection();}
            };
            list.appendChild(div);
        });
        updateToggleButtonText();
    }

    function init(){ createUI(); fetchRemoteWords(); runDetection(); hookInputEvents(); }

    window.addEventListener('load',init);

})();