Auto-clean ChatGPT Tracking Links

It offers two modes. It intelligently auto-cleans tracking links (i.e. links with tracking parameters) when you load the page by observing DOM additions and attribute changes, checking links in dynamic contents etc. It also includes a draggable button for manual cleaning in case if some links are left uncleaned.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Auto-clean ChatGPT Tracking Links
// @namespace    Violentmonkey userscripts by ReporterX
// @version      6.0
// @description  It offers two modes. It intelligently auto-cleans tracking links (i.e. links with tracking parameters) when you load the page by observing DOM additions and attribute changes, checking links in dynamic contents etc. It also includes a draggable button for manual cleaning in case if some links are left uncleaned.
// @author       ReporterX
// @match        *://chat.openai.com/*
// @match        *://chatgpt.com/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-idle
// @icon         https://www.google.com/s2/favicons?sz=64&domain=openai.com
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const trackingParams = new Set([
        'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id',
        'gclid', 'dclid', 'gclsrc', 'wbraid', 'gbraid', '_ga', '_gl', 'fbclid', 'igshid',
        'msclkid', 'mc_cid', 'mc_eid', '_hsenc', '_hsmi', 'hsCtaTracking', 'srsltid',
        'trk', '__tn__', '__cft__', '__biz', 'mkt_tok', 'vero_conv', 'vero_id', 'ef_id',
        's_kwcid', 'pk_campaign', 'pk_kwd', 'piwik_campaign', 'piwik_kwd', 'mtm_campaign',
        'matomo_campaign', 'hsa_acc', 'hsa_cam', 'hsa_grp', 'hsa_ad', 'hsa_src',
        'hsa_net', 'hsa_ver', 'spm', 'yclid', 'ysclid', 'rb_clickid', 'zanpid',
        'cjevent', 'cjdata', 'cr_cc'
    ]);

    function getCleanedUrl(urlString) {
        if (!urlString || !urlString.includes('?')) return null;
        try {
            const url = new URL(urlString);
            let hasChanged = false;
            for (const param of trackingParams) {
                if (url.searchParams.has(param)) {
                    url.searchParams.delete(param);
                    hasChanged = true;
                }
            }
            return hasChanged ? url.toString() : null;
        } catch (e) {
            return null;
        }
    }

    function cleanLinksInElement(element) {
        if (!element || typeof element.querySelectorAll !== 'function') return 0;
        const links = element.querySelectorAll('a[href]');
        let cleanedCount = 0;
        links.forEach(link => {
            let wasCleaned = false;
            const cleanedHref = getCleanedUrl(link.href);
            if (cleanedHref) {
                link.href = cleanedHref;
                wasCleaned = true;
            }
            if (link.hasAttribute('alt')) {
                const cleanedAlt = getCleanedUrl(link.getAttribute('alt'));
                if (cleanedAlt) {
                    link.setAttribute('alt', cleanedAlt);
                    wasCleaned = true;
                }
            }
            if (wasCleaned) {
                cleanedCount++;
            }
        });
        return cleanedCount;
    }

    function createCleanButton() {
        const button = document.createElement('button');
        button.textContent = 'Clean Links';
        button.id = 'clean-links-button';
        document.body.appendChild(button);

        let isMouseDown = false, isDragging = false;
        let startX, startY, initialButtonX, initialButtonY;
        const dragThreshold = 5;

        button.addEventListener('mousedown', (e) => {
            if (e.button !== 0) return;
            isMouseDown = true; isDragging = false;
            startX = e.clientX; startY = e.clientY;
            const rect = button.getBoundingClientRect();
            initialButtonX = rect.left; initialButtonY = rect.top;
            Object.assign(button.style, { bottom: 'unset', right: 'unset', top: `${initialButtonY}px`, left: `${initialButtonX}px`, cursor: 'grabbing' });
            e.preventDefault();
        });

        document.addEventListener('mousemove', (e) => {
            if (!isMouseDown) return;
            const deltaX = e.clientX - startX, deltaY = e.clientY - startY;
            if (!isDragging && Math.sqrt(deltaX**2 + deltaY**2) > dragThreshold) {
                isDragging = true;
            }
            if (isDragging) {
                Object.assign(button.style, { left: `${initialButtonX + deltaX}px`, top: `${initialButtonY + deltaY}px` });
            }
        });

        document.addEventListener('mouseup', () => {
            if (!isMouseDown) return;
            isMouseDown = false;
            button.style.cursor = 'pointer';
            if (isDragging) {
                GM_setValue('button_pos_x', button.style.left);
                GM_setValue('button_pos_y', button.style.top);
            }
        });

        button.addEventListener('click', () => {
            if (isDragging) return;
            const count = cleanLinksInElement(document.body);
            button.textContent = `Cleaned ${count} links.`; // <-- UPDATED TEXT
            button.disabled = true;
            setTimeout(() => {
                button.textContent = "Clean Links";
                button.disabled = false;
            }, 2500);
        });

        const savedX = GM_getValue('button_pos_x', null);
        const savedY = GM_getValue('button_pos_y', null);
        if (savedX && savedY) {
            Object.assign(button.style, { left: savedX, top: savedY, bottom: 'unset', right: 'unset' });
        }
    }

    function activateLiveCleaner() {
        const observer = new MutationObserver((mutationsList) => {
            let newlyCleanedCount = 0;
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            newlyCleanedCount += cleanLinksInElement(node);
                        }
                    });
                } else if (mutation.type === 'attributes') {
                    if (mutation.target.nodeType === Node.ELEMENT_NODE) {
                        newlyCleanedCount += cleanLinksInElement(mutation.target);
                    }
                }
            }
            if (newlyCleanedCount > 0) {
                console.log(`[Userscript] Auto-cleaned ${newlyCleanedCount} link(s) due to page update.`);
            }
        });

        const observerConfig = {
            childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class']
        };

        observer.observe(document.body, observerConfig);
        console.log('[Userscript] Ultimate automatic link cleaner is active.');
    }

    GM_addStyle(`
        #clean-links-button {
            position: fixed; bottom: 15px; left: 15px; z-index: 9999;
            background-color: #202123; color: #ececec; border: 1px solid #4d4d4f;
            border-radius: 8px; padding: 8px 12px; font-size: 14px; cursor: pointer;
            transition: background-color 0.3s, color 0.3s; user-select: none; white-space: nowrap;
        }
        #clean-links-button:hover { background-color: #343541; }
        #clean-links-button:disabled { background-color: #1a4731; color: #999; cursor: not-allowed; }
    `);

    window.addEventListener('load', () => {
        const initialCleanCount = cleanLinksInElement(document.body);
        if (initialCleanCount > 0) {
             console.log(`[Userscript] Initial page scan cleaned ${initialCleanCount} link(s).`);
        }
        createCleanButton();
        activateLiveCleaner();
    });
})();