Greasy Fork

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

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

安装此脚本
作者推荐脚本

您可能也喜欢AttachHowOldtoUserinPosts

安装此脚本
// ==UserScript==
// @name         bangumi 敏感词替换+自定义预设
// @namespace    https://greasyfork.org/zh-CN/users/1386262-zintop
// @version      1.0.3
// @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);
})();