redirect 外链跳转

自动跳转(重定向)到目标链接,免去点击步骤。适配了简书、知乎、微博、QQ邮箱、QQPC、QQNT、印象笔记、贴吧、CSDN、YouTube、微信、微信开放社区、开发者知识库、豆瓣、个人图书馆、Pixiv、搜狗、Google、站长之家、OSCHINA、掘金、腾讯文档、pc6下载站、爱发电、Gitee、天眼查、爱企查、企查查、优设网、51CTO、力扣、花瓣网、飞书、Epic、Steam、语雀、牛客网、哔哩哔哩、少数派、5ch、金山文档、石墨文档、urlshare、酷安、网盘分享、腾讯云开发者社区、腾讯兔小巢

// ==UserScript==
// @name         redirect 外链跳转
// @version      1.50.0
// @description  自动跳转(重定向)到目标链接,免去点击步骤。适配了简书、知乎、微博、QQ邮箱、QQPC、QQNT、印象笔记、贴吧、CSDN、YouTube、微信、微信开放社区、开发者知识库、豆瓣、个人图书馆、Pixiv、搜狗、Google、站长之家、OSCHINA、掘金、腾讯文档、pc6下载站、爱发电、Gitee、天眼查、爱企查、企查查、优设网、51CTO、力扣、花瓣网、飞书、Epic、Steam、语雀、牛客网、哔哩哔哩、少数派、5ch、金山文档、石墨文档、urlshare、酷安、网盘分享、腾讯云开发者社区、腾讯兔小巢
// @author       sakura-flutter
// @namespace    https://github.com/sakura-flutter/tampermonkey-scripts
// @license      GPL-3.0
// @compatible   chrome Latest
// @compatible   firefox Latest
// @compatible   edge Latest
// @run-at       document-start
// @match        *://www.jianshu.com/go-wild*
// @match        *://link.zhihu.com/*
// @match        *://t.cn/*
// @match        *://weibo.cn/sinaurl*
// @match        *://mail.qq.com/cgi-bin/*
// @match        *://c.pc.qq.com/middlem.html*
// @match        *://c.pc.qq.com/pc.html*
// @match        *://c.pc.qq.com/ios.html*
// @match        *://app.yinxiang.com/OutboundRedirect.action*
// @match        *://jump.bdimg.com/safecheck/*
// @match        *://jump2.bdimg.com/safecheck/*
// @match        *://link.csdn.net/*
// @match        *://www.youtube.com/redirect*
// @match        *://weixin110.qq.com/cgi-bin/mmspamsupport-bin/newredirectconfirmcgi*
// @match        *://developers.weixin.qq.com/community/middlepage/href*
// @match        *://www.itdaan.com/link/*
// @match        *://www.douban.com/link2/*
// @match        *://www.360doc.com/content/*
// @match        *://www.pixiv.net/jump.php*
// @match        *://m.sogou.com/*/tc*
// @match        *://m.sogou.com*/tc*
// @match        *://www.chinaz.com/go.shtml*
// @match        *://www.oschina.net/action/GoToLink*
// @match        *://link.juejin.cn/*
// @match        *://docs.qq.com/scenario/link.html*
// @match        *://www.pc6.com/goread.html*
// @match        *://afdian.net/link*
// @match        *://gitee.com/link*
// @match        *://www.tianyancha.com/security*
// @match        *://aiqicha.baidu.com/safetip*
// @match        *://www.qcc.com/web/transfer-link*
// @match        *://link.uisdc.com/*
// @match        *://blog.51cto.com/transfer*
// @match        *://leetcode.cn/link*
// @match        *://huaban.com/go*
// @match        *://security.feishu.cn/link/safety*
// @match        *://redirect.epicgames.com/*
// @match        *://steamcommunity.com/linkfilter/*
// @match        *://*.yuque.com/r/goto*
// @match        *://hd.nowcoder.com/link.html*
// @match        *://game.bilibili.com/linkfilter/*
// @match        *://sspai.com/link*
// @match        *://jump.5ch.net/*
// @match        *://www.kdocs.cn/office/link*
// @match        *://shimo.im/outlink/black*
// @match        *://google.urlshare.cn/umirror_url_check*
// @match        *://www.coolapk.com/link*
// @match        *://wpfx.org/go*
// @match        *://cloud.tencent.com/developer/tools/blog-entry*
// @match        *://support.qq.com/products/*/link-jump*
// @match        *://txc.qq.com/products/*/link-jump*
// @include      /^https?:\/\/www\.google\..{2,7}url/
// ==/UserScript==

