Greasy Fork

Greasy Fork is available in English.

bangumi 敏感词替换+自定义预设

检测bangumi发布/修改内容中含有的敏感词,并对其进行单个替换或批量替换,同时支持自定义预设,不局限于敏感词列表

当前为 2025-07-01 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 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.0.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).*/
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const STORAGE_KEY = 'sensitive_panel_settings';
    const SENSITIVE_WORDS = [
        "白粉", "办证", "辦證", "毕业证", "畢業證", "冰毒", "步枪", "步槍", "春药", "春藥", "大发", "大發",
        "大麻", "代开", "代開", "代考", "贷款", "貸款", "发票", "發票", "海洛因", "妓女", "精神病", "可卡因",
        "批发", "批發", "皮肤病", "皮膚病", "嫖娼", "窃听器", "竊聽器", "上门服务", "上門服務", "商铺", "商鋪",
        "手枪", "手槍", "铁枪", "鐵槍", "钢枪", "鋼槍", "特殊服务", "特殊服務", "騰訊", "香烟", "香煙", "学位证",
        "學位證", "摇头丸", "搖頭丸", "医院", "醫院", "隐形眼镜", "聊天记录", "援交", "找小姐", "找小妹", "作弊",
        "v信", "迷药", "电动车", "早泄", "毒枭", "春节", "当场死亡", "烟草", "假钞", "罂粟", "牛皮癣", "甲状腺",
        "安乐死", "香艳", "医疗政策", "服务中心", "习近平", "李克强", "支那", "前列腺", "迷魂药", "迷情粉",
        "迷藥", "麻醉药", "肛门", "麻果", "麻古", "假币", "私人侦探", "提现", "借腹生子", "代孕", "客服电话",
        "刻章", "套牌车", "麻将机", "走私", "财税务"
    ];

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

    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;
    }

    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;
            padding: 0;
            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;
        `;
        setTimeout(() => loadPanelSettings(panel), 0);

        panel.innerHTML = `
            <div id="sensitive-header" style="background:#f99;color:#fff;padding:5px;cursor:move;">
                敏感词检测
            </div>
            <div id="sensitive-body" style="padding:10px;">
                <div id="sensitive-status"><strong>✅ 没有检测到敏感词</strong></div>
                <div id="sensitive-word-list" style="margin:10px 0;"></div>
                <div style="margin-bottom:10px;">
                    <button id="btn-replace-all">全部替换</button>
                    <button id="btn-replace-star">全部替换为**</button>
                </div>
                <hr>
                <button id="btn-add-preset">添加预设</button>
                <div id="preset-list" style="margin-top:10px;"></div>
                <hr>
                <div>透明度:
                    <input type="range" id="opacity-slider" min="0.2" max="1" step="0.05" value="1">
                </div>
            </div>
        `;
        document.body.appendChild(panel);

        // 拖动
        let isDragging = false, offsetX, offsetY;
        const header = panel.querySelector('#sensitive-header');
        header.onmousedown = function (e) {
            isDragging = true;
            offsetX = e.clientX - panel.offsetLeft;
            offsetY = e.clientY - panel.offsetTop;
            document.onmousemove = function (e) {
                if (isDragging) {
                    panel.style.left = (e.clientX - offsetX) + 'px';
                    panel.style.top = (e.clientY - offsetY) + 'px';
                    panel.style.right = 'auto';
                }
            };
            document.onmouseup = () => {
                if (isDragging) {
                    isDragging = false;
                    savePanelSettings(panel);
                }
            };
        };
        panel.onmouseup = () => savePanelSettings(panel);

        // 透明度
        $('#opacity-slider').oninput = (e) => {
            panel.style.opacity = e.target.value;
            savePanelSettings(panel);
        };

        // 全部替换
        $('#btn-replace-all').onclick = () => {
            const arr = Array.from(detectedWords);
            (function next(i) {
                if (i >= arr.length) return;
                const w = arr[i];
                const r = prompt(`将“${w}”替换为:`);
                if (r != null) {
                    replaceWordInInputs(w, r);
                }
                next(i + 1);
            })(0);
            detectedWords.clear();
            updatePanel();
        };
        // 全部替换为星号
        $('#btn-replace-star').onclick = () => {
            detectedWords.forEach(w => {
                replaceWordInInputs(w, '*'.repeat(w.length));
            });
            detectedWords.clear();
            updatePanel();
        };

        // 添加预设
        $('#btn-add-preset').onclick = showPresetDialog;
        renderPresets();
    }

    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 = () => {
            const div = document.createElement('div');
            div.innerHTML = `<input placeholder="指定内容"> → <input placeholder="替换为">`;
            $('#preset-items').appendChild(div);
        };
        $('#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 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);
        let 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 (e) {
                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 line = document.createElement('div');
                line.style.marginBottom = '4px';
                line.style.wordBreak = 'break-word';
                line.innerHTML = `<strong>${w}</strong>
                    <button data-word="${w}" class="btn-replace">替换</button>`;
                list.appendChild(line);
            });

            list.querySelectorAll('.btn-replace').forEach(btn => {
                btn.onclick = () => {
                    const w = btn.dataset.word;
                    const r = prompt(`将“${w}”替换为:`);
                    if (r != null) {
                        replaceWordInInputs(w, r);
                        detectedWords.delete(w);
                        updatePanel();
                    }
                };
            });
        }
    }

    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 updatePanel() {
        runDetection();
    }

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

    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 init() {
        createUI();
        runDetection();
        hookInputEvents();
    }

    window.addEventListener('load', init);
})();