Greasy Fork 还支持 简体中文。

Bangumi 敏感词检测+替换

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

安裝腳本?
作者推薦腳本

您可能也會喜歡 AttachHowOldtoUserinPosts

安裝腳本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);

})();