// ==UserScript== // @name B站(bilibili)链接参数净化 // @namespace You Boy // @version 1.2 // @description 清理B站链接追踪参数,支持自定义、批量添加和重置规则,性能最优,无页面侵入。通过扩展菜单打开设置面板。 // @author You Boy // @match *://*.bilibili.com/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @run-at document-start // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; // --- 1. 配置与存储 --- // 由脚本 https://greasyfork.org/zh-CN/scripts/393995-bilibili-%E5%B9%B2%E5%87%80%E9%93%BE%E6%8E%A5 提供 const DEFAULT_PARAMS = [ 'spm_id_from', 'from_source', 'msource', 'bsource', 'seid', 'source', 'session_id', 'visit_id', 'sourceFrom', 'from_spmid', 'share_source', 'share_medium', 'share_plat', 'share_session_id', 'share_tag', 'unique_k', 'csource', 'vd_source', 'tab', 'is_story_h5', 'share_from', 'plat_id', '-Arouter', 'spmid', ]; // 从存储加载参数,若不存在则使用默认值。 const paramsToRemove = new Set(GM_getValue('customParams', DEFAULT_PARAMS)); // 将当前参数配置保存到持久化存储中。 function saveParams() { GM_setValue('customParams', Array.from(paramsToRemove)); } // --- 2. 核心净化逻辑 --- /** * 从给定的URL字符串中移除追踪参数。 * @param {string} urlString - 需要净化的URL。 * @returns {string} - 净化后的URL。 */ function cleanUrl(urlString) { if (!urlString || !urlString.startsWith('http')) { return urlString; } // 为避免破坏功能,不处理登录相关URL。 if (urlString.includes('passport.bilibili.com')) { return urlString; } try { const url = new URL(urlString); let modified = false; const params = [...url.searchParams.keys()]; for (const param of params) { if (paramsToRemove.has(param)) { url.searchParams.delete(param); modified = true; } } return modified ? url.toString() : urlString; } catch (e) { // 若URL解析失败,返回原始字符串以避免脚本错误。 return urlString; } } // --- 3. 基于事件的链接净化策略 --- /** * 策略一:鼠标悬停时预净化与缓存。 * 此策略通过在用户悬停时主动净化链接,并被缓存在`data-cleaned-href`属性中,避免了重复计算和重复绑定事件的性能问题。 */ document.addEventListener('mouseover', event => { const link = event.target.closest('a[href]'); // 仅在链接存在且尚未被净化和缓存时执行 if (link && !link.dataset.cleanedHref) { const cleanedHref = cleanUrl(link.href); // 将净化后的链接存入缓存 link.dataset.cleanedHref = cleanedHref; // 立即更新链接的href属性,以提供即时反馈 link.href = cleanedHref; } }, true); /** * 策略二:终极点击防御。 * 此函数在点击生命周期的每一个关键阶段运行, * 构成一个无法被绕过的防御体系。它确保链接在导航前绝对干净,并阻止其他脚本的干扰。 */ const finalClickFix = e => { const link = e.target.closest('a[href]'); if (link) { const cleanedHref = link.dataset.cleanedHref || cleanUrl(link.href); if (link.href !== cleanedHref) { link.href = cleanedHref; } // 这是最关键的一步:阻止任何其他脚本在点击的任何阶段进行干预。 e.stopImmediatePropagation(); } }; // 将终极防御应用于整个点击生命周期。 document.addEventListener('mousedown', finalClickFix, true); document.addEventListener('click', finalClickFix, true); document.addEventListener('contextmenu', finalClickFix, true); // --- 4. 导航补丁 --- // 劫持浏览器的history API,以净化通过脚本进行的页面导航。 const originalPushState = history.pushState; history.pushState = function (state, title, url) { return originalPushState.apply(this, [state, title, cleanUrl(url ? url.toString() : '')]); }; const originalReplaceState = history.replaceState; history.replaceState = function (state, title, url) { return originalReplaceState.apply(this, [state, title, cleanUrl(url ? url.toString() : '')]); }; // 劫持 window.open,确保打开的新窗口URL也是干净的。 const originalOpen = window.open; window.open = function (url, target, features) { return originalOpen.apply(this, [cleanUrl(url ? url.toString() : ''), target, features]); }; // 为Navigation API 打补丁。 if (window.navigation) { window.navigation.addEventListener('navigate', e => { if (!e.canIntercept) return; const destinationUrl = e.destination.url; const cleanedUrl = cleanUrl(destinationUrl); if (destinationUrl !== cleanedUrl) { // 静默地修正URL,而无需触发新的导航事件,避免循环。 e.preventDefault(); history.replaceState(history.state, '', cleanedUrl); } }); } // --- 5. 懒加载设置面板 --- let settingsPanel = null; // 用于存储面板的DOM引用,实现单例模式。 function createSettingsPanel() { if (settingsPanel) return; settingsPanel = document.createElement('div'); settingsPanel.id = 'blc-settings-panel'; document.body.appendChild(settingsPanel); // 使用事件委托来处理面板内的所有点击事件。 settingsPanel.addEventListener('click', e => { const targetId = e.target.id; if (targetId === 'blc-close-btn') { settingsPanel.style.display = 'none'; } else if (targetId === 'blc-add-btn') { addParamsFromInput(); } else if (targetId === 'blc-reset-btn') { if (confirm('确定要重置为默认列表吗?')) { paramsToRemove.clear(); DEFAULT_PARAMS.forEach(p => paramsToRemove.add(p)); saveParams(); renderPanelContent(); } } else if (e.target.classList.contains('blc-delete')) { paramsToRemove.delete(e.target.dataset.param); saveParams(); renderPanelContent(); } }); settingsPanel.addEventListener('keydown', e => { if (e.key === 'Enter' && e.target.id === 'blc-new-param') { addParamsFromInput(); } }); } // 渲染面板的内部HTML。 function renderPanelContent() { if (!settingsPanel) return; settingsPanel.innerHTML = `

