// ==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
// @downloadURL https://update.greasyfork.icu/scripts/537577/bangumi%20%E6%95%8F%E6%84%9F%E8%AF%8D%E6%9B%BF%E6%8D%A2%2B%E8%87%AA%E5%AE%9A%E4%B9%89%E9%A2%84%E8%AE%BE.user.js
// @updateURL https://update.greasyfork.icu/scripts/537577/bangumi%20%E6%95%8F%E6%84%9F%E8%AF%8D%E6%9B%BF%E6%8D%A2%2B%E8%87%AA%E5%AE%9A%E4%B9%89%E9%A2%84%E8%AE%BE.meta.js
// ==/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 = `
✅ 没有检测到敏感词
透明度:
`;
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 = `
${isEdit ? '编辑' : '添加'}预设
`;
document.body.appendChild(dialog);
$('#add-rule').onclick = () => {
const div = document.createElement('div');
div.innerHTML = ` → `;
$('#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 = `
${preset.name}
`;
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 = '✅ 没有检测到敏感词';
} else {
status.innerHTML = `⚠️ 检测到${detectedWords.size}个敏感词`;
detectedWords.forEach(w => {
const line = document.createElement('div');
line.style.marginBottom = '4px';
line.style.wordBreak = 'break-word';
line.innerHTML = `${w}
`;
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);
})();