Greasy Fork

Greasy Fork is available in English.

B站(bilibili)链接参数净化

清理B站链接追踪参数,支持自定义、批量添加和重置规则,性能最优,无页面侵入。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         B站(bilibili)链接参数净化
// @namespace    You Boy
// @version      1.5.1
// @description  清理B站链接追踪参数,支持自定义、批量添加和重置规则,性能最优,无页面侵入。
// @author       You Boy
// @match        *://*.bilibili.com/*
// @icon         https://www.bilibili.com/favicon.ico
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // 如果当前窗口不是顶层窗口,则不执行脚本,防止在iframe中重复运行
    if (window.self !== window.top) {
        return;
    }

    // --- 1. Greasemonkey 沙箱环境 ---
    // 此部分负责GM API调用,并作为注入脚本的入口。
    // 默认参数 感谢脚本 http://greasyfork.icu/zh-CN/scripts/393995 提供
    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', 'live_from', 'launch_id', 'hotRank', 'trackid',
        'share_times', 'desc', 'bbid', 'buvid',
    ];

    const paramsToInject = GM_getValue('customParams', DEFAULT_PARAMS);

    // 监听注入脚本的保存请求 (页面环境 -> 沙箱)
    window.addEventListener('blc-save-params', (event) => {
        GM_setValue('customParams', event.detail);
    });

    // 注册菜单命令,点击时通知注入脚本打开设置面板
    GM_registerMenuCommand('设置', () => {
        window.dispatchEvent(new CustomEvent('blc-open-settings'));
    });

    // 注入UI样式
    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; }
    `);

    // --- 2. 待注入的工作代码 ---
    // 此函数将转换为字符串并注入页面,以突破沙箱限制。
    // 注意:此函数内部无法直接调用 GM_* API。
    function injectedCode() {
        // 确保代码在注入后立即执行并拥有独立作用域
        (() => {
            // 通过注入时挂载在script标签上的data属性,获取初始配置
            const scriptTag = document.getElementById('blc-injected-script');
            const initialParams = JSON.parse(scriptTag.dataset.initialParams);
            const defaultParams = JSON.parse(scriptTag.dataset.defaultParams);

            const paramsToRemove = new Set(initialParams);

            // 向沙箱中的“加载器”发送保存请求
            function saveParams() {
                window.dispatchEvent(new CustomEvent('blc-save-params', {
                    detail: Array.from(paramsToRemove)
                }));
            }

            // --- 核心净化逻辑 ---
            function cleanUrl(urlString) {
                if (!urlString) return urlString;
                // 登录等重要功能页面不处理
                if (urlString.includes('passport.bilibili.com')) return urlString;
                try {
                    const url = new URL(urlString, window.location.href);
                    let modified = false;
                    const paramsToDelete = [];
                    for (const param of url.searchParams.keys()) {
                        if (paramsToRemove.has(param)) {
                            paramsToDelete.push(param);
                            modified = true;
                        }
                    }

                    if (modified) {
                        paramsToDelete.forEach(p => url.searchParams.delete(p));
                        return url.toString();
                    }
                    return urlString;
                } catch (e) {
                    // 如果URL解析失败,返回原始字符串
                    return urlString;
                }
            }

            function extractAndClean(text) {
                if (!text || typeof text !== 'string') return text;

                // 正则匹配 http/https 链接
                // 常见的B站链接格式,也兼容短链 b23.tv
                const urlMatch = text.match(/(https?:\/\/(?:www\.|m\.)?bilibili\.com\/[^ ]+)|(https?:\/\/b23\.tv\/[a-zA-Z0-9]+)/);

                if (urlMatch) {
                    const rawUrl = urlMatch[0];
                    // 对提取出的 URL 进行参数净化
                    const cleanedUrl = cleanUrl(rawUrl);
                    console.log('[B站链接净化] 已从分享文本中提取并净化链接:', cleanedUrl);
                    return text.replace(rawUrl, cleanedUrl);
                }

                // 如果没匹配到链接,说明可能是复制普通评论或弹幕,不做处理
                return text;
            }

            // --- 事件净化策略 ---
            // 策略1: 鼠标悬停预处理,提升性能
            document.addEventListener('mouseover', event => {
                const link = event.target.closest('a[href]');
                if (link && !link.dataset.cleanedHref) {
                    const cleanedHref = cleanUrl(link.href);
                    if (link.href !== cleanedHref) {
                        link.dataset.cleanedHref = cleanedHref; // 缓存净化后的链接
                        link.href = cleanedHref;
                    }
                }
            }, true);

            // 策略2: 终极点击修复,拦截 mousedown/click/contextmenu 事件
            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;
                    }
                    // 阻止B站脚本在点击事件的后续阶段(如mouseup)重新污染链接
                    e.stopImmediatePropagation();
                }
            };
            document.addEventListener('mousedown', finalClickFix, true);
            document.addEventListener('click', finalClickFix, true);
            document.addEventListener('contextmenu', finalClickFix, true);


            // --- 导航补丁 (用于处理SPA页面跳转) ---
            // 拦截 history API
            const originalPushState = history.pushState;
            history.pushState = function (state, title, url) {
                const cleanedUrl = cleanUrl(url ? url.toString() : '');
                return originalPushState.apply(this, [state, title, cleanedUrl]);
            };

            const originalReplaceState = history.replaceState;
            history.replaceState = function (state, title, url) {
                const cleanedUrl = cleanUrl(url ? url.toString() : '');
                return originalReplaceState.apply(this, [state, title, cleanedUrl]);
            };

            // 拦截 window.open
            const originalOpen = window.open;
            window.open = function (url, target, features) {
                const cleanedUrl = cleanUrl(url ? url.toString() : '');
                return originalOpen.apply(this, [cleanedUrl, target, features]);
            };

            if (navigator.clipboard && navigator.clipboard.writeText) {
                const originalWriteText = navigator.clipboard.writeText;
                navigator.clipboard.writeText = function (text) {
                    // 尝试提取和净化
                    const newText = extractAndClean(text);
                    return originalWriteText.apply(this, [newText]);
                };
            }

            document.addEventListener('copy', (e) => {
                let selectedText = window.getSelection().toString();

                if (selectedText && (selectedText.includes('bilibili.com') || selectedText.includes('b23.tv'))) {
                    const cleanedText = extractAndClean(selectedText);

                    if (cleanedText !== selectedText) {
                        e.preventDefault(); 
                        e.clipboardData.setData('text/plain', cleanedText); 
                    }
                }
            }, true);

            // --- 懒加载设置面板 ---
            let settingsPanel = null;

            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();
                            defaultParams.forEach(p => paramsToRemove.add(p));
                            saveParams();
                            renderPanelContent();
                        }
                    } else if (e.target.classList.contains('blc-delete')) {
                        const paramToDelete = e.target.dataset.param;
                        paramsToRemove.delete(paramToDelete);
                        saveParams();
                        renderPanelContent();
                    }
                });

                settingsPanel.addEventListener('keydown', e => {
                    if (e.key === 'Enter' && e.target.id === 'blc-new-param') {
                        addParamsFromInput();
                    }
                });
            }

            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();
                }
            }

            // --- 初始化 ---
            // 检查并清理当前页面URL,防止直接打开带参数的链接
            const currentUrl = window.location.href;
            const cleanedPageUrl = cleanUrl(currentUrl);
            if (currentUrl !== cleanedPageUrl) {
                history.replaceState(history.state, '', cleanedPageUrl);
            }

            // 监听来自“加载器”的打开设置请求
            window.addEventListener('blc-open-settings', () => {
                if (settingsPanel && settingsPanel.style.display !== 'none') {
                    settingsPanel.style.display = 'none';
                    return;
                }
                // 确保body存在后再创建UI
                if (!document.body) {
                    document.addEventListener('DOMContentLoaded', () => {
                        createSettingsPanel();
                        renderPanelContent();
                        settingsPanel.style.display = 'flex';
                    });
                } else {
                    createSettingsPanel();
                    renderPanelContent();
                    settingsPanel.style.display = 'flex';
                }
            });
        })();
    }

    // --- 3. 执行注入 ---
    const injectedScript = document.createElement('script');
    injectedScript.id = 'blc-injected-script';
    // 通过data属性将配置数据“携带”到页面环境
    injectedScript.dataset.initialParams = JSON.stringify(paramsToInject);
    injectedScript.dataset.defaultParams = JSON.stringify(DEFAULT_PARAMS);
    injectedScript.textContent = `(${injectedCode.toString()})();`;

    // 尽早注入脚本以抢占B站脚本的API
    (document.head || document.documentElement).appendChild(injectedScript);
    injectedScript.remove();

})();