// ==UserScript==
// @name bangumi 敏感词替换+自定义预设
// @namespace https://gf.qytechs.cn/zh-CN/users/1386262-zintop
// @version 1.0.1
// @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);
})();