GitHub Clone/Release Proxy Link

复制 Clone 面板并替换为镜像地址;Release 页为每个条目追加代理下载链接。多镜像切换、菜单设置、远程镜像列表更新。

As of 2026-08-25. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         GitHub Clone/Release Proxy Link
// @namespace    https://github.com/daye/github-proxy-monkey
// @version      2.0.0
// @author       daye
// @description  复制 Clone 面板并替换为镜像地址;Release 页为每个条目追加代理下载链接。多镜像切换、菜单设置、远程镜像列表更新。
// @license      MIT
// @icon         https://github.githubassets.com/favicons/favicon.svg
// @homepageURL  https://github.com/daye/github-proxy-monkey
// @supportURL   https://github.com/daye/github-proxy-monkey/issues
// @match        https://github.com/*
// @grant        GM_deleteValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_setClipboard
// @grant        GM_setValue
// @grant        GM_unregisterMenuCommand
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// ==/UserScript==

(function () {
  'use strict';

  var _GM_getValue = /* @__PURE__ */ (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
  var _GM_registerMenuCommand = /* @__PURE__ */ (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)();
  var _GM_setClipboard = /* @__PURE__ */ (() => typeof GM_setClipboard != "undefined" ? GM_setClipboard : void 0)();
  var _GM_setValue = /* @__PURE__ */ (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
  var _GM_unregisterMenuCommand = /* @__PURE__ */ (() => typeof GM_unregisterMenuCommand != "undefined" ? GM_unregisterMenuCommand : void 0)();
  var _GM_xmlhttpRequest = /* @__PURE__ */ (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)();
  const DEFAULT_SETTINGS = {
    selectedMirror: "",
    releaseLinkEnabled: true,
    clonePanelEnabled: true
  };
  const STORAGE_KEY = "settings";
  function loadSettings() {
    const stored = _GM_getValue(STORAGE_KEY, {});
    return { ...DEFAULT_SETTINGS, ...stored };
  }
  function saveSettings(settings) {
    _GM_setValue(STORAGE_KEY, settings);
  }
  function updateSettings(patch) {
    const next = { ...loadSettings(), ...patch };
    saveSettings(next);
    return next;
  }
  function resolveCurrentMirror(settings, mirrors2) {
    return mirrors2.find((m) => m.name === settings.selectedMirror) ?? mirrors2[0];
  }
  const BUILTIN_MIRRORS = [
    { name: "gh-proxy.org", prefix: "https://v4.gh-proxy.org/" },
    { name: "ghfast.top", prefix: "https://ghfast.top/" },
    { name: "gh-proxy.net", prefix: "https://gh-proxy.net/" }
  ];
  const REMOTE_MIRRORS_URL = "https://raw.githubusercontent.com/daye/github-proxy-monkey/main/mirrors.json";
  const REMOTE_CACHE_TTL = 12 * 60 * 60 * 1e3;
  const REMOTE_FETCH_TIMEOUT = 8e3;
  const REMOTE_MAX_MIRRORS = 20;
  const CACHE_KEY = "remoteMirrorsCache";
  function parseMirrors(data) {
    const list = Array.isArray(data) ? data : typeof data === "object" && data !== null && Array.isArray(data.mirrors) ? data.mirrors : [];
    const mirrors2 = list.filter((m) => {
      if (typeof m !== "object" || m === null) return false;
      const candidate = m;
      return typeof candidate.name === "string" && typeof candidate.prefix === "string";
    }).map((m) => ({
      name: m.name,
      prefix: m.prefix.endsWith("/") ? m.prefix : `${m.prefix}/`,
      note: typeof m.note === "string" ? m.note : void 0
    })).slice(0, REMOTE_MAX_MIRRORS);
    return mirrors2.length > 0 ? mirrors2 : void 0;
  }
  function loadCache() {
    const cache = _GM_getValue(CACHE_KEY, void 0);
    if (!cache || !Array.isArray(cache.mirrors) || cache.mirrors.length === 0) {
      return void 0;
    }
    return cache;
  }
  function isFresh(cache) {
    return Date.now() - cache.fetchedAt < REMOTE_CACHE_TTL;
  }
  function fetchRemote() {
    return new Promise((resolve) => {
      const timer = setTimeout(() => resolve(void 0), REMOTE_FETCH_TIMEOUT);
      _GM_xmlhttpRequest({
        method: "GET",
        url: REMOTE_MIRRORS_URL,
        timeout: REMOTE_FETCH_TIMEOUT,
        onload: (res) => {
          clearTimeout(timer);
          if (res.status !== 200) {
            resolve(void 0);
            return;
          }
          try {
            resolve(parseMirrors(JSON.parse(res.responseText)));
          } catch {
            resolve(void 0);
          }
        },
        onerror: () => {
          clearTimeout(timer);
          resolve(void 0);
        },
        ontimeout: () => {
          clearTimeout(timer);
          resolve(void 0);
        }
      });
    });
  }
  function resolveMirrors(onRemoteUpdate) {
    const cache = loadCache();
    if (cache && isFresh(cache)) {
      return { mirrors: cache.mirrors, source: "cache" };
    }
    const fallback = (cache == null ? void 0 : cache.mirrors) ?? BUILTIN_MIRRORS;
    void fetchRemote().then((remote) => {
      if (!remote) return;
      _GM_setValue(CACHE_KEY, {
        mirrors: remote,
        fetchedAt: Date.now()
      });
      onRemoteUpdate == null ? void 0 : onRemoteUpdate(remote);
    });
    return {
      mirrors: fallback,
      source: cache ? "cache" : "builtin"
    };
  }
  const registeredIds = [];
  function registerCommand(label, onClick) {
    const id = _GM_registerMenuCommand(label, onClick);
    registeredIds.push(typeof id === "number" ? id : Number(id));
  }
  function unregisterAll() {
    while (registeredIds.length > 0) {
      const id = registeredIds.pop();
      if (id !== void 0) _GM_unregisterMenuCommand(id);
    }
  }
  function registerMenu(ctx) {
    unregisterAll();
    const { mirrors: mirrors2, settings } = ctx;
    const current = resolveCurrentMirror(settings, mirrors2);
    registerCommand(
      `镜像: ${(current == null ? void 0 : current.name) ?? "(无可用镜像)"}${(current == null ? void 0 : current.note) ? ` (${current.note})` : ""}`,
      () => cycleMirror(ctx)
    );
    registerCommand(
      `Release 代理链接: ${settings.releaseLinkEnabled ? "开" : "关"}`,
      () => {
        ctx.settings = updateSettings({
          releaseLinkEnabled: !ctx.settings.releaseLinkEnabled
        });
        ctx.onApply();
        registerMenu(ctx);
      }
    );
    registerCommand(
      `Clone 镜像面板: ${settings.clonePanelEnabled ? "开" : "关"}`,
      () => {
        ctx.settings = updateSettings({
          clonePanelEnabled: !ctx.settings.clonePanelEnabled
        });
        ctx.onApply();
        registerMenu(ctx);
      }
    );
    registerCommand("🔄 清除远程镜像缓存并刷新", () => {
      location.reload();
    });
  }
  function cycleMirror(ctx) {
    const { mirrors: mirrors2 } = ctx;
    if (mirrors2.length === 0) return;
    const settings = loadSettings();
    const currentIndex = mirrors2.findIndex((m) => m.name === settings.selectedMirror);
    const base = currentIndex < 0 ? 0 : currentIndex;
    const next = mirrors2[(base + 1) % mirrors2.length];
    if (!next) return;
    ctx.settings = updateSettings({ selectedMirror: next.name });
    ctx.onApply();
    registerMenu(ctx);
  }
  const SELECTORS = {
    /** Clone 面板容器(新版 GitHub 仓库页代码弹层) */
    cloneContainer: [
      // 新版:Code panel dialog overlay → content → container
      '[data-testid="repository-code-button"] + div[data-dialog-overlay]',
      'dialog[role="dialog"]',
      // 旧版:内联样式和特定 class 容器
      'div[class*="LocalTab-module__CloneContainer"]',
      'div[class*="CloneContainer"]'
    ],
    /** Clone 面板中 HTTPS 克隆地址输入框 */
    cloneHttpsInput: ["#clone-with-https", 'input[aria-label*="Clone"]'],
    /** Clone 面板中复制按钮(clipboard-copy 组件或原生按钮) */
    cloneCopyButton: ["clipboard-copy", "button[data-view-component]"],
    /** Release 列表条目(仓库 Releases 页 / 单个 release 页) */
    releaseEntry: [".Box-row", "li.Box-row"],
    /** Release 条目标题链接 */
    releaseTitleLink: ["a.Truncate", "a.Link--primary"],
    /** Release 条目内可代理的下载链接(源码包或 Assets 附件) */
    releaseDownloadLink: [
      'a[href*="/archive/refs/tags/"]',
      'a[href*="/releases/download/"]'
    ]
  };
  function queryFirst(scope, selectors) {
    for (const selector of selectors) {
      const el = scope.querySelector(selector);
      if (el) return el;
    }
    return null;
  }
  function queryAll(scope, selectors) {
    for (const selector of selectors) {
      const els = Array.from(scope.querySelectorAll(selector));
      if (els.length > 0) return els;
    }
    return [];
  }
  const GITHUB_HOST = "https://github.com";
  function isGithubUrl(url) {
    return url === GITHUB_HOST || url.startsWith(`${GITHUB_HOST}/`);
  }
  function withProxy(url, mirror) {
    if (!url) return void 0;
    let absolute = url.trim();
    if (absolute.startsWith("/")) {
      absolute = GITHUB_HOST + absolute;
    }
    if (!isGithubUrl(absolute)) return void 0;
    const prefix = mirror.prefix.endsWith("/") ? mirror.prefix : `${mirror.prefix}/`;
    if (absolute.startsWith(prefix)) return absolute;
    return prefix + absolute;
  }
  const CLONE_PANEL_CLASS = "gh-proxy-clone-container";
  const CLONE_BTN_CLASS = "gh-proxy-copy-btn";
  function processCloneContainer(mirror) {
    const original = queryFirst(document, SELECTORS.cloneContainer);
    if (!(original == null ? void 0 : original.parentNode)) return;
    if (document.querySelector(`.${CLONE_PANEL_CLASS}`)) return;
    const clone = original.cloneNode(true);
    clone.classList.add(CLONE_PANEL_CLASS);
    const input = clone.querySelector(
      SELECTORS.cloneHttpsInput[0]
    );
    const proxyUrl = input ? withProxy(input.value, mirror) : void 0;
    if (input && proxyUrl) {
      input.value = proxyUrl;
      input.readOnly = true;
    }
    if (input && proxyUrl) {
      replaceCopyButton(clone, proxyUrl);
    }
    original.parentNode.insertBefore(clone, original.nextSibling);
  }
  function replaceCopyButton(clone, proxyUrl) {
    const oldBtn = queryFirst(clone, SELECTORS.cloneCopyButton) ?? clone.querySelector("button");
    if (!(oldBtn == null ? void 0 : oldBtn.parentNode)) return;
    const newBtn = document.createElement("button");
    newBtn.className = `${CLONE_BTN_CLASS} ${oldBtn.className || ""}`;
    newBtn.innerHTML = oldBtn.innerHTML;
    newBtn.addEventListener("click", (e) => {
      e.stopPropagation();
      e.preventDefault();
      const ok = copyToClipboard(proxyUrl);
      const orig = newBtn.innerHTML;
      newBtn.textContent = ok ? "✅ 已复制镜像地址" : "❌ 复制失败";
      setTimeout(() => {
        newBtn.innerHTML = orig;
      }, 1500);
    });
    oldBtn.parentNode.replaceChild(newBtn, oldBtn);
  }
  function copyToClipboard(text) {
    try {
      _GM_setClipboard(text, "text");
      return true;
    } catch {
      return false;
    }
  }
  const RELEASE_LINK_CLASS = "gh-proxy-release-link";
  function processRelease(mirror) {
    const entries = queryAll(document, SELECTORS.releaseEntry);
    for (const entry of entries) {
      if (entry.querySelector(`.${RELEASE_LINK_CLASS}`)) continue;
      const titleLink = queryFirst(entry, SELECTORS.releaseTitleLink);
      if (!(titleLink == null ? void 0 : titleLink.parentNode)) continue;
      const downloadLink = queryFirst(entry, SELECTORS.releaseDownloadLink);
      if (!downloadLink) continue;
      const proxyUrl = withProxy(
        downloadLink.getAttribute("href"),
        mirror
      );
      if (!proxyUrl) continue;
      const newLink = document.createElement("a");
      newLink.href = proxyUrl;
      newLink.textContent = " ⚡ 代理下载";
      newLink.className = RELEASE_LINK_CLASS;
      newLink.target = "_blank";
      newLink.rel = "noopener noreferrer";
      newLink.style.marginLeft = "8px";
      newLink.style.fontWeight = "bold";
      newLink.style.color = "#2c974b";
      titleLink.parentNode.insertBefore(newLink, titleLink.nextSibling);
    }
  }
  let currentMirror = resolveCurrentMirror(loadSettings(), []);
  function applyFeatures() {
    const settings = loadSettings();
    currentMirror = resolveCurrentMirror(settings, menuCtx.mirrors);
    if (!currentMirror) return;
    if (settings.clonePanelEnabled) {
      processCloneContainer(currentMirror);
    }
    if (settings.releaseLinkEnabled) {
      processRelease(currentMirror);
    }
  }
  const menuCtx = {
    mirrors: [],
    settings: loadSettings(),
    onApply: applyFeatures
  };
  const { mirrors } = resolveMirrors((remote) => {
    menuCtx.mirrors = remote;
    registerMenu(menuCtx);
    applyFeatures();
  });
  menuCtx.mirrors = mirrors;
  registerMenu(menuCtx);
  applyFeatures();
  let observerTimer;
  const observer = new MutationObserver(() => {
    if (observerTimer !== void 0) return;
    observerTimer = window.setTimeout(() => {
      observerTimer = void 0;
      applyFeatures();
    }, 100);
  });
  observer.observe(document.body, { childList: true, subtree: true });
  document.addEventListener("turbo:load", applyFeatures);
  document.addEventListener("pjax:end", applyFeatures);

})();