Greasy Fork

Greasy Fork is available in English.

去除链接重定向

能原地解析的链接绝不在后台访问,去除重定向的过程快速且高效,平均时间在0.02ms~0.05ms之间。几乎没有任何在后台访问网页获取去重链接的操作,一切都在原地进行,对速度精益求精。去除网页内链接的重定向,具有高准确性和高稳定性,以及相比同类插件更低的时间占用。

当前为 2024-07-09 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name              去除链接重定向
// @author            Meriel
// @description       能原地解析的链接绝不在后台访问,去除重定向的过程快速且高效,平均时间在0.02ms~0.05ms之间。几乎没有任何在后台访问网页获取去重链接的操作,一切都在原地进行,对速度精益求精。去除网页内链接的重定向,具有高准确性和高稳定性,以及相比同类插件更低的时间占用。
// @version           2.0.9
// @namespace         Violentmonkey Scripts
// @update            2024-07-09
// @grant             GM.xmlHttpRequest
// @match             *://www.baidu.com/*
// @match             *://tieba.baidu.com/*
// @match             *://v.baidu.com/*
// @match             *://xueshu.baidu.com/*
// @include           *://www.google*
// @match             *://www.google.com/*
// @match             *://docs.google.com/*
// @match             *://mail.google.com/*
// @match             *://play.google.com/*
// @match             *://www.youtube.com/*
// @match             *://encrypted.google.com/*
// @match             *://www.so.com/*
// @match             *://www.zhihu.com/*
// @match             *://daily.zhihu.com/*
// @match             *://zhuanlan.zhihu.com/*
// @match             *://weibo.com/*
// @match             *://twitter.com/*
// @match             *://www.sogou.com/*
// @match             *://juejin.im/*
// @match             *://juejin.cn/*
// @match             *://mail.qq.com/*
// @match             *://addons.mozilla.org/*
// @match             *://www.jianshu.com/*
// @match             *://www.douban.com/*
// @match             *://getpocket.com/*
// @match             *://51.ruyo.net/*
// @match             *://steamcommunity.com/*
// @match             *://blog.csdn.net/*
// @match             *://*.blog.csdn.net/*
// @match             *://*.oschina.net/*
// @match             *://app.yinxiang.com/*
// @match             *://www.logonews.cn/*
// @match             *://afdian.net/*
// @match             *://blog.51cto.com/*
// @match             *://xie.infoq.cn/*
// @match             *://gitee.com/*
// @match             *://sspai.com/*
// @match             *://*.bing.com/*
// @connect           *
// @supportURL        https://github.com/MerielVaren/remove-link-redirects/issues/new/choose
// @homepage          https://github.com/MerielVaren/remove-link-redirects
// @run-at            document-start
// @namespace         http://greasyfork.icu/zh-CN/users/876245-meriel-varen
// @license           MIT
// ==/UserScript==

