复制 Clone 面板并替换为镜像地址;Release 页为每个条目追加代理下载链接。多镜像切换、菜单设置、远程镜像列表更新。
ของเมื่อวันที่
// ==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);
})();