Bangumi 敏感词检测+替换

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

安裝腳本?
作者推薦腳本

您可能也會喜歡 AttachHowOldtoUserinPosts

安裝腳本
// ==UserScript==
// @name         Bangumi 敏感词检测+替换
// @namespace    https://greasyfork.org/zh-CN/users/1386262-zintop
// @version      1.3.2
// @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|index\/create|index\/.+\/edit|anime\/list\/.+)|subject\/\d+\/?$|settings(\?.*)?$)/
// @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 applyTheme() {
        const theme = document.documentElement.getAttribute("data-theme");
        const bg = theme === "dark" ? "#444" : "#fff";
        const panel = $('#sensitive-panel');
        if (panel) panel.style.background = bg;
        document.querySelectorAll('.sensitive-dialog').forEach(d => d.style.background = bg);
    }
    function observeThemeChange() {
        const observer = new MutationObserver(() => applyTheme());
        observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
    }

    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; 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>
            </div>
            <div style="padding:5px;">
                <button id="update-words">手动更新词库</button>
            </div>
            <div id="preset-list" style="padding:5px;"></div>
        `;
        document.body.appendChild(panel);

        applyTheme();
        observeThemeChange();

        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.className = 'sensitive-dialog';
        dialog.style.cssText = `position: fixed; top: 20%; left: 50%; transform: translateX(-50%);
            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);

        applyTheme(); // 预设弹窗适配关灯模式

        $('#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);

})();