链接清理参数列表

×
${Array.from(paramsToRemove).sort().map(p => `
${p} ×
`).join('')}
`; document.getElementById('blc-new-param').focus(); } // 从输入框读取并添加新参数。 function addParamsFromInput() { const input = document.getElementById('blc-new-param'); if (!input) return; const newParams = input.value.split(',') .map(p => p.trim()) .filter(p => p); // 过滤掉无效的空字符串 if (newParams.length > 0) { newParams.forEach(p => paramsToRemove.add(p)); saveParams(); input.value = ''; renderPanelContent(); } } // --- 6. 初始化 --- // 在页面加载时执行一次URL清理 const currentUrl = window.location.href; const cleanedPageUrl = cleanUrl(currentUrl); if (currentUrl !== cleanedPageUrl) { history.replaceState(history.state, '', cleanedPageUrl); } // 注册用户菜单命令,用于打开设置面板。 GM_registerMenuCommand('设置', () => { if (settingsPanel && settingsPanel.style.display !== 'none') { settingsPanel.style.display = 'none'; return; } createSettingsPanel(); renderPanelContent(); settingsPanel.style.display = 'flex'; }); // 注入设置面板所需的CSS样式。 GM_addStyle(` #blc-settings-panel { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10000; width: 450px; max-width: 90vw; height: 400px; max-height: 80vh; background: #fff; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); flex-direction: column; } .blc-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; border-bottom: 1px solid #eee; flex-shrink: 0; } .blc-header h3 { margin: 0; font-size: 16px; } #blc-close-btn { font-size: 24px; cursor: pointer; color: #999; } .blc-add { display: flex; padding: 10px 15px; border-bottom: 1px solid #eee; flex-shrink: 0; } #blc-new-param { flex-grow: 1; border: 1px solid #ccc; border-radius: 4px; padding: 8px; margin-right: 10px; } #blc-add-btn, #blc-reset-btn { border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; } #blc-add-btn { background-color: #00a1d6; color: #fff; margin-right: 5px; } #blc-add-btn:hover { background-color: #00b5e5; } #blc-reset-btn { background-color: #f1f1f1; color: #333; border: 1px solid #ccc; } #blc-reset-btn:hover { background-color: #e0e0e0; } .blc-list { padding: 10px; overflow-y: auto; flex-grow: 1; display: flex; flex-wrap: wrap; align-content: flex-start; } .blc-param { display: inline-flex; align-items: center; background: #eef0f2; color: #333; padding: 5px 10px; border-radius: 15px; margin: 5px; font-size: 14px; } .blc-param span { margin-right: 8px; } .blc-delete { color: #999; cursor: pointer; font-weight: bold; font-size: 16px; } .blc-delete:hover { color: #ff4d4d; } `); })();