User:花开夜/refcheck.js
外观
< User:花开夜
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// ==UserScript==
// @name 中文维基百科来源快检
// @namespace http://tampermonkey.net/
// @version 1.3
// @description 在中文维基百科页面检测劣质和可疑的引用来源,并在“查看历史”旁添加“快检”按钮。报告中支持按分类筛选。仅在主条目空间运行。
// @author Gemini
// @match https://zh.wikipedia.org/wiki/*
// @match https://zh.m.wikipedia.org/wiki/*
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// console.log("中文维基百科来源快检脚本开始运行 v1.3");
// --- Namespace Check ---
// Only run in main article namespace (ns=0)
if (typeof mw !== 'undefined' && typeof mw.config !== 'undefined' && mw.config.get('wgNamespaceNumber') !== 0) {
console.log("中文维基百科来源快检: 非主条目空间 (namespace: " + mw.config.get('wgNamespaceNumber') + "),脚本不运行。");
return;
}
// Fallback for environments where mw might not be fully loaded, though less likely for a userscript @run-at document-idle
// This check is basic and might not cover all non-article pages if mw.config is unavailable.
// A more robust check could involve looking at wgPageName or wgCanonicalNamespace.
if (typeof mw === 'undefined' || typeof mw.config === 'undefined') {
const path = window.location.pathname;
if (path.includes('/wiki/Wikipedia:') || path.includes('/wiki/Talk:') || path.includes('/wiki/User:') ||
path.includes('/wiki/User_talk:') || path.includes('/wiki/File:') || path.includes('/wiki/Template:') ||
path.includes('/wiki/Help:') || path.includes('/wiki/Category:') || path.includes('/wiki/Portal:') ||
path.includes('/wiki/Draft:') || path.includes('/wiki/MediaWiki:') || path.includes('/wiki/Module:') ||
path.includes('/wiki/Special:')) {
console.log("中文维基百科来源快检: 通过URL路径检测到非主条目空间,脚本不运行。");
return;
}
}
// --- Helper function to add CSS ---
function addCustomCSS(css) {
if (typeof GM_addStyle !== "undefined") {
GM_addStyle(css);
} else {
try {
const style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
} catch (e) {
console.error("中文维基百科来源快检: 添加CSS失败: ", e);
}
}
}
// --- Configuration for Sources ---
const techBlogWhitelist = [ // 知名技术博客白名单 (域名或特定路径关键词)
// Ensure these are specific enough to avoid broad unintended matches.
// For domains, prefer matching `hostname.endsWith(whitelistedDomain)`
// For paths, `url.includes(whitelistedPath)`
"blog.google", "ai.googleblog.com", "developers.google.com/ब्लॉग", "android-developers.googleblog.com", "chrome.blogspot.com", "blog.youtube", // Google
"openai.com/blog", // OpenAI
"blogs.microsoft.com", "developer.microsoft.com/en-us/graph/blogs", "azure.microsoft.com/blog", "windows.com/blog", "techcommunity.microsoft.com", // Microsoft
"aws.amazon.com/blogs", // AWS
"engineering.fb.com", "developers.facebook.com", "meta.com/blog", "engineering.instagram.com", // Meta
"apple.com/newsroom", "developer.apple.com/news", "machinelearning.apple.com", "webkit.org/blog", // Apple
"techcrunch.com", "thenextweb.com", "arstechnica.com", "wired.com/category/", // Tech News
"github.blog", "blog.cloudflare.com", "stackoverflow.blog", "krebsonsecurity.com", "schneier.com", // Security & Dev Platforms
"blog.mozilla.org", "blog.chromium.org", "web.dev", "vercel.com/blog", "netlify.com/blog", // Web Dev
"netflixtechblog.com", "uber.engineering", "engineering.atspotify.com", "airbnb.io", "linkedin.engineering", "engineering.linkedin.com", // Company Engineering Blogs
"figma.com/blog", "slack.engineering", "dropbox.tech", "djangoproject.com/weblog", "palletsprojects.com/blog", // Specific Tech Stacks
"kubernetes.io/blog", "rust-lang.org/blog", "golang.org/blog", "go.dev/blog", "nodejs.org/en/blog", "v8.dev/blog", "php.net/manual", "wiki.php.net/rfc", // Languages & Runtimes
"blog.jetbrains.com", "infoq.com", "martinfowler.com", "baeldung.com", // Dev Resources
"distill.pub", // Research
"blog.twitter.com", "engineering.twitter.com", // Legacy Twitter (X)
"oracle.com/blogs", "blogs.oracle.com", // Oracle
"ibm.com/blogs", "developer.ibm.com/blogs", // IBM
"intel.com/newsroom", "blogs.intel.com", // Intel
"cisco.com/c/en/us/about/blogs.html", "blogs.cisco.com", // Cisco
"redhat.com/en/blog", "opensource.com", "redhat.com/developers/blog" // Red Hat / Open Source
];
const redSources = [
// 法轮功背景媒体
{ keyword: "大纪元", name: "大纪元系列媒体", domains: ["epochtimes.com", "epochtimes.com.tw", "epochweekly.com", "djyimg.com", "theepochtimes.com", "epochtimestruth.com", "lagranepoca.com"] },
{ keyword: "新唐人", name: "新唐人电视台系列媒体", domains: ["ntdtv.com", "ntdtv.com.tw", "ntdca.com", "ntd.tv", "ntdnews.com", "ntdchina.com"] },
{ keyword: "看中国", name: "看中国", domains: ["kanzhongguo.com", "secretchina.com", "secretchina.org", "visiontimes.com"] },
{ keyword: "明慧网", name: "明慧网", domains: ["minghui.org", "falundafa.org", "falunhr.org"] },
{ keyword: "希望之声", name: "希望之声国际广播电台", domains: ["soundofhope.org", "soundofhope.info", "sohnetwork.com", "ozvoice.org", "bayvoice.net"] },
{ keyword: "人民报", name: "人民报", domains: ["renminbao.com"] },
{ keyword: "禁闻网", name: "禁闻网", domains: ["bannedbook.org"] },
{ keyword: "动态网", name: "动态网", domains: ["dongtaiwang.com"] },
{ keyword: "无界浏览", name: "无界浏览", domains: ["wujieliulan.com"] },
{ keyword: "正见网", name: "正见网", domains: ["zhengjian.org"] },
{ keyword: "阿波罗新闻网", name: "阿波罗新闻网", domains: ["aboluowang.com"] },
{ keyword: "博讯新闻网", name: "博讯新闻网", domains: ["boxun.com", "news.boxun.com"] },
// 中国大陆可任意编辑的在线百科
{ keyword: "百度百科", name: "百度百科", domains: ["baike.baidu.com"] },
{ keyword: "互动百科", name: "互动百科/快懂百科", domains: ["baike.com", "hudong.com", "kuaidongbaike.com"] },
{ keyword: "快懂百科", name: "快懂百科", domains: ["kuaidongbaike.com"] },
{ keyword: "搜狗百科", name: "搜狗百科", domains: ["baike.sogou.com"] },
{ keyword: "头条百科", name: "头条百科", domains: ["baike.com", "www.baike.com/wiki", "baike.toutiao.com"] },
{ keyword: "360百科", name: "360百科", domains: ["baike.so.com"] },
{ keyword: "MBA智库百科", name: "MBA智库百科", domains: ["wiki.mbalib.com"] },
{ keyword: "萌娘百科", name: "萌娘百科", domains: ["zh.moegirl.org.cn", "mzh.moegirl.org.cn", "hmoegirl.com", "moegirl.org"] },
{ keyword: "维基百科镜像", name: "维基百科镜像站", domains: ["wikipedia.cn", "www.wikizero.com", "www.wikipedia.wiki", "www.wiki.zh-cn.nina.az", " पूरी दुनिया全览", "wiki.biligame.com", "wiki.zorua.top", "truthwiki.org", "everipedia.org", "萬維百科", "www.wanweibaike.net", "www.peekme.cc"] },
// 用户生成内容 (UGC) 和自媒体平台
{ keyword: "知乎", name: "知乎专栏/回答", domains: ["zhihu.com", "zhuanlan.zhihu.com"] },
{ keyword: "微信公众平台", name: "微信公众平台", domains: ["mp.weixin.qq.com"] },
{ keyword: "Quora", name: "Quora", domains: ["quora.com"] },
{ keyword: "Reddit", name: "Reddit (非官方/个人subreddits)", domains: ["reddit.com"] },
{ keyword: "Medium", name: "Medium (个人博客)", domains: ["medium.com"] },
{ name: "Bilibili 专栏", domains: ["bilibili.com/read", "www.bilibili.com/read"] },
{ keyword: "今日头条", name: "今日头条 (自媒体号)", domains: ["toutiao.com", "jinritoutiao.com", "pstatp.com"] },
{ keyword: "网易号", name: "网易号", domains: ["dy.163.com", "www.163.com/dy"] },
{ keyword: "搜狐号", name: "搜狐号", domains: ["sohu.com/a/", "m.sohu.com/a/"] },
{ keyword: "企鹅号", name: "企鹅号", domains: ["om.qq.com", "kuaibao.qq.com"] },
{ keyword: "大风号", name: "大风号", domains: ["ifeng.com/shanklist", "ishare.ifeng.com"] },
{ keyword: "简书", name: "简书", domains: ["jianshu.com"] },
{ keyword: "豆瓣", name: "豆瓣 (用户日记/小组)", domains: ["douban.com/note/", "douban.com/group/"] },
{ keyword: "人人小站", name: "人人小站", domains: ["zhan.renren.com"] },
{ keyword: "Lofter", name: "Lofter (乐乎)", domains: ["lofter.com"] },
{ keyword: "百度贴吧", name: "百度贴吧", domains: ["tieba.baidu.com"] },
{ keyword: "小红书", name: "小红书", domains: ["xiaohongshu.com"] },
{ keyword: "抖音", name: "抖音", domains: ["douyin.com"] },
{ keyword: "快手", name: "快手", domains: ["kuaishou.com"] },
{ keyword: "AcFun", name: "AcFun (用户投稿)", domains: ["acfun.cn"] },
{ name: "巴哈姆特哈拉区", domains: ["forum.gamer.com.tw"] },
// 博客类 (除非在白名单内) - This rule is applied more generically in checkSource if blog keywords are found.
// Specific platform domains help catch them even if keywords are missed.
{ name: "博客/网志 (平台)", domains: ["blogspot.com", "wordpress.com", "tumblr.com", "typepad.com", "livejournal.com", "substack.com", "ghost.io", "wixsite.com", "weebly.com", "ameblo.jp", "fc2.com"] }, // fc2 has many blogs
{ keyword: "博客", name: "博客/网志 (关键词)" }, // Keyword-based identification for blogs
{ keyword: "网志", name: "博客/网志 (关键词)" },
{ keyword: "个人网站", name: "个人网站/博客 (非机构)" }, // Generic term
// 新闻农场或低质量内容聚合
{ name: "内容农场", domains: [
"kknews.cc", "pttnews.cc", "read01.com", "omgtw.com", "twgreatdaily.com", "cocomy.net", "mission-health.com", "ezp9.com",
"bomb01.com", "novelfeed.com",
"fun01.cc", "life.tw", "xcnnews.com", "lookaside.fbsbx.com/crawler/cache/",
"www.zmedia.com.tw", "www.how01.com", "www.pixnet.net/pcard",
"buzzfeed.com", // BuzzFeed main site (not BuzzFeed News)
"shareonion.com", "hssszn.com", "bepost.org", "buzzhand.com", "adaymag.com", "bigspeak.org", "cmoney.tw/follow",
"zi.media", "budтность.com"
]}
];
const orangeSources = [
{ name: "微博 (用户生成, 可能为第一方来源)", domains: ["weibo.com", "weibo.cn", "weibointl.api.weibo.com", "m.weibo.cn"] },
{ name: "YouTube (用户频道/视频, 可能为第一方来源)", domains: ["youtube.com", "youtu.be", "m.youtube.com"] },
{ name: "X / Twitter (用户生成, 可能为第一方来源)", domains: ["twitter.com", "x.com"] },
{ name: "Facebook (用户生成, 可能为第一方来源)", domains: ["facebook.com", "m.facebook.com"] },
{ name: "Bilibili 视频", domains: ["bilibili.com", "space.bilibili.com", "player.bilibili.com"] }, // Videos, excluding /read
{ keyword: "美国之音", name: "美国之音 (VOA)", domains: ["voachinese.com", "voanews.com", "america.gov", "voacantonese.com"] },
{ keyword: "香港自由新闻", name: "香港自由新闻 (HKFP)", domains: ["hongkongfp.com"] },
{ keyword: "公民行动影音纪录资料库", name: "公民行动影音纪录资料库 (台湾)", domains: ["civilmedia.tw"] },
{ keyword: "维权网", name: "维权网", domains: ["wqw2010.blogspot.com", "weiquanwang.org", "nchrd.org"] },
{ keyword: "寒冬", name: "寒冬 (Bitter Winter)", domains: ["bitterwinter.org", "zh.bitterwinter.org"] },
{ keyword: "改变中国", name: "改变中国 (China Change)", domains: ["chinachange.org"] },
{ keyword: "光传媒", name: "光传媒", domains: ["ipkmedia.com"] },
{ keyword: "议报", name: "议报", domains: ["yibaochina.com", "yibao.net"] },
{ keyword: "中国茉莉花革命网", name: "中国茉莉花革命网", domains: ["molihua.org"] },
{ keyword: "香港独立媒体", name: "香港独立媒体 (InMedia HK)", domains: ["inmediahk.net"] },
{ keyword: "端传媒", name: "端传媒 (The Initium)", domains: ["theinitium.com"] },
{ name: "布赖特巴特新闻网 (Breitbart)", domains: ["breitbart.com"] },
{ name: "点新闻 (Dot Dot News)", domains: ["dotdotnews.com"] },
{ name: "世界新闻网 (WND / WorldNetDaily)", domains: ["wnd.com"] },
{ name: "InfoWars", name: "InfoWars (应停止使用)", domains: ["infowars.com", "newswars.com", "prisonplanet.com"] },
{ name: "The Gateway Pundit", domains: ["thegatewaypundit.com"] },
{ name: "每日来电 (The Daily Caller)", domains: ["dailycaller.com"] },
{ name: "新闻极限 (Newsmax)", domains: ["newsmax.com"] },
{ name: "Zero Hedge", domains: ["zerohedge.com"] },
{ name: "The Unz Review", domains: ["unz.com"] },
{ name: "Consortium News", domains: ["consortiumnews.com"] },
{ name: "The Grayzone", domains: ["thegrayzone.com"] },
{ name: "MintPress News", domains: ["mintpressnews.com"] },
{ name: "TFIGlobal", domains: ["tfiglobalnews.com"] },
{ name: "PJ Media (Pajamas Media)", domains: ["pjmedia.com"] },
{ name: "The Federalist", domains: ["thefederalist.com"] },
{ name: "RedState", domains: ["redstate.com"] }
];
const blueSources = [
{ keyword: "自由亚洲电台", name: "自由亚洲电台 (RFA)", domains: ["rfa.org"] },
{ keyword: "中国数字时代", name: "中国数字时代 (CDT)", domains: ["chinadigitaltimes.net", "中国数字时代.net"] },
{ keyword: "大公报", name: "大公报", domains: ["takungpao.com.hk", "takungpao.com", "tkww.hk"] },
{ keyword: "文汇报", name: "文汇报 (香港)", domains: ["wenweipo.com", "hkwwp.com"] },
{ keyword: "中国时报", name: "中国时报 (中时新闻网)", domains: ["chinatimes.com", "ctinews.com"] },
{ keyword: "自由时报", name: "自由时报 (Liberty Times)", domains: ["ltn.com.tw", "libertytimes.com.tw"] },
{ keyword: "苹果日报", name: "苹果日报 (台湾/香港, 已停运部分)", domains: ["tw.appledaily.com", "hk.appledaily.com", "appledaily.com", "nextdigital.com.hk"] },
{ keyword: "民视新闻", name: "民视新闻 (FTV News)", domains: ["news.ftv.com.tw", "ftvnews.com.tw"] },
{ keyword: "三立新闻", name: "三立新闻网 (SETN)", domains: ["setn.com", "news.setn.com"] },
{ name: "新头壳 (Newtalk)", domains: ["newtalk.tw"] },
{ name: "上报 (Up Media)", domains: ["upmedia.mg"] },
{ name: "风传媒 (Storm Media)", domains: ["storm.mg"] },
{ name: "镜周刊 (Mirror Media)", domains: ["mirrormedia.mg"] },
{ name: "ETtoday新闻云", name: "ETtoday新闻云 (东森新闻云)", domains: ["ettoday.net", "star.ettoday.net"] },
{ keyword: "德国之声", name: "德国之声 (DW)", domains: ["dw.com"] },
{ name: "辅仁媒体 (VJMedia)", domains: ["vjmedia.com.hk"] },
{ keyword: "多维新闻网", name: "多维新闻网 (已停止运营)", domains: ["dwnews.com"] },
{ name: "Russia Today (RT)", domains: ["rt.com", "russian.rt.com", "actualidad.rt.com", "телеканал RT"] },
{ name: "Sputnik (俄罗斯卫星通讯社)", domains: ["sputniknews.cn", "sputniknews.com", "sputnikglobe.com"] },
{ name: "半岛电视台 (Al Jazeera)", domains: ["aljazeera.com", "aljazeera.net"] }
];
let allReferencesCache = [];
let currentFilter = 'all';
const pageCSS = `
#quick-check-button-container { display: inline-block; vertical-align: bottom; margin-left: 0.2em; }
#quick-check-button {
display: block; padding: 0.5em 0.6em; cursor: pointer;
border-radius: 4px 4px 0 0; font-size: inherit; color: #0645ad;
font-weight: bold; text-decoration: none; line-height: 1.5;
background-color: transparent; border: 1px solid transparent;
}
#quick-check-button.qc-underline { text-decoration: underline !important; }
#p-views ul li#quick-check-button-container { padding: 0; }
#quick-check-button:hover { text-decoration: underline; background-color: #f0f0f0; }
#quick-check-button.qc-orange { background-color: #ffc107 !important; color: black !important; border-color: #dda600; text-decoration: none !important; }
#quick-check-button.qc-red { background-color: #dc3545 !important; color: white !important; border-color: #b02a37; text-decoration: none !important; }
#quick-check-modal { display: none; position: fixed; z-index: 200000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.7); font-family: sans-serif; }
#quick-check-modal-content { background-color: #f8f9fa; margin: 4% auto; padding: 20px 25px; border: 1px solid #a2a9b1; width: 90%; max-width: 750px; border-radius: 6px; box-shadow: 0 8px 25px rgba(0,0,0,0.4); display: flex; flex-direction: column; }
#quick-check-modal-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #a2a9b1; padding-bottom: 12px; margin-bottom: 10px; }
#quick-check-modal-content h2 { margin-top: 0; margin-bottom:0; font-size: 1.4em; color: #202122; }
#quick-check-modal-about-link { font-size: 0.8em; color: #0645ad; text-decoration: underline; cursor: pointer; margin-left: auto; padding-left: 15px; }
#quick-check-modal-about-link:hover { color: #0b0080; }
#quick-check-modal-close { color: #72777d; font-size: 28px; font-weight: bold; line-height: 1; cursor: pointer; padding: 0 5px; }
#quick-check-modal-close:hover, #quick-check-modal-close:focus { color: #000; text-decoration: none; }
#quick-check-about-section { display: none; font-size: 0.9em; padding: 10px; margin-top:10px; border: 1px solid #ccc; background-color: #f0f0f0; border-radius: 4px; }
#quick-check-about-section p { margin: 0.5em 0; }
#quick-check-about-section ul { margin: 0.5em 0 0.5em 20px; padding: 0; }
#quick-check-about-section li { margin-bottom: 0.3em; }
.quick-check-source-list { flex-grow: 1; overflow-y: auto; margin-top: 5px; min-height: 100px; /* Ensure it has some height */ }
.quick-check-source-item { padding: 10px 5px; border-bottom: 1px solid #dee2e6; }
.quick-check-source-item:last-child { border-bottom: none; }
.quick-check-source-item .source-text { display: block; margin-bottom: 6px; color: #202122; font-size: 0.9em; }
.quick-check-source-item .source-url { font-size: 0.8em; color: #0645ad; word-break: break-all; }
.quick-check-source-item .source-url a { color: #0645ad; }
.quick-check-source-item .source-status { font-weight: bold; padding: 4px 8px; border-radius: 4px; font-size: 0.8em; margin-left: 8px; display: inline-block; vertical-align: middle; margin-top: 4px; }
.quick-check-source-item .source-status-red { background-color: #dc3545; color: white; }
.quick-check-source-item .source-status-orange { background-color: #ffc107; color: black; }
.quick-check-source-item .source-status-blue { background-color: #cfe2ff; color: #004085; border: 1px solid #b8daff; }
#quick-check-summary { margin-bottom: 10px; font-weight: bold; padding: 10px; background-color: #e9ecef; border-radius: 4px; font-size: 0.9em; }
#quick-check-summary .status-count, #quick-check-summary .summary-total { margin-right: 8px; cursor: pointer; padding: 3px 5px; border-radius: 3px; display: inline-block; margin-bottom: 5px; }
#quick-check-summary .status-count:hover, #quick-check-summary .summary-total:hover { text-decoration: underline; background-color: #d1d5db; }
#quick-check-summary .status-count-red { color: #d00; }
#quick-check-summary .status-count-orange { color: #c60; }
#quick-check-summary .status-count-blue { color: #004085; }
#quick-check-summary .status-count-green { color: #080; }
#quick-check-summary .summary-total { color: #333; }
#quick-check-summary .active-filter { background-color: #60a5fa !important; color: white !important; text-decoration: none !important; }
body.skin-minerva #p-views ul, body.skin-minerva #quick-check-button-container { display: flex !important; }
body.skin-minerva #quick-check-button { padding: 0.8em 0.6em; }
`;
function init() {
addCustomCSS(pageCSS);
const historyTabSelectors = [
'#ca-history', '#ca-history a', '#p-views ul li#ca-history',
'.tabs ul li a[href*="action=history"]', '#pagehistory',
'.vector-page-toolbar-container .vector-page-tools-landmark li#ca-history a',
'.mw-portlet-ca-history a', '#mw-content-actions-history a',
'.page-actions-menu__list-item--history a', '#page-actions a[href*="action=history"]',
'#p-views li[id="ca-history"] a'
];
let historyTabElement = null; let PviewsUl = null;
for (const selector of historyTabSelectors) {
historyTabElement = document.querySelector(selector);
if (historyTabElement) break;
}
if (historyTabElement) {
const parentLi = historyTabElement.closest('li');
if (parentLi && parentLi.parentNode && parentLi.parentNode.tagName === 'UL') PviewsUl = parentLi.parentNode;
}
if (!PviewsUl) PviewsUl = document.querySelector('#p-views ul');
let targetInsertionParent = null; let insertAfterThisElement = null; let appendToEnd = false;
if (historyTabElement) {
const parentLi = historyTabElement.closest('li');
if (parentLi && parentLi.parentNode) { targetInsertionParent = parentLi.parentNode; insertAfterThisElement = parentLi; }
else if (PviewsUl) { targetInsertionParent = PviewsUl; insertAfterThisElement = PviewsUl.querySelector('li:last-child'); appendToEnd = !insertAfterThisElement; }
else {
const altContainers = ['.vector-menu-tabs ul', '#mw-panel .vector-menu-portal .vector-menu-tabs ul', '.page-actions-menu__list', '.vector-page-tools ul', '#mw-content-actions ul', '#p-cactions ul', historyTabElement.parentElement];
for (const sel of altContainers) { targetInsertionParent = typeof sel === 'string' ? document.querySelector(sel) : sel; if (targetInsertionParent) { appendToEnd = true; break; } }
}
} else if (PviewsUl) { targetInsertionParent = PviewsUl; appendToEnd = true; }
else {
const fallbackContainers = ['.vector-page-tools ul', '.vector-page-toolbar-container .vector-page-tools-landmark ul', '#mw-content-actions ul', '#p-cactions ul', '#mw-content-actions', '#p-cactions .pBody > ul', document.getElementById('p-navigation') ? document.getElementById('p-navigation').querySelector('ul') : null];
for (const sel of fallbackContainers) { targetInsertionParent = typeof sel === 'string' ? document.querySelector(sel) : sel; if (targetInsertionParent) { appendToEnd = true; break; } }
}
if (targetInsertionParent) insertButton(targetInsertionParent, insertAfterThisElement, appendToEnd);
else { console.error("中文维基百科来源快检: 最终未能找到合适的标签容器来插入按钮。"); return; }
createModal(); scanSourcesAndUpdateButton();
}
function insertButton(parentElement, afterElement = null, appendToEnd = false) {
if (!parentElement) return;
if (document.getElementById('quick-check-button-container')) return;
const newLi = document.createElement('li'); newLi.id = 'quick-check-button-container';
if (afterElement && afterElement.className && typeof afterElement.className === 'string') newLi.className = afterElement.className;
else if (parentElement.firstElementChild && parentElement.firstElementChild.tagName === 'LI' && parentElement.firstElementChild.className && typeof parentElement.firstElementChild.className === 'string') newLi.className = parentElement.firstElementChild.className;
if (newLi.style.paddingLeft || newLi.style.paddingRight) { newLi.style.paddingLeft = '0'; newLi.style.paddingRight = '0';}
const quickCheckLink = document.createElement('a'); quickCheckLink.id = 'quick-check-button'; quickCheckLink.href = '#';
quickCheckLink.textContent = '快检'; quickCheckLink.title = '快速检查此页面的引用来源可靠性';
quickCheckLink.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); showReport(); });
newLi.appendChild(quickCheckLink);
let siblingA = null;
if (afterElement && afterElement.querySelector('a')) siblingA = afterElement.querySelector('a');
else if (parentElement.querySelector('li > a')) siblingA = parentElement.querySelector('li > a');
if (siblingA && siblingA.className && typeof siblingA.className === 'string') {
if (!siblingA.id || siblingA.id === "" || siblingA.id.startsWith("ca-") || siblingA.id.startsWith("pt-")) quickCheckLink.className = siblingA.className;
quickCheckLink.id = 'quick-check-button';
}
if (appendToEnd) parentElement.appendChild(newLi);
else if (afterElement && afterElement.parentNode === parentElement) parentElement.insertBefore(newLi, afterElement.nextSibling);
else parentElement.appendChild(newLi);
}
function createModal() {
if (document.getElementById('quick-check-modal')) return;
const modalHTML = `
<div id="quick-check-modal">
<div id="quick-check-modal-content">
<div id="quick-check-modal-header">
<h2>引用来源快速检查报告</h2>
<span id="quick-check-modal-about-link">关于</span>
<span id="quick-check-modal-close" title="关闭">×</span>
</div>
<div id="quick-check-about-section">
<p><strong>中文维基百科来源快检脚本 (v1.3)</strong></p>
<p>此脚本旨在帮助编辑者快速识别维基百科条目中可能存在问题的引用来源。它会根据预设的列表将来源分为不同等级:</p>
<ul>
<li><strong>红色警报:</strong> 通常为高度不可靠、不应使用的来源,或需要严格审查的用户生成内容。</li>
<li><strong>橙色关注:</strong> 来源存在争议、可能有偏见或可靠性较低,需谨慎使用。</li>
<li><strong>蓝色关注:</strong> 来源值得进一步关注,可能因其特定立场、报道角度或在某些情况下可靠性存疑。按钮会以下划线提示。</li>
<li><strong>常规来源:</strong> 未在上述列表中发现的来源,但这并不保证其绝对可靠,仍需编辑者自行判断。</li>
</ul>
<p>脚本通过匹配来源名称中的关键词及URL中的域名进行判断。点击报告中的摘要数字可以筛选对应类别的来源。</p>
<p><strong>重要:</strong> 此工具仅为辅助,不能替代编辑者的人工判断和来源核查责任。列表并非详尽无遗,来源的可靠性评估也可能随时间变化。</p>
</div>
<div id="quick-check-summary">正在分析...</div>
<div class="quick-check-source-list" id="quick-check-results"></div>
</div>
</div>`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
document.getElementById('quick-check-modal-close').addEventListener('click', () => {
document.getElementById('quick-check-modal').style.display = 'none';
});
window.addEventListener('click', (event) => {
if (event.target == document.getElementById('quick-check-modal')) {
document.getElementById('quick-check-modal').style.display = 'none';
}
});
document.getElementById('quick-check-modal-about-link').addEventListener('click', () => {
const aboutSection = document.getElementById('quick-check-about-section');
aboutSection.style.display = aboutSection.style.display === 'none' || aboutSection.style.display === '' ? 'block' : 'none';
});
}
function getReferences() {
const references = [];
const refListItems = document.querySelectorAll('ol.references li[id^="cite_note-"], ul.references li[id^="cite_note-"], ol[class*="references-group-"] li[id^="cite_note-"]');
refListItems.forEach((item, index) => {
let sourceText = ''; let sourceUrl = '';
const refTextElement = item.querySelector('.reference-text');
if (refTextElement) sourceText = refTextElement.innerText;
else { const tempClone = item.cloneNode(true); tempClone.querySelectorAll('.mw-cite-backlink, .reference-accessdate, .mw-reference-text-cite-sep').forEach(el => el.remove()); sourceText = tempClone.innerText; }
const links = item.querySelectorAll('.reference-text a[href], .citation a[href], span.reference-text > a[href], .ref-content a[href]');
let primaryLinkElement = null;
if (links.length > 0) {
primaryLinkElement = Array.from(links).find(l => { const href = l.getAttribute('href'); if (!href) return false; return (l.classList.contains('external') || href.startsWith('http')) && !href.includes('archive.org') && !href.includes('web.archive.org') && !href.includes('webcitation.org') && !href.startsWith(window.location.origin + '/wiki/') && !href.startsWith(window.location.origin + '/w/') && !l.closest('.mw-cite-backlink'); });
if (!primaryLinkElement) primaryLinkElement = Array.from(links).find(l => { const href = l.getAttribute('href'); if (!href) return false; return (l.classList.contains('external') || href.startsWith('http')) && !l.closest('.mw-cite-backlink'); });
if (!primaryLinkElement && links.length > 0) primaryLinkElement = Array.from(links).find(l => l.getAttribute('href') && !l.closest('.mw-cite-backlink')) || links[0];
}
if (primaryLinkElement && primaryLinkElement.getAttribute('href')) {
sourceUrl = primaryLinkElement.getAttribute('href');
if (!sourceText.trim() || sourceText.trim() === sourceUrl || sourceText.length < 20 || sourceText.startsWith('^')) {
const linkText = primaryLinkElement.innerText.trim();
if (linkText && linkText !== sourceUrl && linkText.length > 5) sourceText = linkText;
else if (sourceText.startsWith('^') && sourceText.length > 1) { const potentialText = sourceText.substring(1).trim(); if (potentialText.length > (linkText ? linkText.length : 0)) sourceText = potentialText; }
}
}
sourceText = sourceText.replace(/^[\s\^·↑↓มากที่สุด]*(跳转至:|Jump up to:|内容|导航|搜索|参见:|参看:|另见:|參見:|參看:|參閱:|見:|參:)?/i, '').trim();
sourceText = sourceText.replace(/(\s*\[\d+\])*\s*((查阅日期.*?)|\(accessed .*?\)| retrieved .*?)?\s*$/i, '').trim();
sourceText = sourceText.replace(/\s*(\.|。)\s*$/, '').trim();
sourceText = sourceText.replace(/^引用错误:无效的.+标记.+$/, "无法解析的引用模板").trim();
if (sourceText.length === 0 && sourceUrl) sourceText = sourceUrl;
references.push({ id: item.id || `ref-${index}`, text: sourceText || "无法提取引用文本", url: sourceUrl.trim() });
});
return references;
}
function isTechBlogWhitelisted(text, url) {
const lowerText = text.toLowerCase();
const lowerUrl = url.toLowerCase();
if (!url && !text) return false; // Should not happen with valid refs
for (const item of techBlogWhitelist) {
const lowerItem = item.toLowerCase().trim();
// Check if the whitelist item is a path/domain part or a text keyword
if (lowerItem.includes('/') || lowerItem.includes('.')) { // Likely a domain or path
if (lowerUrl.includes(lowerItem)) return true;
} else if (lowerItem.startsWith(" ") && lowerText.includes(lowerItem)) { // Keyword (prefixed with space)
return true;
} else if (lowerText.includes(lowerItem)) { // General keyword in text
return true;
}
}
// Specific check for openai.com/blog against blog.openai.com
if (lowerUrl.includes("blog.openai.com") && techBlogWhitelist.some(entry => entry === "openai.com/blog")) return true;
return false;
}
function checkSource(text, url) {
const lowerText = text.toLowerCase(); const lowerUrl = url.toLowerCase();
const blogKeywords = ["blog", "博客", "网志"];
const isPotentiallyBlog = blogKeywords.some(kw => lowerText.includes(kw)) || lowerUrl.includes("blog");
if (isPotentiallyBlog && isTechBlogWhitelisted(text, url)) {
// Whitelisted tech blog: don't mark as blog-related red.
// Allow other rules to match if the URL/text also matches another category.
} else if (isPotentiallyBlog) {
let matchedPlatformDomain = null;
const isRedListedBlogPlatform = redSources.some(src => {
if (src.name && src.name.startsWith("博客/网志 (平台)") && src.domains) {
const matchedDomain = src.domains.find(d => lowerUrl.includes(d.toLowerCase()));
if (matchedDomain) {
matchedPlatformDomain = matchedDomain;
return true;
}
}
return false;
});
if (isRedListedBlogPlatform && matchedPlatformDomain) {
return { status: 'red', name: '博客/网志 (平台)', matched: matchedPlatformDomain };
}
return { status: 'red', name: '博客/网志 (需审核)', matched: blogKeywords.find(kw => lowerText.includes(kw)) || 'blog in URL' };
}
for (const src of redSources) {
if (src.name && src.name.startsWith("博客/网志") && (src.keyword === "blog" || src.keyword === "博客" || src.keyword === "网志" || src.keyword === "个人网站")) {
continue; // Already handled by the more specific blog logic above
}
if (src.keyword && lowerText.includes(src.keyword.toLowerCase())) {
return { status: 'red', name: src.name, matched: src.keyword };
}
if (src.domains) {
for (const domain of src.domains) {
const normalizedDomain = domain.toLowerCase();
if (lowerUrl.includes(`//${normalizedDomain}/`) || lowerUrl.endsWith(`//${normalizedDomain}`) || lowerUrl.includes(`.${normalizedDomain}/`) || lowerUrl.endsWith(`.${normalizedDomain}`) || lowerUrl.includes(`//${normalizedDomain}:`) || lowerUrl.includes(`.${normalizedDomain}:`)) {
return { status: 'red', name: src.name, matched: domain };
}
}
}
}
for (const src of orangeSources) {
if (src.keyword && lowerText.includes(src.keyword.toLowerCase())) return { status: 'orange', name: src.name, matched: src.keyword };
if (src.domains) {
for (const domain of src.domains) { const normalizedDomain = domain.toLowerCase(); if (lowerUrl.includes(`//${normalizedDomain}/`) || lowerUrl.endsWith(`//${normalizedDomain}`) || lowerUrl.includes(`.${normalizedDomain}/`) || lowerUrl.endsWith(`.${normalizedDomain}`) || lowerUrl.includes(`//${normalizedDomain}:`) || lowerUrl.includes(`.${normalizedDomain}:`)) return { status: 'orange', name: src.name, matched: domain }; }
}
}
for (const src of blueSources) {
if (src.keyword && lowerText.includes(src.keyword.toLowerCase())) return { status: 'blue', name: src.name, matched: src.keyword };
if (src.domains) {
for (const domain of src.domains) { const normalizedDomain = domain.toLowerCase(); if (lowerUrl.includes(`//${normalizedDomain}/`) || lowerUrl.endsWith(`//${normalizedDomain}`) || lowerUrl.includes(`.${normalizedDomain}/`) || lowerUrl.endsWith(`.${normalizedDomain}`) || lowerUrl.includes(`//${normalizedDomain}:`) || lowerUrl.includes(`.${normalizedDomain}:`)) return { status: 'blue', name: src.name, matched: domain }; }
}
}
return { status: 'green', name: '未发现已知风险来源' };
}
function scanSourcesAndUpdateButton() {
const references = getReferences();
let hasRed = false; let hasOrange = false; let hasBlue = false;
const button = document.getElementById('quick-check-button');
if (!button) { return; }
button.classList.remove('qc-red', 'qc-orange', 'qc-underline');
if (references.length === 0) return;
references.forEach(ref => {
const result = checkSource(ref.text, ref.url);
if (result.status === 'red') hasRed = true;
else if (result.status === 'orange') hasOrange = true;
else if (result.status === 'blue') hasBlue = true;
});
if (hasRed) button.classList.add('qc-red');
else if (hasOrange) button.classList.add('qc-orange');
else if (hasBlue) button.classList.add('qc-underline');
}
function renderReportItems(filterType = 'all') {
const resultsDiv = document.getElementById('quick-check-results');
if (!resultsDiv) return;
resultsDiv.innerHTML = '';
allReferencesCache.forEach(refWithResult => {
const { ref, result } = refWithResult;
if (filterType !== 'all' && result.status !== filterType) return;
const itemDiv = document.createElement('div'); itemDiv.className = 'quick-check-source-item';
let statusClass = ''; let reportStatusText = `状态: ${result.name}`;
if (result.status === 'red') statusClass = 'source-status-red';
else if (result.status === 'orange') statusClass = 'source-status-orange';
else if (result.status === 'blue') statusClass = 'source-status-blue';
let displayText = ref.text; if (displayText.length > 350) displayText = displayText.substring(0, 347) + "...";
const tempDiv = document.createElement('div'); tempDiv.textContent = displayText; const sanitizedDisplayText = tempDiv.innerHTML;
itemDiv.innerHTML = `<span class="source-text"><strong>引用 (<a href="#${encodeURIComponent(ref.id)}" title="跳转到引用">${ref.id || 'N/A'}</a>):</strong> ${sanitizedDisplayText}</span> ${ref.url ? `<span class="source-url"><strong>链接:</strong> <a href="${ref.url}" target="_blank" rel="noopener noreferrer">${ref.url}</a></span>` : '<span class="source-url">无有效链接</span>'} ${result.status !== 'green' ? `<span class="source-status ${statusClass}">${reportStatusText} ${result.matched ? `(匹配: ${result.matched})` : ''}</span>` : `<span class="source-status" style="color: #080;">${reportStatusText}</span>`}`;
const refLink = itemDiv.querySelector(`a[href="#${encodeURIComponent(ref.id)}"]`);
if (refLink) {
refLink.addEventListener('click', (e) => {
e.preventDefault(); const modal = document.getElementById('quick-check-modal'); if(modal) modal.style.display = 'none';
const targetElement = document.getElementById(ref.id);
if (targetElement) {
targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
targetElement.style.transition = 'background-color 0.3s ease-out, box-shadow 0.3s ease-out';
targetElement.style.backgroundColor = '#fff3cd'; targetElement.style.boxShadow = '0 0 10px #ffc107';
setTimeout(() => { targetElement.style.backgroundColor = ''; targetElement.style.boxShadow = ''; }, 2000);
}
});
}
resultsDiv.appendChild(itemDiv);
});
}
function updateSummaryActiveFilter(newFilter) {
currentFilter = newFilter;
const summaryDiv = document.getElementById('quick-check-summary');
if (!summaryDiv) return;
['summary-total', 'summary-red', 'summary-orange', 'summary-blue', 'summary-green'].forEach(id => {
const el = summaryDiv.querySelector(`#${id}`); if (el) el.classList.remove('active-filter');
});
const activeElId = newFilter === 'all' ? 'summary-total' : `summary-${newFilter}`;
const activeEl = summaryDiv.querySelector(`#${activeElId}`); if (activeEl) activeEl.classList.add('active-filter');
}
function showReport() {
allReferencesCache = [];
const references = getReferences();
let redCount = 0, orangeCount = 0, blueCount = 0, greenCount = 0;
if (references.length === 0) {
const summaryDiv = document.getElementById('quick-check-summary');
const resultsDiv = document.getElementById('quick-check-results');
if(summaryDiv) summaryDiv.innerHTML = "怎么会这样?🤦♂️<br>这个条目没有任何来源!也可能是JS出现了问题,或者条目格式不规范。请检查控制台和条目自身的信息以核实。";
if(resultsDiv) resultsDiv.innerHTML = '';
const modal = document.getElementById('quick-check-modal');
if(modal) modal.style.display = 'block';
return;
}
references.forEach(ref => {
const result = checkSource(ref.text, ref.url);
allReferencesCache.push({ ref, result });
if (result.status === 'red') redCount++;
else if (result.status === 'orange') orangeCount++;
else if (result.status === 'blue') blueCount++;
else greenCount++;
});
const summaryDiv = document.getElementById('quick-check-summary');
const modal = document.getElementById('quick-check-modal');
if (!summaryDiv || !modal) { createModal(); if(!document.getElementById('quick-check-summary') || !document.getElementById('quick-check-modal')) { console.error("中文维基百科来源快检: 模态框关键元素仍未找到。"); return; }}
summaryDiv.innerHTML = `
<span id="summary-total" class="summary-total" title="点击显示所有引用">总计 ${references.length} 个引用。</span>
<span id="summary-red" class="status-count status-count-red" title="点击仅显示红色警报">红色警报: ${redCount}</span>
<span id="summary-orange" class="status-count status-count-orange" title="点击仅显示橙色关注">橙色关注: ${orangeCount}</span>
<span id="summary-blue" class="status-count status-count-blue" title="点击仅显示蓝色关注">蓝色关注: ${blueCount}</span>
<span id="summary-green" class="status-count status-count-green" title="点击仅显示常规来源">常规来源: ${greenCount}</span>`;
summaryDiv.querySelector('#summary-total').addEventListener('click', () => { updateSummaryActiveFilter('all'); renderReportItems('all'); });
summaryDiv.querySelector('#summary-red').addEventListener('click', () => { updateSummaryActiveFilter('red'); renderReportItems('red'); });
summaryDiv.querySelector('#summary-orange').addEventListener('click', () => { updateSummaryActiveFilter('orange'); renderReportItems('orange'); });
summaryDiv.querySelector('#summary-blue').addEventListener('click', () => { updateSummaryActiveFilter('blue'); renderReportItems('blue'); });
summaryDiv.querySelector('#summary-green').addEventListener('click', () => { updateSummaryActiveFilter('green'); renderReportItems('green'); });
updateSummaryActiveFilter('all'); renderReportItems('all');
if (modal) modal.style.display = 'block';
}
function initializeScript() {
if (document.getElementById('quick-check-button-container')) { scanSourcesAndUpdateButton(); return; }
init();
}
if (typeof mw !== 'undefined' && typeof mw.hook === 'function') {
mw.hook('wikipage.content').add(() => { setTimeout(initializeScript, 450); });
if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(initializeScript, 450); }
else { document.addEventListener('DOMContentLoaded', () => { setTimeout(initializeScript, 450); }); }
} else {
if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(initializeScript, 450); }
else { document.addEventListener('DOMContentLoaded', () => { setTimeout(initializeScript, 450); }); }
}
})();