(() => {
  class App {
    constructor() {
      this.registeredProviders = [];
      this.mutationObserver = new MutationObserver((mutations) => {
        mutations.forEach(this.handleMutation.bind(this));
      });
    }

    /**
     * 处理变动
     * @param mutation
     * @returns
     * */
    handleMutation(mutation) {
      if (mutation.type === "childList") {
        mutation.addedNodes.forEach((node) => {
          if (node instanceof HTMLAnchorElement) {
            this.handleNode(node);
          }
        });
      }
    }

    /**
     * 处理节点
     * @param node
     * @returns
     */
    handleNode(node) {
      for (const provider of this.registeredProviders) {
        if (this.isMatchProvider(node, provider)) {
          provider.resolve(node);
          break;
        }
      }
    }

    /**
     * A 标签是否匹配服务提供者
     * @param element
     * @param provider
     */
    isMatchProvider(element, provider) {
      if (element.getAttribute(Marker.RedirectStatusDone)) {
        return false;
      }
      if (provider.linkTest instanceof RegExp && !provider.linkTest.test(element.href)) {
        return false;
      }
      if (typeof provider.linkTest === "function" && !provider.test(element)) {
        return false;
      }
      if (provider.linkTest instanceof Boolean) {
        return provider.linkTest;
      }
      return true;
    }

    /**
     * 当页面准备就绪时,进行初始化动作
     */
    async pageOnReady() {
      for (const provider of this.registeredProviders) {
        if (provider.onInit) {
          await provider.onInit();
        }
      }
    }

    /**
     * 注册服务提供者
     * @param providers
     */
    registerProvider(providers) {
      for (const provider of providers) {
        if (provider.urlTest === false) {
          continue;
        }
        if (provider.urlTest instanceof RegExp && !provider.urlTest.test(location.hostname)) {
          continue;
        }
        if (typeof provider.urlTest === "function" && provider.urlTest() === false) {
          continue;
        }
        this.registeredProviders.push(provider);
      }
      return this;
    }

    /**
     * 启动应用
     */
    bootstrap() {
      addEventListener("DOMContentLoaded", this.pageOnReady.bind(this));
      this.mutationObserver.observe(document.documentElement, {
        childList: true,
        subtree: true,
      });
    }
  }

  const Marker = {
    RedirectStatusDone: "redirect-status-done",
  };

  /**
   * 监听 URL 变化
   * @param event
   */
  function urlChange(event) {
    const destinationUrl = event?.destination?.url || "";
    if (destinationUrl.startsWith("about:blank")) return;
    const href = destinationUrl || location.href;
    if (href !== location.href) {
      location.href = href;
    }
  }

  /**
   * 根据url上的路径匹配,去除重定向
   * @param {HTMLAnchorElement} element
   * @param {RegExp} tester
   * @returns {boolean}
   */
  function matchLinkFromUrl(element, tester) {
    const match = tester.exec(element.href);
    if (!match || !match[1]) return "";

    try {
      return decodeURIComponent(match[1]);
    } catch {
      return /https?:\/\//.test(match[1]) ? match[1] : "";
    }
  }

  /**
   * 重试异步操作
   * @param {() => Promise<any>} operation
   * @param {number} maxRetries
   * @param {number} currentRetry
   */
  async function retryAsyncOperation(operation, maxRetries, currentRetry = 0) {
    try {
      // 尝试执行操作
      return await operation();
    } catch (err) {
      if (currentRetry < maxRetries) {
        // 如果当前重试次数小于最大重试次数,等待一段时间后重试
        await new Promise((resolve) => setTimeout(resolve, 1000)); // 等待1秒
        return retryAsyncOperation(operation, maxRetries, currentRetry + 1);
      }
      // 如果重试次数用尽,抛出错误
      throw err;
    }
  }

  /**
   * 去除重定向
   * @param element A标签元素
   * @param realUrl 真实的地址
   * @param options
   */
  function removeLinkRedirect(element, realUrl, options = {}) {
    if (options.force || (realUrl && element.href !== realUrl)) {
      element.setAttribute("redirect-status-done", element.href);
      element.href = realUrl;
    }
  }

  const providers = [
    {
      name: "如有乐享",
      urlTest: /51\.ruyo\.net/,
      linkTest: /\/[^\?]*\?u=(.*)/,
      resolve: function (element) {
        removeLinkRedirect(element, new URL(element.href).searchParams.get("u"));
      },
    },
    {
      name: "Mozilla",
      urlTest: /addons\.mozilla\.org/,
      linkTest: /outgoing\.prod\.mozaws\.net\/v\d\/\w+\/(.*)/,
      resolve: function (element) {
        removeLinkRedirect(element, matchLinkFromUrl(element, this.linkTest));
      },
    },
    {
      name: "爱发电",
      urlTest: /afdian\.net/,
      linkTest: /afdian\.net\/link\?target=(.*)/,
      resolve: function (element) {
        removeLinkRedirect(element, new URL(element.href).searchParams.get("target"));
      },
    },
    {
      name: "印象笔记",
      urlTest: /app\.yinxiang\.com/,
      linkTest: /^http:\/\//,
      resolve: function (element) {
        if (element.hasAttribute("data-mce-href")) {
          if (!element.onclick) {
            removeLinkRedirect(element, element.href, { force: true });
            element.onclick = (e) => {
              // 阻止事件冒泡, 因为上层元素绑定的click事件会重定向
              if (e.stopPropagation) {
                e.stopPropagation();
              }
              element.setAttribute("target", "_blank");
              window.top ? window.top.open(element.href) : window.open(element.href);
            };
          }
        }
        // 分享页面
        else if (/^https:\/\/app\.yinxiang\.com\/OutboundRedirect\.action\?dest=/.test(element.href)) {
          removeLinkRedirect(element, new URL(element.href).searchParams.get("dest"));
        }
      },
      onInit: async function() {
        const handler = (e) => {
          const dom = e.target;
          const tagName = dom.tagName.toUpperCase();
          switch (tagName) {
            case "A": {
              this.resolve(dom);
              break;
            }
            case "IFRAME": {
              if (dom.hasAttribute("redirect-link-removed")) {
                return;
              }
              dom.setAttribute("redirect-link-removed", "1");
              dom.contentWindow.document.addEventListener("mouseover", handler);
              break;
            }
          }
        };
        document.addEventListener("mouseover", handler);
      },
    },
    {
      name: "Bing",
      urlTest: /bing\.com/,
      linkTest: /.+\.bing\.com\/ck\/a\?.*&u=a1(.*)&ntb=1/,
      resolve: function (element) {
        removeLinkRedirect(
          element,
          BingProvider.textDecoder.decode(
            Uint8Array.from(
              Array.from(
                atob(
                  element.href
                    .split("&u=a1")[1]
                    .split("&ntb=1")[0]
                    .replace(/[-_]/g, (e) => ("-" === e ? "+" : "/"))
                    .replace(/[^A-Za-z0-9\\+\\/]/g, ""),
                ),
              ).map((e) => e.charCodeAt(0)),
            ),
          ),
        );
      },
    },
    {
      name: "51CTO博客",
      urlTest: /blog\.51cto\.com/,
      linkTest: true,
      resolve: function (element) {
        this.container = document.querySelector(".article-detail");
        if (this.container?.contains(element)) {
          if (!element.onclick && element.href) {
            element.onclick = function removeLinkRedirectOnClickFn(e) {
              e.stopPropagation();
              e.preventDefault();
              e.stopImmediatePropagation();
              const $a = document.createElement("a");
              $a.href = element.href;
              $a.target = element.target;
              $a.click();
            };
          }
        }
      },
    },
    {
      name: "CSDN",
      urlTest: /blog\.csdn\.net/,
      linkTest: /^https?:\/\//,
      resolve: function (element) {
        this.container = document.querySelector("#content_views");
        if (this.container?.contains(element)) {
          if (!element.onclick && element.origin !== window.location.origin) {
            removeLinkRedirect(element, element.href, { force: true });
            element.onclick = (e) => {
              // 阻止事件冒泡, 因为上层元素绑定的click事件会重定向
              if (e.stopPropagation) {
                e.stopPropagation();
              }
              element.setAttribute("target", "_blank");
            };
          }
        }
      },
    },
    {
      name: "知乎日报",
      urlTest: /daily\.zhihu\.com/,
      linkTest: /zhihu\.com\/\?target=(.*)/,
      resolve: function (element) {
        removeLinkRedirect(element, new URL(element.href).searchParams.get("target"));
      },
    },
    {
      name: "Google Docs",
      urlTest: /docs\.google\.com/,
      linkTest: /www\.google\.com\/url\?q=(.*)/,
      resolve: function (element) {
        removeLinkRedirect(element, new URL(element.href).searchParams.get("q"));
      },
    },
    {
      name: "Pocket",
      urlTest: /getpocket\.com/,
      linkTest: /getpocket\.com\/redirect\?url=(.*)/,
      resolve: function (element) {
        removeLinkRedirect(element, new URL(element.href).searchParams.get("url"));
      },
    },
    {
      name: "Gitee",
      urlTest: /gitee\.com/,
      linkTest: /gitee\.com\/link\?target=(.*)/,
      resolve: function (element) {
        removeLinkRedirect(element, new URL(element.href).searchParams.get("target"));
      },
    },
    {
      name: "InfoQ",
      urlTest: /infoq\.cn/,
      linkTest: /infoq\.cn\/link\?target=(.*)/,
      resolve: function (element) {
        removeLinkRedirect(element, new URL(element.href).searchParams.get("target"));
      },
    },
    {
      name: "掘金",
      urlTest: /juejin\.(im|cn)/,
      linkTest: /link\.juejin\.(im|cn)\/\?target=(.*)/,
      resolve: function (element) {
        const finalURL = new URL(element.href).searchParams.get("target");
        removeLinkRedirect(element, finalURL);
        if (this.linkTest.test(element.title)) {
          element.title = finalURL;
        }
      },
    },
    {
      name: "QQ邮箱",
      urlTest: /mail\.qq\.com/,
      linkTest: true,
      resolve: function (element) {
        this.container = document.querySelector("#contentDiv");
        if (this.container?.contains(element)) {
          if (element.onclick) {
            element.onclick = (e) => {
              // 阻止事件冒泡, 因为上层元素绑定的click事件会重定向
              if (e.stopPropagation) {
                e.stopPropagation();
              }
            };
          }
        }
      },
    },
    {
      name: "OS China",
      urlTest: /oschina\.net/,
      linkTest: /oschina\.net\/action\/GoToLink\?url=(.*)/,
      resolve: function (element) {
        removeLinkRedirect(element, new URL(element.href).searchParams.get("url"));
      },
    },
    {
      name: "Google Play",
      urlTest: /play\.google\.com/,
      linkTest: function (element) {
        if (/google\.com\/url\?q=(.*)/.test(element.href)) {
          return true;
        } else if (/^\/store\/apps\/details/.test(location.pathname)) {
          return true;
        }
        return false;
      },
      resolve: function (element) {
        removeLinkRedirect(element, new URL(element.href).searchParams.get("q"));
        // 移除开发者栏目下的重定向
        const eles = [].slice.call(document.querySelectorAll("a.hrTbp"));
        for (const ele of eles) {
          if (!ele.href || ele.getAttribute(Marker.RedirectStatusDone)) {
            continue;
          }
          ele.setAttribute(Marker.RedirectStatusDone, ele.href);
          ele.setAttribute("target", "_blank");
          ele.addEventListener(
            "click",
            (event) => {
              event.stopPropagation();
            },
            true,
          );
        }
      },
    },
    {
      name: "少数派",
      urlTest: /sspai\.com/,
      linkTest: /sspai\.com\/link\?target=(.*)/,
      resolve: function (element) {
        removeLinkRedirect(element, new URL(element.href).searchParams.get("target"));
      },
    },
    {
      name: "Steam Community",
      urlTest: /steamcommunity\.com/,
      linkTest: /steamcommunity\.com\/linkfilter\/\?url=(.*)/,
      resolve: function (element) {
        removeLinkRedirect(element, new URL(element.href).searchParams.get("url"));
      },
    },
    {
      name: "百度贴吧",
      urlTest: /tieba\.baidu\.com/,
      linkTest: /jump\d*\.bdimg\.com/,
      resolve: function (element) {
        if (!this.test.test(element.href)) {
          return;
        }
        let url = "";
        const text = element.innerText || element.textContent || "";
        try {
          if (/https?:\/\//.test(text)) {
            url = decodeURIComponent(text);
          }
        } catch (e) {
          url = /https?:\/\//.test(text) ? text : "";
        }
        if (url) {
          removeLinkRedirect(element, url);
        }
      },
    },
    {
      name: "Twitter",
      urlTest: /twitter\.com/,
      linkTest: /t\.co\/\w+/,
      resolve: function (element) {
        if (!this.linkTest.test(element.href)) {
          return;
        }
        if (/https?:\/\//.test(element.title)) {
          const url = decodeURIComponent(element.title);
          removeLinkRedirect(element, url);
          return;
        }
        const innerText = element.innerText.replace(/…$/, "");
        if (/https?:\/\//.test(innerText)) {
          removeLinkRedirect(element, innerText);
          return;
        }
      },
    },
    {
      name: "百度视频",
      urlTest: /v\.baidu\.com/,
      linkTest: /v\.baidu\.com\/link\?url=/,
      resolve: function (element) {
        GM.xmlHttpRequest({
          method: "GET",
          url: element.href,
          anonymous: true,
          onload: (res) => {
            if (res.finalUrl) {
              removeLinkRedirect(element, res.finalUrl);
            }
          },
          onerror: (err) => {
            console.error(err);
          },
        });
      },
    },
    {
      name: "微博",
      urlTest: /\.weibo\.com/,
      linkTest: /t\.cn\/\w+/,
      resolve: function (element) {
        if (!(this.linkTest.test(element.href) && /^https?:\/\//.test(element.title))) {
          return;
        }
        const url = decodeURIComponent(element.title);
        if (url) {
          element.href = url;
          removeLinkRedirect(element, url);
        }
      },
    },
    {
      name: "百度搜索",
      urlTest: /www\.baidu\.com/,
      linkTest: /www\.baidu\.com\/link\?url=/,
      unresolvable: ["nourl.ubs.baidu.com", "lightapp.baidu.com"],
      handleOneElement: (element) => {
        retryAsyncOperation(async () => {
          const res = await GM.xmlHttpRequest({
            method: "GET",
            url: element.href,
            anonymous: true,
          });
          if (res.finalUrl) {
            removeLinkRedirect(element, res.finalUrl);
          }
        }, 3);
      },
      resolve: async function(element) {
        const url = element.closest(".cos-row") ? null : element.closest(".c-container")?.getAttribute("mu");
        if (url && url !== "null" && url !== "undefined" && !this.unresolvable.some((u) => url.includes(u))) {
          removeLinkRedirect(element, url);
        } else {
          this.handleOneElement(element);
        }
      },
    },
    {
      name: "豆瓣",
      urlTest: /douban\.com/,
      linkTest: /douban\.com\/link2\/?\?url=(.*)/,
      resolve: function (element) {
        removeLinkRedirect(element, new URL(element.href).searchParams.get("url"));
      },
    },
    {
      name: "Google搜索",
      urlTest: /\w+\.google\./,
      linkTest: true,
      resolve: function (element) {
        const traceProperties = ["ping", "data-jsarwt", "data-usg", "data-ved"];
        // 移除追踪
        for (const property of traceProperties) {
          if (element.getAttribute(property)) {
            element.removeAttribute(property);
          }
        }
        // 移除多余的事件
        if (element.getAttribute("onmousedown")) {
          element.removeAttribute("onmousedown");
        }
        // 尝试去除重定向
        if (element.getAttribute("data-href")) {
          const realUrl = element.getAttribute("data-href");
          removeLinkRedirect(element, realUrl);
        }
        const url = new URL(element.href);
        if (url.searchParams.get("url")) {
          removeLinkRedirect(element, url.searchParams.get("url"));
        }
      },
    },
    {
      name: "简书",
      urlTest: /www\.jianshu\.com/,
      linkTest: function (element) {
        const isLink1 = /links\.jianshu\.com\/go/.test(element.href);
        const isLink2 = /link\.jianshu\.com(\/)?\?t=/.test(element.href);
        const isLink3 = /jianshu\.com\/go-wild\/?\?(.*)url=/.test(element.href);
        if (isLink1 || isLink2 || isLink3) {
          return true;
        }
        return false;
      },
      resolve: function (element) {
        const search = new URL(element.href).searchParams;
        removeLinkRedirect(element, search.get("to") || search.get("t") || search.get("url"));
      },
    },
    {
      name: "标志情报局",
      urlTest: /www\.logonews\.cn/,
      linkTest: /link\.logonews\.cn\/\?url=(.*)/,
      resolve: function (element) {
        removeLinkRedirect(element, new URL(element.href).searchParams.get("url"));
      },
    },
    {
      name: "360搜索",
      urlTest: /www\.so\.com/,
      linkTest: /so\.com\/link\?(.*)/,
      resolve: function (element) {
        const url = element.getAttribute("data-mdurl") || element.getAttribute("e-landurl");
        if (url) {
          removeLinkRedirect(element, url);
        }
        // remove track
        element.removeAttribute("e_href");
        element.removeAttribute("data-res");
      },
    },
    {
      name: "搜狗搜索",
      urlTest: /www\.sogou\.com/,
      linkTest: /www\.sogou\.com\/link\?url=/,
      resolve: function (element) {
        // 从这个a往上找到的第一个class=vrwrap的元素
        const vrwrap = element.closest(".vrwrap");
        // 往子孙找到的第一个class包含r-sech的元素
        const rSech = vrwrap.querySelector(".r-sech");
        const url = rSech.getAttribute("data-url");
        removeLinkRedirect(element, url);
      },
    },
    {
      name: "Youtube",
      urlTest: /www\.youtube\.com/,
      linkTest: /www\.youtube\.com\/redirect\?.{1,}/,
      resolve: function (element) {
        removeLinkRedirect(element, new URL(element.href).searchParams.get("q"));
      },
    },
    {
      name: "知乎",
      urlTest: /www\.zhihu\.com/,
      linkTest: /zhihu\.com\/\?target=(.*)/,
      resolve: function (element) {
        removeLinkRedirect(element, new URL(element.href).searchParams.get("target"));
      },
    },
    {
      name: "百度学术",
      urlTest: /xueshu\.baidu\.com/,
      linkTest: /xueshu\.baidu\.com\/s?\?(.*)/,
      resolve: function (element) {
        const realHref = element.getAttribute("data-link") || element.getAttribute("data-url");
        if (realHref) {
          removeLinkRedirect(element, decodeURIComponent(realHref));
        }
      },
    },
    {
      name: "知乎专栏",
      urlTest: /zhuanlan\.zhihu\.com/,
      linkTest: /link\.zhihu\.com\/\?target=(.*)/,
      resolve: function (element) {
        removeLinkRedirect(element, new URL(element.href).searchParams.get("target"));
      },
    },
  ];

  unsafeWindow?.navigation?.addEventListener("navigate", urlChange);
  unsafeWindow.addEventListener("replaceState", urlChange);
  unsafeWindow.addEventListener("pushState", urlChange);
  unsafeWindow.addEventListener("popState", urlChange);
  unsafeWindow.addEventListener("hashchange", urlChange);

  const app = new App();
  app.registerProvider(providers).bootstrap();
})();