Greasy Fork

Greasy Fork is available in English.

B站(bilibili)链接参数净化

清理B站链接追踪参数,支持自定义、批量添加和重置规则,性能最优,无页面侵入。通过扩展菜单打开设置面板。

当前为 2025-07-11 提交的版本,查看 最新版本

// ==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
// ==/UserScript==

(function () {
    'use strict';

    // --- 1. 配置与存储 ---
    // 由脚本 http://greasyfork.icu/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 = `
            <div class="blc-header">
                <h3>链接清理参数列表</h3>
                <span id="blc-close-btn">&times;</span>
            </div>
            <div class="blc-add">
                <input type="text" id="blc-new-param" placeholder="输入参数,用逗号,分隔批量添加"/>
                <button id="blc-add-btn">添加</button>
                <button id="blc-reset-btn">重置</button>
            </div>
            <div class="blc-list">
                ${Array.from(paramsToRemove).sort().map(p => `
                    <div class="blc-param">
                        <span>${p}</span>
                        <span class="blc-delete" data-param="${p}">&times;</span>
                    </div>
                `).join('')}
            </div>
        `;
        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; }
    `);

})();