/******/ (() => { // webpackBootstrap
/******/ 	"use strict";
/******/ 	// The require scope
/******/ 	var __webpack_require__ = {};
/******/ 	
/************************************************************************/
/******/ 	/* webpack/runtime/define property getters */
/******/ 	(() => {
/******/ 		// define getter functions for harmony exports
/******/ 		__webpack_require__.d = (exports, definition) => {
/******/ 			for(var key in definition) {
/******/ 				if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ 					Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ 				}
/******/ 			}
/******/ 		};
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/hasOwnProperty shorthand */
/******/ 	(() => {
/******/ 		__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/make namespace object */
/******/ 	(() => {
/******/ 		// define __esModule on exports
/******/ 		__webpack_require__.r = (exports) => {
/******/ 			if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 				Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 			}
/******/ 			Object.defineProperty(exports, '__esModule', { value: true });
/******/ 		};
/******/ 	})();
/******/ 	
/************************************************************************/
var __webpack_exports__ = {};

// NAMESPACE OBJECT: ./src/utils/ready-state.ts
var ready_state_namespaceObject = {};
__webpack_require__.r(ready_state_namespaceObject);
__webpack_require__.d(ready_state_namespaceObject, {
  "DOMContentLoaded": () => (DOMContentLoaded),
  "complete": () => (complete),
  "interactive": () => (interactive),
  "load": () => (load),
  "loading": () => (loading)
});

;// CONCATENATED MODULE: ./src/utils/log.ts
const isDebug = "production" !== 'production';

function warn(...args) {
  isDebug && warn.force(...args);
}

warn.force = function (...args) {
  console.warn('%c      warn      ', 'background: #ffa500; padding: 1px; color: #fff;', ...args);
};

function error(...args) {
  isDebug && error.force(...args);
}

error.force = function (...args) {
  console.error('%c      error      ', 'background: red; padding: 1px; color: #fff;', ...args);
};

function table(...args) {
  isDebug && console.table(...args);
}


;// CONCATENATED MODULE: ./src/utils/ready-state.ts
/**
 * readyState 因为脚本加载时机不一定监听到所有变化
 * 所以 pool 中的状态区分先后顺序
 * 靠后定义的会自动将靠前定义的但没有监听到的执行一次,但实际上不再是原来的状态
 */

const pool = new Map([['loading', []], ['interactive', []], ['DOMContentLoaded', []], // 扩展状态
['complete', []], ['load', []] // 扩展状态,不一定可以监听到
]);
let currentState = document.readyState;

const execute = (readyState = currentState) => {
  currentState = readyState;

  for (const [state, functions] of pool) {
    while (functions.length) {
      functions.shift()();
    }

    if (readyState === state) break;
  }
};

warn('document.readyState', currentState);

if (document.readyState !== 'complete') {
  document.addEventListener('readystatechange', () => execute(document.readyState));
  window.addEventListener('DOMContentLoaded', () => execute('DOMContentLoaded'));
}

window.addEventListener('load', () => execute('load'));

const wrapper = (readyState, fn) => new Promise(resolve => {
  pool.get(readyState).push(function () {
    resolve(fn?.());
  }); // 立即检查一下

  execute();
});

const loading = fn => wrapper('loading', fn);
const interactive = fn => wrapper('interactive', fn);
const DOMContentLoaded = fn => wrapper('DOMContentLoaded', fn);
const complete = fn => wrapper('complete', fn);
const load = fn => wrapper('load', fn);
;// CONCATENATED MODULE: ./src/utils/querystring.ts
/**
 * 解析 query
 * @param href 或 带有参数格式的 string;有 search 则不再 hash
 */
function parse(href = location.href) {
  if (!href) return {};
  let search;

  try {
    // 链接
    const url = new URL(href);
    ({
      search
    } = url); // 主要处理对hash的search

    if (!search && url.hash.includes('?')) {
      search = url.hash.split('?')[1];
    }
  } catch {
    // 非链接,如:a=1&b=2、?a=1、/foo?a=1、/foo#bar?a=1
    if (href.includes('?')) {
      search = href.split('?')[1];
    } else {
      search = href;
    }
  }

  return Object.fromEntries(new URLSearchParams(search));
}
function stringify(obj) {
  return Object.entries(obj) // 过滤 undefined,保留 null 且转成 ''
  .filter(([, value]) => value !== undefined).map(([key, value]) => `${key}=${value ?? ''}`).join('&');
}
;// CONCATENATED MODULE: ./src/utils/selector.ts
const $ = document.querySelector.bind(document);
const $$ = document.querySelectorAll.bind(document);
;// CONCATENATED MODULE: ./src/scripts/redirect/sites/t-cn.ts

const weibo = async () => {
  let link = $('.open-url a[href]')?.href;
  link || (link = await fetch(location.href).then(response => response.headers.get('location')));
  return {
    link
  };
};
;// CONCATENATED MODULE: ./src/scripts/redirect/sites/weixin110-qq-com.ts
/* eslint-disable camelcase */

const {
  atob
} = window;
const weixin = () => {
  const {
    main_type,
    midpagecode
  } = parse();
  /**
   * main_type 貌似是旧的规则
   */

  switch (main_type) {
    case '2':
      {
        const url = new URL(location.href); // 转为 1 可还原链接

        url.searchParams.set('main_type', '1');
        location.replace(url.href);
        return {};
      }

    case '1':
      break;
  }
  /**
   * midpagecode 似乎是新的规则
   */


  const MAGIC_KEY = atob(atob('Tmpjek56ZGhNbUZrWWpRMFpURTNZekZpTUdGa1lqSTBZalZqWmpKaVpERXlZek0wWkRsaU5UWmxNRFpqWTJRMlpHUTBZekk1TVdJME1qTmlOV0prTjJabU5tUmhZbVJqTlRVM1l6azVNbVkxWkRZd1pEZzVNbUkyT0Rjd1pqYzBOakV3TldNM05HRmhNalJqTXpBMk0yUTNOR1ExT1dJMFlXVTFOVFF6WldJM1lqSmtObVUwT1dOak1qYzNNMkZsTVRjM01UWTNNemcwTmpRM04ySmpOalppTTJNelltUTNPVE5sWkRJNFpEZGhaVE5rTnpZeE0yUm1ZVGRpWW1ReQ=='));

  if (midpagecode && midpagecode !== MAGIC_KEY && !window.cgiData?.url) {
    const url = new URL(location.href); // 会还原链接

    url.searchParams.set('midpagecode', MAGIC_KEY);
    location.replace(url.href);
    return {};
  }

  return {
    // 如果解析得到,会出现在页面这里
    selector: '.weui-msg__text-area .ui-ellpisis-content p'
  };
};
;// CONCATENATED MODULE: ./src/scripts/redirect/sites/www-360doc-com.ts


const doc360 = () => {
  $('#artContent').addEventListener('click', event => {
    const {
      target
    } = event;
    const href = target.href;
    warn(target);
    if (target.nodeName !== 'A') return;
    if (!href) return; // 是否本站

    if (new RegExp(location.host).test(new URL(href).host)) return;
    event.stopPropagation();
    window.open(href);
  }, true);
  return {};
};
;// CONCATENATED MODULE: ./src/scripts/redirect/sites/www-pixiv-net.ts

const pixiv = () => {
  let link; // 链接居然是直接拼在url上的
  // https://www.pixiv.net/jump.php?https%3A%2F%2Fwww.huawei.com%2Fcn%2Fcorporate-information

  for (const [key, value] of Object.entries(parse())) {
    try {
      link || (link = new URL(key).href);
    } catch {}

    try {
      link || (link = new URL(value).href);
    } catch {}
  }

  return {
    link
  };
};
;// CONCATENATED MODULE: ./src/scripts/redirect/sites/index.ts






const sites = [{
  name: '简书',
  test: 'www.jianshu.com/go-wild',
  use: () => ({
    query: 'url'
  })
}, {
  name: '知乎',
  test: 'link.zhihu.com/',
  use: () => ({
    query: 'target'
  })
}, {
  name: '微博',
  test: /^t\.cn\//,
  readyState: 'interactive',
  use: weibo
}, {
  name: '微博',
  // 不同规则
  test: 'weibo.cn/sinaurl',
  use: () => ({
    link: parse().toasturl || parse().u
  })
}, {
  name: 'QQ邮箱',
  test: [/^mail\.qq\.com\/cgi-bin\/readtemplate/, // 好像不用登录也可以
  /^mail\.qq\.com\/cgi-bin\/mail_spam/ // 需要登录邮箱才可以,不过这里仍然可以帮忙跳转
  ],
  use: () => ({
    link: parse().url || parse().gourl
  })
}, {
  name: 'QQPC',
  test: 'c.pc.qq.com/middlem.html',
  use: () => ({
    query: 'pfurl'
  })
}, {
  // 被阻止访问
  name: 'QQPC2',
  test: 'c.pc.qq.com/pc.html',
  use: () => ({
    query: 'url'
  })
}, {
  name: 'QQNT',
  test: 'c.pc.qq.com/ios.html',
  use: () => ({
    query: 'url'
  })
}, {
  name: '腾讯文档',
  test: 'docs.qq.com/scenario/link.html',
  use: () => ({
    query: 'url'
  })
}, {
  name: '印象笔记',
  test: /^app\.yinxiang\.com\/OutboundRedirect/,
  use: () => ({
    query: 'dest'
  })
}, {
  name: '贴吧',
  test: /^jump2?\.bdimg\.com\/safecheck/,
  // 以前的地址没有 2
  readyState: 'interactive',
  use: () => ({
    selector: '.warning_info a:nth-of-type(1)[href]',
    attr: 'href'
  })
}, {
  name: 'CSDN',
  test: 'link.csdn.net/',
  use: () => ({
    query: 'target'
  })
}, {
  name: 'YouTube',
  test: 'www.youtube.com/redirect',
  use: () => ({
    query: 'q'
  })
}, {
  name: '微信',
  test: /^weixin110\.qq\.com\/cgi-bin\/mmspamsupport-bin\/newredirectconfirmcgi/,
  readyState: 'interactive',
  use: weixin
}, {
  name: '微信开放社区',
  test: 'developers.weixin.qq.com/community/middlepage/href',
  use: () => ({
    query: 'href'
  })
}, {
  name: '开发者知识库',
  test: /^www\.itdaan.com\/link\//,
  readyState: 'interactive',
  use: () => ({
    selector: '.safety-url'
  })
}, {
  name: '豆瓣',
  test: 'www.douban.com/link2/',
  use: () => ({
    query: 'url'
  })
}, {
  name: '个人图书馆',
  test: /^www\.360doc.com\/content\//,
  readyState: 'interactive',
  use: doc360
}, {
  name: 'Pixiv',
  test: 'www.pixiv.net/jump.php',
  use: pixiv
}, {
  name: '搜狗',
  test: /^m\.sogou\.com.*tc$/,
  use: () => ({
    query: 'url'
  })
}, {
  name: 'Google',
  test: /^www\.google\..{2,7}url$/,
  use: () => ({
    link: parse().url || parse().q
  })
}, {
  name: '站长之家',
  test: 'www.chinaz.com/go.shtml',
  use: () => ({
    query: 'url'
  })
}, {
  name: 'OSCHINA',
  test: 'www.oschina.net/action/GoToLink',
  use: () => ({
    query: 'url'
  })
}, {
  name: '掘金',
  test: 'link.juejin.cn/',
  use: () => ({
    query: 'target'
  })
}, {
  name: 'pc6下载站',
  test: 'www.pc6.com/goread.html',
  use: () => ({
    query: 'gourl'
  })
}, {
  name: '爱发电',
  test: 'afdian.net/link',
  use: () => ({
    query: 'target'
  })
}, {
  name: 'Gitee',
  test: 'gitee.com/link',
  use: () => ({
    query: 'target'
  })
}, {
  name: '天眼查',
  test: 'www.tianyancha.com/security',
  use: () => ({
    query: 'target'
  })
}, {
  name: '爱企查',
  test: 'aiqicha.baidu.com/safetip',
  use: () => ({
    query: 'target'
  })
}, {
  name: '企查查',
  test: 'www.qcc.com/web/transfer-link',
  use: () => ({
    query: 'link'
  })
}, {
  name: '优设网',
  test: 'link.uisdc.com/',
  use: () => ({
    query: 'redirect'
  })
}, {
  name: '51CTO',
  test: 'blog.51cto.com/transfer',
  use: () => ({
    link: location.search.slice(1)
  })
}, {
  name: '力扣',
  test: 'leetcode.cn/link/',
  use: () => ({
    query: 'target'
  })
}, {
  name: '花瓣网',
  test: 'huaban.com/go',
  readyState: 'interactive',
  use: () => {
    const nextData = JSON.parse($('#__NEXT_DATA__').textContent);
    return {
      link: nextData.props.pageProps?.data.link
    };
  }
}, {
  name: '飞书',
  test: 'security.feishu.cn/link/safety',
  use: () => ({
    query: 'target'
  })
}, {
  name: 'Epic',
  test: /^redirect\.epicgames\.com\//,
  use: () => ({
    query: 'redirectTo'
  })
}, {
  name: 'Steam',
  test: 'steamcommunity.com/linkfilter/',
  use: () => ({
    query: 'url'
  })
}, {
  name: '语雀',
  test: /\.yuque\.com\/r\/goto(\/?)$/,
  use: () => ({
    query: 'url'
  })
}, {
  name: '牛客网',
  test: 'hd.nowcoder.com/link.html',
  use: () => ({
    query: 'target'
  })
}, {
  name: '哔哩哔哩',
  test: 'game.bilibili.com/linkfilter/',
  use: () => ({
    query: 'url'
  })
}, {
  name: '少数派',
  test: 'sspai.com/link',
  use: () => ({
    query: 'target'
  })
}, {
  name: '5ch',
  test: 'jump.5ch.net/',
  use: () => ({
    link: location.search.slice(1)
  })
}, {
  name: '金山文档',
  test: 'www.kdocs.cn/office/link',
  use: () => ({
    query: 'target'
  })
}, {
  name: '石墨文档',
  test: 'shimo.im/outlink/black',
  use: () => ({
    query: 'url'
  })
}, {
  name: 'urlshare',
  test: 'google.urlshare.cn/umirror_url_check',
  use: () => ({
    query: 'url'
  })
}, {
  name: '酷安',
  test: 'www.coolapk.com/link',
  use: () => ({
    query: 'url'
  })
}, {
  name: '网盘分享',
  test: 'wpfx.org/go/',
  use: () => ({
    query: 'url'
  })
}, {
  name: '腾讯云开发者社区',
  test: 'cloud.tencent.com/developer/tools/blog-entry',
  use: () => ({
    query: 'target'
  })
}, {
  name: '腾讯兔小巢',
  // 两个域名
  test: /^(support|txc)\.qq\.com\/products\/\d+\/link-jump$/,
  use: () => ({
    query: 'jump'
  })
}];
/* harmony default export */ const redirect_sites = (sites);
;// CONCATENATED MODULE: ./src/scripts/redirect/index.ts
function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; }

var id = 0;

function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + name; }







function hidePage() {
  interactive(() => {
    document.body.style.cssText = 'display:none !important;';
  });
}

var _sites = /*#__PURE__*/_classPrivateFieldLooseKey("sites");

var _includes = /*#__PURE__*/_classPrivateFieldLooseKey("includes");

var _parse = /*#__PURE__*/_classPrivateFieldLooseKey("parse");

var _ensure = /*#__PURE__*/_classPrivateFieldLooseKey("ensure");

class App {
  constructor(sites) {
    Object.defineProperty(this, _ensure, {
      value: _ensure2
    });
    Object.defineProperty(this, _parse, {
      value: _parse2
    });
    Object.defineProperty(this, _includes, {
      value: _includes2
    });
    Object.defineProperty(this, _sites, {
      writable: true,
      value: void 0
    });
    _classPrivateFieldLooseBase(this, _sites)[_sites] = sites;
  }

  boot() {
    const briefURL = location.host + location.pathname;

    _classPrivateFieldLooseBase(this, _sites)[_sites].forEach(async site => {
      const {
        name,
        test,
        use
      } = site;
      if (!_classPrivateFieldLooseBase(this, _includes)[_includes](test, briefURL)) return;
      const {
        readyState: state
      } = site;
      if (state) await ready_state_namespaceObject[state]();
      const redirection = await _classPrivateFieldLooseBase(this, _parse)[_parse](use);
      table({
        name,
        briefURL,
        redirection
      });
      if (!redirection) return;
      location.replace(redirection); // 为什么要这样做?
      // 只是为了避免被问“哎!怎么好像没有跳转啊?!”的烦恼(实际上跳转了只是外链打开慢)(x_x)

      hidePage();
    });
  }

}

function _includes2(test, url) {
  return [].concat(test).some(item => {
    if (typeof item === 'string') return item === url;
    if (item instanceof RegExp) return item.test(url);
    return false;
  });
}

async function _parse2(use) {
  const {
    query,
    link,
    selector,
    attr
  } = await use();
  let redirection;

  if (query) {
    redirection = parse()[query];
  } else if (link) {
    redirection = link;
  } else if (selector) {
    redirection = $(selector)?.[attr ?? 'innerText'];
  }

  redirection && (redirection = _classPrivateFieldLooseBase(this, _ensure)[_ensure](redirection.trim()));
  return redirection;
}

function _ensure2(url) {
  try {
    // eslint-disable-next-line no-new
    new URL(url);
  } catch (error) {
    warn(error); // 修复某些链接没有 protocol 导致跳转不正确
    // https://greasyfork.org/zh-CN/scripts/416338-redirect-外链跳转/discussions/69178

    const protocol = 'http:';
    url = protocol + '//' + url;
  }

  return url;
}

new App(redirect_sites).boot();
/******/ })()
;