30秒,让AI写一个「跳过外链中转页」的油猴脚本

129 阅读4分钟

声明在先:纯技术分享,仅供学习交流。
不喜欢请默默关掉,不必喷我,喷了我也不回(手动狗头保命🐶)。

前段时间,看到一篇很有意思的文章,讲的是如何绕过那些“脱裤子放屁”式的外链跳转页面——就是你点开一个链接,不是直接跳走,而是先到一个中转页,提醒你“即将离开本站,请注意安全”的那种。

原文在这里 👉🏻
脱裤子放屁 - 你们讨厌这样的页面吗?一个浏览器插件,用于跳过掘金,知乎,少数派等网站的外链中转站页面。让你的互联网浏览 - 掘金

看完之后我就在想:这种事情,能不能让 AI 帮我写个脚本搞定?

于是,我打开了某 AI 工具,扔给它几句话,它噼里啪啦就给我吐出了一段油猴脚本。


📜 脚本代码(AI 生成版)

// ==UserScript==
// @name         Redirect Skipper
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  自动跳过链接中转页面,包含详细调试日志
// @author       Original idea from juejin.cn article
// @match        *://*/*
// @grant        GM_log
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // 调试开关 - 设置为 true 时在控制台输出详细日志
    const DEBUG = false;

    function debugLog(...args) {
        if (DEBUG) {
            console.log('[Redirect Skipper]', new Date().toLocaleTimeString(), ...args);
        }
    }

    function errorLog(...args) {
        console.error('[Redirect Skipper] ERROR:', ...args);
    }

    // 需要处理的网站列表
    let hostnames = ["juejin.cn", "sspai.com", "zhihu.com"];

    debugLog('脚本开始执行');
    debugLog('当前域名:', location.hostname);
    debugLog('处理的域名列表:', hostnames);

    /**
     * 检查当前域名是否在支持列表中
     */
    function checkCurrentHostname() {
        const currentHost = location.hostname;
        const isSupported = hostnames.some(host => {
            // 支持子域名匹配,比如 www.zhihu.com 匹配 zhihu.com
            return currentHost === host || currentHost.endsWith('.' + host);
        });

        debugLog('域名检查结果:', {
            '当前域名': currentHost,
            '是否支持': isSupported,
            '匹配规则': isSupported ? hostnames.find(h => currentHost === h || currentHost.endsWith('.' + h)) : '无'
        });

        return isSupported;
    }

    /**
     * 核心函数:查找并替换带有 ?target= 参数的链接
     */
    function findByTarget() {
        const isSupported = checkCurrentHostname();
        if (!isSupported) {
            debugLog('当前域名不在处理列表中,跳过执行');
            return;
        }

        debugLog('开始查找需要处理的链接...');

        const linkKeyword = "?target=";
        const selector = `a[href*="${linkKeyword}"]:not([data-redirect-skipper])`;
        debugLog('使用的选择器:', selector);

        try {
            const aLinks = document.querySelectorAll(selector);
            debugLog('找到的链接数量:', aLinks.length);

            if (!aLinks.length) {
                debugLog('未找到符合条件的链接');
                return;
            }

            aLinks.forEach((a, index) => {
                const originalHref = a.href;
                debugLog(`处理链接 ${index + 1}:`, originalHref);

                const targetIndex = originalHref.indexOf(linkKeyword);
                if (targetIndex !== -1) {
                    const encodedUrl = originalHref.substring(targetIndex + linkKeyword.length);
                    const decodedUrl = decodeURIComponent(encodedUrl);

                    debugLog(`  ✓ 解析结果:`, {
                        '原始URL': originalHref,
                        'target位置': targetIndex,
                        '编码后的目标URL': encodedUrl,
                        '解码后的目标URL': decodedUrl
                    });

                    // 验证URL是否有效
                    if (decodedUrl && decodedUrl.startsWith('http')) {
                        a.href = decodedUrl;
                        a.setAttribute("data-redirect-skipper", "true");
                        debugLog(`  ✓ 已替换为:`, decodedUrl);

                        // 添加点击事件监听,记录用户交互
                        a.addEventListener('click', function(e) {
                            debugLog('用户点击了已处理的链接:', decodedUrl);
                        }, { once: true });
                    } else {
                        errorLog('解析出的URL无效:', decodedUrl);
                    }
                } else {
                    debugLog(`  ✗ 未找到 ${linkKeyword} 参数`);
                }
            });

            debugLog('链接处理完成');

        } catch (err) {
            errorLog('执行过程中发生错误:', err);
        }
    }

    /**
     * 监听文档变化(用于处理动态加载的内容)
     */
    function observerDocument() {
        debugLog('开始监听文档变化...');

        const observer = new MutationObserver((mutationsList) => {
            debugLog('检测到文档变化,变更数量:', mutationsList.length);

            for (const mutation of mutationsList) {
                if (mutation.type === "childList" && mutation.addedNodes.length) {
                    debugLog('发现新增节点,数量:', mutation.addedNodes.length);
                    debugLog('新增节点:', mutation.addedNodes);
                    findByTarget();
                }
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        debugLog('MutationObserver 已启动');
        return observer;
    }

    /**
     * 扫描页面中的链接并统计信息
     */
    function scanPageLinks() {
        const allLinks = document.querySelectorAll('a[href]');
        const targetLinks = document.querySelectorAll('a[href*="?target="]');
        const processedLinks = document.querySelectorAll('a[data-redirect-skipper]');

        debugLog('页面链接统计:', {
            '总链接数': allLinks.length,
            '包含?target=的链接数': targetLinks.length,
            '已处理的链接数': processedLinks.length
        });

        // 列出所有包含?target=的链接
        targetLinks.forEach((link, i) => {
            debugLog(`链接${i}:`, link.href, '已处理:', link.hasAttribute('data-redirect-skipper'));
        });
    }

    // 主执行流程
    function init() {
        debugLog('===== 脚本初始化开始 =====');

        // 检查油猴环境
        if (typeof GM_info !== 'undefined') {
            debugLog('油猴脚本信息:', {
                '脚本名称': GM_info.script.name,
                '版本': GM_info.script.version,
                '运行位置': GM_info.scriptRunAt
            });
        }

        // 初始扫描
        scanPageLinks();
        findByTarget();

        // 启动DOM变化监听
        const observer = observerDocument();

        // 监听页面事件
        const events = ['load', 'hashchange', 'popstate'];
        events.forEach(event => {
            window.addEventListener(event, function() {
                debugLog(`事件触发: ${event}`);
                if (event === 'load') {
                    scanPageLinks();
                }
                findByTarget();
            });
        });

        // 为了调试,将关键函数暴露到全局
        if (DEBUG) {
            window.RedirectSkipper = {
                scanPageLinks,
                findByTarget,
                checkCurrentHostname,
                observer
            };
            debugLog('调试函数已暴露到 window.RedirectSkipper');
        }

        debugLog('===== 脚本初始化完成 =====');
    }

    // 延迟初始化,确保DOM已加载
    if (document.readyState === 'loading') {
        debugLog('文档还在加载中,等待DOMContentLoaded事件');
        document.addEventListener('DOMContentLoaded', init);
    } else {
        debugLog('文档已就绪,立即初始化');
        init();
    }

})();

🤖 我是怎么让 AI 写出来的?

其实提示词特别简单,大意是:

“帮我写一个油猴脚本,自动跳过掘金、知乎、少数派等网站的外链中转页,直接跳转到目标链接。”

第一次生成的代码运行后没生效,我就让它“加一下详细的日志,方便我调试”。结果第二次生成的版本直接就能用了,连调试都省了……

不得不说,这年头,善用 AI 真是提效大杀器。


⚙️ 如何自定义支持其他网站?

如果你希望这个脚本也处理其他网站,只需要修改代码开头 hostnames 数组。

比如加一个 example.com

let hostnames = ["juejin.cn", "sspai.com", "www.zhihu.com", "example.com"];

改完保存,刷新页面即可生效。


🧠 实现原理(一张图看懂)

简单来说,就是:

  1. 匹配当前域名是否在支持列表里;
  2. 找出所有带 ?target= 参数的链接;
  3. 提取 target 后面的真实地址,替换掉原链接;
  4. 监听页面变化,动态加载的内容也能处理。

💭 一些随想

为什么这么多网站都要加跳转页?

原文作者列了几点:

  • 防止钓鱼攻击
  • 增强用户安全意识
  • 品牌保护
  • 遵守法律法规
  • 控制流量去向

不过评论区很多人提到,SEO 可能是更关键的原因。毕竟外链如果出了问题,责任在用户点击,平台可以一定程度上规避风险。

无论初衷如何,最终多出来的那一次点击、多等的那一两秒,都是用户在承担……


温馨提示:本脚本仅供技术交流与学习,使用请遵守相关网站规定,注意网络安全。