// ==UserScript==
// @name SuperGit - Github国内加速助手
// @namespace https://hsya.top/
// @version 1.10.0
// @description 专为国内开发者打造。自动在 GitHub 页面注入「镜像下载」按钮,覆盖 Release、Raw、Clone ZIP 等 8 大场景,一键选择最优镜像节点,下载速度提升数十倍。
// @author 划水鸭
// @license MIT
// @homepage https://hsya.top/supergit.html
// @supportURL https://hsya.top/supergit.html
// @match *://github.com/*
// @match *://gist.github.com/*
// @match *://*.github.com/*
// @connect github.com
// @connect githubusercontent.com
// @connect codeload.github.com
// @connect raw.githubusercontent.com
// @connect api.akams.cn
// @connect hsya.top
// @connect *
// @icon https://hsya.top/static/img/supergit.png
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant GM_download
// @grant GM_openInTab
// @grant GM_addStyle
// @grant GM_info
// @grant GM_registerMenuCommand
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
if (window.__supergit_injected) return;
window.__supergit_injected = true;
const VERSION = 'v' + GM_info.script.version;
// 图标:GitHub CSP 限制外域 img-src,需通过 GM_xmlhttpRequest 预加载转 base64
const ICON_URL = 'https://hsya.top/static/img/supergit.png';
let ICON_URI = 'data:image/svg+xml,' + encodeURIComponent(
''
);
function preloadIcon() {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET',
url: ICON_URL,
responseType: 'blob',
timeout: 10000,
onload: (resp) => {
try {
const reader = new FileReader();
reader.onloadend = () => {
if (reader.result) ICON_URI = reader.result;
resolve();
};
reader.onerror = () => resolve();
reader.readAsDataURL(resp.response);
} catch {
resolve();
}
},
onerror: () => resolve(),
ontimeout: () => resolve(),
});
});
}
// 下载小图标
const DL_SVG = '';
// GitHub 常见可下载位置配置
const INJECT_SCENARIOS = {
'release-zip': {
label: 'Release ZIP/Tar',
desc: 'Release 页面的源码压缩包',
defaultOn: true
},
'release-asset': {
label: 'Release 附件',
desc: 'Release 页面的上传附件',
defaultOn: true
},
'raw-file': {
label: 'Raw 原始文件',
desc: '代码文件查看页的原始文件',
defaultOn: true
},
'clone-zip': {
label: 'Clone 弹框 ZIP',
desc: '仓库主页 Clone 弹框的 ZIP 下载',
defaultOn: true
},
'repo-archive': {
label: '仓库 Code 按钮',
desc: '仓库主页 Code 按钮旁',
defaultOn: true
},
'lfs-file': {
label: 'LFS 大文件',
desc: 'Git LFS 管理的大文件',
defaultOn: true
},
'gist-raw': {
label: 'Gist 原始文件',
desc: 'Gist 代码片段的原始文件',
defaultOn: true
},
'compare-diff': {
label: 'Compare/Diff 补丁',
desc: '代码比较页面的 Patch/Diff',
defaultOn: true
},
};
// ===== 状态 =====
let allNodes = [];
let visibleKeys = [];
let panelOpen = false;
let injectConfig = {};
// ===== Storage 封装 (GM API) =====
function gmGet(keys) {
const result = {};
if (typeof keys === 'string') keys = [keys];
for (const k of keys) {
try {
result[k] = GM_getValue(k);
} catch {
result[k] = undefined;
}
}
return result;
}
function gmSet(obj) {
for (const [k, v] of Object.entries(obj)) {
try {
GM_setValue(k, v);
} catch (e) {
console.error('[SuperGit] GM_setValue error:', e);
}
}
}
// ===== 跨域请求 (GM_xmlhttpRequest) =====
function gmFetch(url, options = {}) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: options.method || 'GET',
url: url,
headers: options.headers || {},
responseType: options.responseType || '',
timeout: options.timeout || 30000,
onload: (resp) => {
if (resp.status >= 200 && resp.status < 400) {
resolve(resp);
} else {
reject(new Error(`HTTP ${resp.status}`));
}
},
onerror: (err) => reject(err),
ontimeout: () => reject(new Error('Timeout')),
});
});
}
// ===== 从 API 加载镜像节点 =====
async function fetchMirrorNodesFromAPI() {
const resp = await gmFetch('https://api.akams.cn/github');
if (resp.status >= 200 && resp.status < 400) {
const data = typeof resp.response === 'string' ? JSON.parse(resp.response) : resp.response;
if (data.code === 200 && data.data && Array.isArray(data.data)) {
const filtered = data.data
.filter(node => node.latency > 0 && node.latency < 1500)
.sort((a, b) => a.latency - b.latency);
if (!filtered.length) throw new Error('API返回节点列表为空');
return filtered;
}
throw new Error('API返回数据格式错误');
}
throw new Error(`HTTP错误`);
}
// ===== 从远程 JS 兜底加载镜像节点 =====
async function fetchMirrorNodesFromFallback() {
console.log('[SuperGit] 正在从远程脚本加载镜像节点...');
const resp = await gmFetch('https://hsya.top/static/js/super-git.js');
if (resp.status !== 200) throw new Error(`远程脚本HTTP错误: ${resp.status}`);
const text = typeof resp.response === 'string' ? resp.response : JSON.stringify(resp.response);
// 提取 DEFAULT_NODES 数组
const match = text.match(/const\s+DEFAULT_NODES\s*=\s*\[([\s\S]*?)\]/);
if (!match) throw new Error('无法从远程脚本提取节点列表');
// 解析所有 URL
const urlRegex = /"([^"]+)"/g;
const nodes = [];
let urlMatch;
while ((urlMatch = urlRegex.exec(match[1])) !== null) {
nodes.push(urlMatch[1]);
}
if (!nodes.length) throw new Error('远程脚本中未找到节点URL');
// 对节点进行测速
console.log(`[SuperGit] 找到 ${nodes.length} 个备用节点,开始测速...`);
const results = await testNodesWithFallback(nodes);
const valid = results
.filter(r => r.success)
.map(r => ({ url: r.url, latency: r.latencyMs }))
.sort((a, b) => a.latency - b.latency);
if (!valid.length) throw new Error('备用节点测速全部失败');
console.log(`[SuperGit] 备用节点测速完成: ${valid.length} 个可用`);
return valid;
}
// ===== 使用 GM_xmlhttpRequest 对备用节点列表测速 =====
function testNodesWithFallback(urls) {
return new Promise((resolve) => {
const results = [];
const queue = [...urls];
const timeout = 1500;
const concurrency = 8;
let active = 0;
let finished = false;
// 超时保护:60秒后强制结束
const safetyTimer = setTimeout(() => {
if (!finished) {
finished = true;
results.sort((a, b) => {
if (a.success !== b.success) return a.success ? -1 : 1;
return a.latencyMs - b.latencyMs;
});
resolve(results);
}
}, 60000);
function next() {
while (active < concurrency && queue.length) {
const url = queue.shift();
active++;
const start = Date.now();
const testUrl = url + (url.includes('?') ? '&' : '?') + '_=' + Date.now() + Math.random();
GM_xmlhttpRequest({
method: 'GET',
url: testUrl,
timeout: timeout,
onload: () => {
results.push({ url, success: true, latencyMs: Date.now() - start });
active--;
next();
},
onerror: () => {
results.push({ url, success: false, latencyMs: timeout, errorMsg: '连接失败' });
active--;
next();
},
ontimeout: () => {
results.push({ url, success: false, latencyMs: timeout, errorMsg: '超时' });
active--;
next();
}
});
}
if (queue.length === 0 && active === 0) {
if (!finished) {
finished = true;
clearTimeout(safetyTimer);
results.sort((a, b) => {
if (a.success !== b.success) return a.success ? -1 : 1;
return a.latencyMs - b.latencyMs;
});
resolve(results);
}
}
}
next();
});
}
// ===== 硬编码最后兜底节点(API + 远程脚本均不可用时) =====
const LAST_RESORT_NODES = [
"https://gh-proxy.com/",
"https://ghproxy.net/",
"https://hub.ddayh.com/",
"https://fastgit.cc/",
"https://ghproxy.053000.xyz/",
"https://gh.con.sh/"
];
// ===== 加载镜像节点 (原 background.js loadMirrorNodes) =====
async function loadMirrorNodes(source) {
source = source || '自动';
const oldNodes = allNodes.slice();
try {
console.log(`[${source}刷新] 正在从 API 加载镜像节点...`);
allNodes = await fetchMirrorNodesFromAPI();
console.log(`[${source}刷新] 加载了 ${allNodes.length} 个镜像节点 (API)`);
// 成功才更新缓存和时间戳
gmSet({
mirrorNodes: allNodes,
sg_nodes_update_time: Date.now()
});
} catch (error) {
console.warn(`[${source}刷新] API 加载失败:`, error.message);
try {
allNodes = await fetchMirrorNodesFromFallback();
console.log(`[${source}刷新] 从兜底脚本加载了 ${allNodes.length} 个镜像节点`);
gmSet({
mirrorNodes: allNodes,
sg_nodes_update_time: Date.now()
});
} catch (fallbackError) {
console.error(`[${source}刷新] 兜底加载也失败:`, fallbackError);
// 全部外部资源失败,使用硬编码节点做最后测速
try {
console.log(`[${source}刷新] 使用硬编码兜底节点...`);
const results = await testNodesWithFallback(LAST_RESORT_NODES);
const valid = results
.filter(r => r.success)
.map(r => ({ url: r.url, latency: r.latencyMs }))
.sort((a, b) => a.latency - b.latency);
if (valid.length) {
allNodes = valid;
} else {
// 所有方式均失败,保留旧节点
allNodes = oldNodes;
}
} catch {
allNodes = oldNodes;
}
// 不更新缓存,让下次启动时重新尝试
}
}
}
// ===== 初始化 =====
async function init() {
try {
// 预加载图标(绕过 GitHub CSP)— 后台执行不阻塞
const iconPromise = preloadIcon();
// 优先从 GM storage 加载缓存
const store = gmGet(['mirrorNodes', 'sg_visible_keys', 'sg_inject_config', 'sg_nodes_update_time']);
if (store.mirrorNodes && Array.isArray(store.mirrorNodes) && store.mirrorNodes.length > 0) {
allNodes = store.mirrorNodes;
console.log('[SuperGit] 从本地存储加载镜像节点:', allNodes.length);
}
visibleKeys = store.sg_visible_keys || [];
if (!visibleKeys.length && allNodes.length) {
visibleKeys = allNodes.slice(0, 10).map(n => nodeKey(n));
saveVisibleKeys();
}
// 加载注入配置
injectConfig = store.sg_inject_config || {};
Object.keys(INJECT_SCENARIOS).forEach(k => {
if (injectConfig[k] === undefined) {
injectConfig[k] = INJECT_SCENARIOS[k].defaultOn;
}
});
saveInjectConfig();
// 立即注入 UI(不等待节点加载,也不等待图标)
injectUI();
bindEvents();
refreshNodeList();
setupInject();
// 后台加载图标
iconPromise.then(() => {
// 图标加载完成后更新面板和弹窗中的图标
document.querySelectorAll('#supergit-fab img, #supergit-panel h3 img, #sg-download-modal h4 img').forEach(img => {
img.src = ICON_URI;
});
});
// 周期刷新 (每小时)
setInterval(loadMirrorNodes, 60 * 60 * 1000);
// 缓存过期则刷新(异步,不阻塞 UI)
const lastUpdate = store.sg_nodes_update_time || 0;
const expired = Date.now() - lastUpdate > 60 * 60 * 1000;
if (expired || !allNodes.length) {
console.log(`[SuperGit] 缓存${expired ? '已过期' : '为空'},正在刷新节点...`);
loadMirrorNodes().then(() => {
const validKeys = new Set(allNodes.map(n => nodeKey(n)));
visibleKeys = visibleKeys.filter(k => validKeys.has(k));
if (!visibleKeys.length && allNodes.length) visibleKeys = allNodes.slice(0, 10).map(n => nodeKey(n));
saveVisibleKeys();
refreshNodeList();
if (!allNodes.length) autoRefreshNodes();
});
} else {
// 缓存未过期,后台异步加载最新节点
if (allNodes.length) {
console.log('[SuperGit] 缓存可用,后台异步加载最新节点...');
loadMirrorNodes().then(() => {
const validKeys = new Set(allNodes.map(n => nodeKey(n)));
visibleKeys = visibleKeys.filter(k => validKeys.has(k));
if (!visibleKeys.length && allNodes.length) visibleKeys = allNodes.slice(0, 10).map(n => nodeKey(n));
saveVisibleKeys();
refreshNodeList();
});
}
}
// 注册油猴菜单命令
GM_registerMenuCommand('打开节点管理面板', () => {
if (!panelOpen) togglePanel();
});
} catch (err) {
console.error('[SuperGit] 初始化出错:', err);
}
}
// ===== 工具函数 =====
function nodeKey(node) {
return node.url || '';
}
function shortDomain(url) {
try {
const h = new URL(url).hostname.replace('www.', '');
const parts = h.split('.');
return parts.length > 2 ? parts.slice(-2).join('.') : h;
} catch {
return url;
}
}
function delayClass(ms) {
return ms < 300 ? 'sg-delay-fast' : ms < 800 ? 'sg-delay-medium' : 'sg-delay-slow';
}
function dmDelayClass(ms) {
return ms < 300 ? 'sg-dm-fast' : ms < 800 ? 'sg-dm-medium' : 'sg-dm-slow';
}
function delayPercent(ms) {
return Math.min(100, (ms / 1500) * 100);
}
function saveVisibleKeys() {
gmSet({
sg_visible_keys: visibleKeys
});
}
function saveInjectConfig() {
gmSet({
sg_inject_config: injectConfig
});
}
// 生成镜像链接
function mirrorUrl(githubUrl, nodeUrl) {
if (!nodeUrl || !githubUrl) return '';
const base = nodeUrl.replace(/\/+$/, '');
const path = githubUrl.replace(/^\/+/, '');
return base + '/' + path;
}
// ===== 私有仓库检测 =====
const REPO_VISIBILITY_CACHE = new Map();
const GITHUB_RESERVED_OWNERS = new Set([
'about', 'account', 'apps', 'codespaces', 'collections', 'contact', 'customer-stories',
'dashboard', 'enterprise', 'events', 'explore', 'features', 'home', 'issues', 'join',
'login', 'logout', 'marketplace', 'new', 'notifications', 'orgs', 'organizations',
'pricing', 'projects', 'pulls', 'readme', 'search', 'security', 'settings', 'signup',
'site', 'sponsors', 'stars', 'topics', 'trending', 'users', 'watching'
]);
// 从 GitHub / codeload / raw 链接中解析 owner/repo
function parseRepoFromUrl(url) {
if (!url) return '';
let u;
try {
u = new URL(url, location.href);
} catch {
return '';
}
const host = u.hostname.toLowerCase().replace(/^www\./, '');
const parts = u.pathname.split('/').filter(Boolean);
if (!parts.length) return '';
if (host === 'codeload.github.com' || host === 'raw.githubusercontent.com') {
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : '';
}
if (host !== 'github.com' && !host.endsWith('.github.com')) return '';
if (host === 'gist.github.com') return '';
if (GITHUB_RESERVED_OWNERS.has(parts[0].toLowerCase())) return '';
if (parts.length < 2) return '';
return `${parts[0]}/${parts[1].replace(/\.git$/i, '')}`;
}
// 只对当前页面所属仓库做同步判断,避免把页面上的 Public/Private 标签错用到其他仓库
function isPrivateRepo(githubUrl) {
const fullName = parseRepoFromUrl(githubUrl);
if (!fullName) return false;
const cacheKey = fullName.toLowerCase();
if (REPO_VISIBILITY_CACHE.has(cacheKey)) return REPO_VISIBILITY_CACHE.get(cacheKey);
// 当前页面必须就是这个仓库(GitHub SPA 导航后 meta 可能滞后,因此以 location 为准)
const pageRepo = parseRepoFromUrl(location.href);
if (!pageRepo || pageRepo.toLowerCase() !== cacheKey) return false;
let isPrivate = null;
const pageNwo = (document.querySelector('meta[name="octolytics-dimension-repository_nwo"]')
?.getAttribute('content') || '').trim();
const pagePublic = document.querySelector('meta[name="octolytics-dimension-repository_public"]')
?.getAttribute('content');
if (pageNwo && pageNwo.toLowerCase() === cacheKey) {
if (pagePublic === 'false') {
isPrivate = true;
} else if (pagePublic === 'true') {
isPrivate = false;
}
}
if (isPrivate === null) {
const label = (document.querySelector('[data-testid="repo-visibility-label"]')
?.textContent || '').trim().toLowerCase();
if (label) isPrivate = label.includes('private') || label.includes('私有');
}
if (isPrivate === null) {
const oldLabel = (document.querySelector('#repo-title-component .Label, #repo-title-component [class*="Label"]')
?.textContent || '').trim().toLowerCase();
if (oldLabel) isPrivate = oldLabel.includes('private') || oldLabel.includes('私有');
}
if (isPrivate !== null) REPO_VISIBILITY_CACHE.set(cacheKey, isPrivate);
return isPrivate === true;
}
// 兜底:节点为空时自动刷新一次
let _autoRefreshing = false;
function autoRefreshNodes(onDone) {
if (_autoRefreshing || allNodes.length > 0) {
onDone?.();
return;
}
_autoRefreshing = true;
console.log('[SuperGit] 节点列表为空,自动刷新...');
loadMirrorNodes().then(() => {
_autoRefreshing = false;
if (allNodes.length) {
visibleKeys = visibleKeys.filter(k => allNodes.some(n => nodeKey(n) === k));
if (!visibleKeys.length) visibleKeys = allNodes.slice(0, 10).map(n => nodeKey(n));
saveVisibleKeys();
refreshNodeList();
showToast(`自动刷新成功,${allNodes.length} 个节点`);
} else {
showToast('自动刷新失败,请手动刷新');
}
onDone?.();
}).catch(() => {
_autoRefreshing = false;
showToast('自动刷新失败,请手动刷新');
onDone?.();
});
}
function showToast(msg) {
const t = document.createElement('div');
t.className = 'sg-toast';
t.textContent = msg;
document.body.appendChild(t);
requestAnimationFrame(() => t.classList.add('sg-show'));
setTimeout(() => {
t.classList.remove('sg-show');
setTimeout(() => t.remove(), 300);
}, 2000);
}
// 私有仓库提示弹窗(阻止镜像下载)
function showPrivateRepoNotice() {
let modal = document.getElementById('sg-private-notice');
if (modal) {
modal.classList.add('sg-visible');
return;
}
modal = document.createElement('div');
modal.id = 'sg-private-notice';
modal.innerHTML = `
!
私有仓库,不支持镜像下载
第三方镜像无法访问私有仓库,请使用 GitHub 原生下载(Download ZIP / Release 附件)或 git clone。
`;
document.body.appendChild(modal);
const close = () => {
if (!modal) return;
modal.classList.remove('sg-visible');
document.removeEventListener('keydown', onKey);
setTimeout(() => modal.remove(), 200);
};
const onKey = (e) => {
if (e.key === 'Escape') close();
};
modal.querySelector('.sg-private-overlay').addEventListener('click', close);
modal.querySelector('.sg-private-ok').addEventListener('click', close);
document.addEventListener('keydown', onKey);
requestAnimationFrame(() => modal.classList.add('sg-visible'));
}
// ===== 注入 CSS =====
GM_addStyle(`
/* SuperGit 右下角浮动按钮 */
#supergit-fab {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 2147483647;
width: 48px;
height: 48px;
border-radius: 50%;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background: #2da44e;
color: #fff;
font-size: 22px;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
transition: all 0.2s ease;
line-height: 1;
}
#supergit-fab:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
}
#supergit-fab:active {
transform: scale(0.95);
}
#supergit-fab svg {
width: 24px;
height: 24px;
fill: currentColor;
}
/* 脉冲动画 - 有节点时绿色脉冲 */
@keyframes sg-pulse {
0% { box-shadow: 0 0 0 0 rgba(45, 164, 78, 0.5); }
70% { box-shadow: 0 0 0 10px rgba(45, 164, 78, 0); }
100% { box-shadow: 0 0 0 0 rgba(45, 164, 78, 0); }
}
#supergit-fab.sg-pulse {
animation: sg-pulse 2s infinite;
}
/* 面板遮罩 */
#supergit-overlay {
position: fixed;
inset: 0;
z-index: 2147483646;
background: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 0.2s ease;
pointer-events: none;
}
#supergit-overlay.sg-visible {
opacity: 1;
pointer-events: auto;
}
/* 管理面板 */
#supergit-panel {
position: fixed;
bottom: 84px;
right: 24px;
z-index: 2147483647;
width: 380px;
max-height: 520px;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 12px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
color: #e6edf3;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 14px;
overflow: hidden;
transform: translateY(20px) scale(0.95);
opacity: 0;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
display: flex;
flex-direction: column;
}
#supergit-panel.sg-visible {
transform: translateY(0) scale(1);
opacity: 1;
pointer-events: auto;
}
/* 面板头部 */
.sg-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 12px;
border-bottom: 1px solid #21262d;
flex-shrink: 0;
}
.sg-panel-header h3 {
margin: 0;
font-size: 15px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
color: #e6edf3;
}
.sg-panel-header h3 svg {
width: 20px;
height: 20px;
fill: #2da44e;
}
.sg-close-btn {
width: 32px;
height: 32px;
border: none;
border-radius: 6px;
background: transparent;
color: #8b949e;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.sg-close-btn:hover {
background: #21262d;
color: #e6edf3;
}
/* 状态栏 */
.sg-status-bar {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 20px;
font-size: 12px;
color: #8b949e;
border-bottom: 1px solid #21262d;
}
.sg-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.sg-status-dot.sg-online { background: #2da44e; }
.sg-status-dot.sg-offline { background: #6e7681; }
.sg-status-bar .sg-last-update {
margin-left: auto;
}
/* 工具栏 */
.sg-toolbar {
display: flex;
gap: 8px;
padding: 12px 20px;
border-bottom: 1px solid #21262d;
}
.sg-toolbar-btn {
padding: 6px 12px;
border: 1px solid #30363d;
border-radius: 6px;
background: #21262d;
color: #e6edf3;
font-size: 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
transition: all 0.15s;
white-space: nowrap;
}
.sg-toolbar-btn:hover {
background: #30363d;
border-color: #8b949e;
}
.sg-toolbar-btn svg {
width: 14px;
height: 14px;
fill: currentColor;
}
.sg-toolbar-btn.sg-loading {
opacity: 0.6;
pointer-events: none;
}
.sg-toolbar-btn svg.sg-spin {
animation: sg-spin 1s linear infinite;
}
@keyframes sg-spin {
to { transform: rotate(360deg); }
}
/* 节点列表 */
.sg-node-list {
max-height: 280px;
overflow-y: auto;
padding: 8px 0;
flex: 1;
}
.sg-node-list::-webkit-scrollbar {
width: 6px;
}
.sg-node-list::-webkit-scrollbar-thumb {
background: #30363d;
border-radius: 3px;
}
.sg-node-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 20px;
transition: background 0.15s;
}
.sg-node-item:hover {
background: #161b22;
}
/* 自定义 Checkbox */
.sg-checkbox {
position: relative;
width: 16px;
height: 16px;
flex-shrink: 0;
cursor: pointer;
}
.sg-checkbox input {
position: absolute;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
margin: 0;
}
.sg-checkbox .sg-checkmark {
width: 16px;
height: 16px;
border: 1.5px solid #30363d;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
background: #0d1117;
}
.sg-checkbox input:checked + .sg-checkmark {
background: #2da44e;
border-color: #2da44e;
}
.sg-checkbox input:checked + .sg-checkmark::after {
content: '';
width: 4px;
height: 8px;
border: solid #fff;
border-width: 0 2px 2px 0;
transform: rotate(45deg) translate(-0.5px, -0.5px);
}
.sg-node-info {
flex: 1;
min-width: 0;
}
.sg-node-domain {
font-size: 13px;
font-weight: 500;
color: #e6edf3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sg-node-delay {
display: flex;
align-items: center;
gap: 6px;
margin-top: 2px;
}
.sg-delay-bar {
height: 3px;
border-radius: 2px;
background: #21262d;
flex: 1;
max-width: 60px;
overflow: hidden;
}
.sg-delay-bar-fill {
height: 100%;
border-radius: 2px;
transition: width 0.3s ease;
}
.sg-delay-text {
font-size: 11px;
color: #8b949e;
}
/* 延迟颜色等级 */
.sg-delay-fast { color: #2da44e; }
.sg-delay-fast .sg-delay-bar-fill { background: #2da44e; }
.sg-delay-medium { color: #d29922; }
.sg-delay-medium .sg-delay-bar-fill { background: #d29922; }
.sg-delay-slow { color: #f85149; }
.sg-delay-slow .sg-delay-bar-fill { background: #f85149; }
.sg-node-actions {
flex-shrink: 0;
}
.sg-test-btn {
padding: 4px 8px;
border: 1px solid #30363d;
border-radius: 4px;
background: transparent;
color: #8b949e;
font-size: 11px;
cursor: pointer;
transition: all 0.15s;
}
.sg-test-btn:hover {
background: #21262d;
color: #e6edf3;
border-color: #8b949e;
}
/* 空状态 */
.sg-empty {
padding: 32px 20px;
text-align: center;
color: #8b949e;
font-size: 13px;
}
/* 提示消息 */
.sg-toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(20px);
z-index: 2147483648;
background: #1f6feb;
color: #fff;
padding: 8px 20px;
border-radius: 6px;
font-size: 13px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
opacity: 0;
transition: all 0.25s ease;
pointer-events: none;
white-space: nowrap;
}
.sg-toast.sg-show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
/* ===== 私有仓库提示弹窗 ===== */
#sg-private-notice {
position: fixed;
inset: 0;
z-index: 2147483649;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
#sg-private-notice.sg-visible {
opacity: 1;
pointer-events: auto;
}
.sg-private-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.6);
}
.sg-private-card {
position: relative;
width: 380px;
max-width: calc(100vw - 32px);
padding: 20px;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 12px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
color: #c9d1d9;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
transform: translateY(10px) scale(0.97);
transition: transform 0.2s ease;
}
#sg-private-notice.sg-visible .sg-private-card {
transform: translateY(0) scale(1);
}
.sg-private-icon {
width: 32px;
height: 32px;
margin-bottom: 12px;
border-radius: 50%;
background: #da3633;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 700;
}
.sg-private-card h4 {
margin: 0 0 8px;
font-size: 16px;
color: #f0f6fc;
}
.sg-private-card p {
margin: 0 0 16px;
font-size: 13px;
line-height: 1.6;
color: #8b949e;
}
.sg-private-card code {
padding: 1px 5px;
border-radius: 4px;
background: #21262d;
font-size: 12px;
}
.sg-private-ok {
padding: 6px 16px;
border: none;
border-radius: 6px;
background: #238636;
color: #fff;
font-size: 13px;
cursor: pointer;
}
.sg-private-ok:hover {
background: #2ea043;
}
/* ===== 注入下载按钮(绿色文字按钮) ===== */
.sg-dl-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border: 1px solid #2da44e;
border-radius: 5px;
background: transparent;
color: #2da44e;
font-size: 12px;
font-weight: 500;
cursor: pointer;
vertical-align: middle;
margin-left: 6px;
transition: all 0.15s;
flex-shrink: 0;
white-space: nowrap;
line-height: 1.4;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}
.sg-dl-btn:hover {
background: #2da44e;
color: #fff;
}
.sg-dl-btn svg {
width: 12px;
height: 12px;
fill: currentColor;
flex-shrink: 0;
}
/* ===== 下载弹窗 ===== */
#sg-download-modal {
position: fixed;
inset: 0;
z-index: 2147483648;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
#sg-download-modal.sg-visible {
opacity: 1;
pointer-events: auto;
}
#sg-dm-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.6);
}
#sg-dm-card {
position: relative;
width: 400px;
max-height: 480px;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 12px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
transform: translateY(12px) scale(0.97);
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
#sg-download-modal.sg-visible #sg-dm-card {
transform: translateY(0) scale(1);
}
/* 弹窗头部 */
.sg-dm-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid #21262d;
flex-shrink: 0;
}
.sg-dm-header h4 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #ffffff;
display: flex;
align-items: center;
gap: 8px;
}
.sg-dm-header h4 img, .sg-dm-header h4 .sg-dm-icon {
width: 16px;
height: 16px;
}
.sg-dm-refresh-btn {
width: 28px;
height: 28px;
border: none;
border-radius: 6px;
background: transparent;
color: #8b949e;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.sg-dm-refresh-btn:hover {
background: #21262d;
color: #e6edf3;
}
.sg-dm-refresh-btn svg {
width: 14px;
height: 14px;
fill: currentColor;
transition: transform 0.3s;
}
.sg-dm-refresh-btn.sg-spin svg {
animation: sg-spin 1s linear infinite;
}
/* 弹窗文件信息 */
.sg-dm-fileinfo {
padding: 10px 18px;
background: #161b22;
border-bottom: 1px solid #21262d;
font-size: 12px;
color: #8b949e;
flex-shrink: 0;
}
.sg-dm-fileinfo .sg-dm-filename {
color: #e6edf3;
font-weight: 500;
font-size: 13px;
word-break: break-all;
}
/* 弹窗节点列表 */
.sg-dm-nodelist {
flex: 1;
overflow-y: auto;
padding: 6px 0;
}
.sg-dm-nodelist::-webkit-scrollbar {
width: 6px;
}
.sg-dm-nodelist::-webkit-scrollbar-thumb {
background: #30363d;
border-radius: 3px;
}
.sg-dm-node-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 18px;
transition: background 0.12s;
}
.sg-dm-node-item:hover {
background: #161b22;
}
.sg-dm-domain {
flex: 1;
font-size: 13px;
color: #e6edf3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sg-dm-node-delay {
font-size: 11px;
color: #8b949e;
white-space: nowrap;
min-width: 40px;
text-align: right;
}
.sg-dm-node-delay.sg-dm-fast { color: #2da44e; }
.sg-dm-node-delay.sg-dm-medium { color: #d29922; }
.sg-dm-node-delay.sg-dm-slow { color: #f85149; }
.sg-dm-download-btn {
padding: 4px 12px;
border: 1px solid #2da44e;
border-radius: 5px;
background: transparent;
color: #2da44e;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.sg-dm-download-btn:hover {
background: #2da44e;
color: #fff;
}
/* ===== 管理面板 Tab ===== */
.sg-tabs {
display: flex;
border-bottom: 1px solid #21262d;
flex-shrink: 0;
}
.sg-tab {
flex: 1;
padding: 10px 0;
text-align: center;
font-size: 13px;
color: #8b949e;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.15s;
background: none;
border-top: none;
border-left: none;
border-right: none;
}
.sg-tab:hover {
color: #e6edf3;
background: #161b22;
}
.sg-tab.sg-active {
color: #e6edf3;
border-bottom-color: #2da44e;
}
.sg-tab-content {
display: none;
overflow-y: auto;
flex: 1;
}
.sg-tab-content.sg-active {
display: flex;
flex-direction: column;
}
/* 注入设置区域 */
.sg-inject-setting {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
border-bottom: 1px solid #21262d;
font-size: 13px;
}
.sg-inject-setting:last-child {
border-bottom: none;
}
.sg-inject-label {
display: flex;
flex-direction: column;
gap: 2px;
}
.sg-inject-label .sg-label-title {
color: #e6edf3;
font-weight: 500;
}
.sg-inject-label .sg-label-desc {
color: #6e7681;
font-size: 11px;
}
/* ===== 开关(增强样式) ===== */
.sg-toggle {
position: relative;
width: 40px;
height: 22px;
cursor: pointer;
display: inline-block;
flex-shrink: 0;
}
.sg-toggle input {
position: absolute;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
margin: 0;
z-index: 1;
}
.sg-toggle .sg-toggle-track {
width: 40px;
height: 22px;
border-radius: 11px;
background: #30363d;
transition: background 0.25s;
position: relative;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3);
}
.sg-toggle input:checked + .sg-toggle-track {
background: #2da44e;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2), 0 0 0 2px rgba(45, 164, 78, 0.2);
}
.sg-toggle .sg-toggle-track::after {
content: '';
position: absolute;
width: 16px;
height: 16px;
border-radius: 50%;
background: #fff;
top: 3px;
left: 3px;
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.sg-toggle input:checked + .sg-toggle-track::after {
transform: translateX(18px);
}
/* 开关状态文字 */
.sg-toggle-status {
font-size: 11px;
margin-left: 6px;
flex-shrink: 0;
}
.sg-toggle-status.sg-on {
color: #2da44e;
}
.sg-toggle-status.sg-off {
color: #6e7681;
}
/* ===== 底部 footer ===== */
.sg-footer {
padding: 8px 16px;
border-top: 1px solid #21262d;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 11px;
color: #484f58;
flex-shrink: 0;
background: #010409;
}
.sg-footer a {
color: #8b949e;
text-decoration: none;
transition: color 0.15s;
}
.sg-footer a:hover {
color: #58a6ff;
}
`);
// ===== 注入 UI =====
function injectUI() {
// 浮动按钮
const fab = document.createElement('button');
fab.id = 'supergit-fab';
fab.title = 'SuperGit镜像节点管理';
const fabImg = document.createElement('img');
fabImg.src = ICON_URI;
fabImg.alt = 'SuperGit';
fabImg.style.cssText = 'width:32px;height:32px;';
fab.appendChild(fabImg);
document.body.appendChild(fab);
// 遮罩
const overlay = document.createElement('div');
overlay.id = 'supergit-overlay';
document.body.appendChild(overlay);
// 管理面板
const panel = document.createElement('div');
panel.id = 'supergit-panel';
panel.innerHTML = buildPanelHTML();
document.body.appendChild(panel);
if (allNodes.length > 0) fab.classList.add('sg-pulse');
// 下载弹窗
const modal = document.createElement('div');
modal.id = 'sg-download-modal';
modal.innerHTML = buildModalHTML();
document.body.appendChild(modal);
}
function buildPanelHTML() {
const scenariosHTML = Object.entries(INJECT_SCENARIOS).map(([key, cfg]) => {
const checked = injectConfig[key] ? 'checked' : '';
const statusClass = injectConfig[key] ? 'sg-on' : 'sg-off';
const statusText = injectConfig[key] ? '已开启' : '已关闭';
return `
`;
}).join('');
return `
管理各位置下载按钮的显示,关闭后需刷新页面生效。
${scenariosHTML}
`;
}
function buildModalHTML() {
return `
`;
}
// ===== 面板切换 =====
function togglePanel() {
const panel = document.getElementById('supergit-panel');
const overlay = document.getElementById('supergit-overlay');
const fab = document.getElementById('supergit-fab');
panelOpen = !panelOpen;
fab.classList.remove('sg-pulse');
panel.classList.toggle('sg-visible', panelOpen);
overlay.classList.toggle('sg-visible', panelOpen);
if (panelOpen) refreshNodeList();
}
// ===== 绑定事件 =====
function bindEvents() {
const fab = document.getElementById('supergit-fab');
const overlay = document.getElementById('supergit-overlay');
fab.addEventListener('click', togglePanel);
overlay.addEventListener('click', togglePanel);
document.getElementById('sg-close-btn').addEventListener('click', togglePanel);
// Tab 切换
const panel = document.getElementById('supergit-panel');
panel.querySelectorAll('.sg-tab').forEach(tab => {
tab.addEventListener('click', () => {
panel.querySelectorAll('.sg-tab').forEach(t => t.classList.remove('sg-active'));
panel.querySelectorAll('.sg-tab-content').forEach(c => c.classList.remove(
'sg-active'));
tab.classList.add('sg-active');
document.getElementById('sg-tab-' + tab.dataset.tab).classList.add('sg-active');
});
});
// 注入配置 checkbox
panel.querySelectorAll('.sg-inject-cb').forEach(cb => {
cb.addEventListener('change', () => {
const key = cb.dataset.key;
injectConfig[key] = cb.checked;
saveInjectConfig();
const statusEl = document.querySelector(`[data-status-for="${key}"]`);
if (statusEl) {
statusEl.textContent = cb.checked ? '已开启' : '已关闭';
statusEl.className = 'sg-toggle-status ' + (cb.checked ? 'sg-on' : 'sg-off');
}
showToast(cb.checked ? '已开启,刷新页面后生效' : '已关闭,刷新页面后生效');
});
});
// 刷新节点
document.getElementById('sg-refresh-btn').addEventListener('click', refreshNodes);
// 全选 / 全不选 / 默认 Top10
document.getElementById('sg-select-all-btn').addEventListener('click', () => {
visibleKeys = allNodes.map(n => nodeKey(n));
saveVisibleKeys();
refreshNodeList();
showToast('已全选所有节点');
});
document.getElementById('sg-deselect-all-btn').addEventListener('click', () => {
visibleKeys = [];
saveVisibleKeys();
refreshNodeList();
showToast('已取消所有节点');
});
document.getElementById('sg-top10-btn').addEventListener('click', () => {
visibleKeys = allNodes.slice(0, 10).map(n => nodeKey(n));
saveVisibleKeys();
refreshNodeList();
showToast('已恢复默认 Top10 节点');
});
// 节点列表委托
document.getElementById('sg-node-list').addEventListener('change', (e) => {
if (!e.target.classList.contains('sg-node-cb')) return;
const key = e.target.dataset.key;
if (e.target.checked) {
if (!visibleKeys.includes(key)) visibleKeys.push(key);
} else {
visibleKeys = visibleKeys.filter(k => k !== key);
}
saveVisibleKeys();
});
document.getElementById('sg-node-list').addEventListener('click', (e) => {
const btn = e.target.closest('.sg-test-btn');
if (btn) testNode(btn.dataset.url, btn);
});
// 下载弹窗事件
const modal = document.getElementById('sg-download-modal');
document.getElementById('sg-dm-close').addEventListener('click', () => modal.classList.remove(
'sg-visible'));
document.getElementById('sg-dm-overlay').addEventListener('click', () => modal.classList.remove(
'sg-visible'));
// 弹窗刷新按钮
document.getElementById('sg-dm-refresh-btn').addEventListener('click', () => {
const refreshEl = document.getElementById('sg-dm-refresh-btn');
refreshEl.classList.add('sg-spin');
loadMirrorNodes('手动').then(() => {
// 获取当前弹窗中的 githubUrl 并重新渲染
const firstBtn = document.querySelector('.sg-dm-download-btn');
const ghUrl = firstBtn ? firstBtn.dataset.ghUrl : '';
renderModalNodeList(ghUrl);
refreshEl.classList.remove('sg-spin');
showToast('节点已刷新');
}).catch(() => {
refreshEl.classList.remove('sg-spin');
showToast('刷新失败');
});
});
document.getElementById('sg-dm-nodelist').addEventListener('click', (e) => {
try {
const btn = e.target.closest('.sg-dm-download-btn');
if (!btn) return;
const ghUrl = btn.dataset.ghUrl;
const nodeUrl = btn.dataset.nodeUrl;
if (!ghUrl || !nodeUrl) return;
if (isPrivateRepo(ghUrl)) {
modal.classList.remove('sg-visible');
showPrivateRepoNotice();
return;
}
const url = mirrorUrl(ghUrl, nodeUrl);
if (!url) return;
downloadViaMirror(url);
showToast('开始下载...');
modal.classList.remove('sg-visible');
} catch (err) {
console.error('[SuperGit] 下载出错:', err);
showToast('下载出错: ' + (err.message || '未知错误'));
}
});
}
// ===== 下载 (GM_download + GM_openInTab 降级) =====
function downloadViaMirror(url) {
if (!url) return;
try {
GM_download({
url: url,
name: url.split('/').pop() || 'download',
saveAs: false,
onerror: (err) => {
console.error('[SuperGit] GM_download 失败,使用新标签页:', err);
GM_openInTab(url, { active: false, insert: true });
}
});
} catch (e) {
console.warn('[SuperGit] GM_download 异常,使用新标签页:', e);
GM_openInTab(url, { active: false, insert: true });
}
}
// ===== 打开下载弹窗 =====
function openDownloadModal(githubUrl, filename) {
const modal = document.getElementById('sg-download-modal');
document.getElementById('sg-dm-filename').textContent = filename || githubUrl;
const visibleNodes = visibleKeys.length ? allNodes.filter(n => visibleKeys.includes(nodeKey(n))) : allNodes
.slice(0, 10);
const listEl = document.getElementById('sg-dm-nodelist');
if (!visibleNodes.length) {
listEl.innerHTML =
'暂无可用节点
正在自动刷新...
也可点击右上角刷新按钮
';
if (!_autoRefreshing) {
autoRefreshNodes(() => {
renderModalNodeList(githubUrl);
});
}
} else {
renderModalNodeList(githubUrl);
}
modal.classList.add('sg-visible');
}
function renderModalNodeList(githubUrl) {
const listEl = document.getElementById('sg-dm-nodelist');
const visibleNodes = visibleKeys.length ? allNodes.filter(n => visibleKeys.includes(nodeKey(n))) : allNodes
.slice(0, 10);
if (!visibleNodes.length) {
listEl.innerHTML =
'暂无可用节点
点击右上角刷新按钮重试
';
return;
}
listEl.innerHTML = visibleNodes.map(node => {
const lat = node.latency || 0;
const cls = dmDelayClass(lat);
return `
${shortDomain(node.url)}
${lat}ms
`;
}).join('');
}
// ===== 注入下载按钮到 GitHub 页面 =====
let injectTimer = null;
function setupInject() {
const anyEnabled = Object.values(injectConfig).some(v => v);
if (!anyEnabled) return;
scheduleInject();
// GitHub 是 SPA,监听路由变化重新注入
let lastUrl = location.href;
const observer = new MutationObserver((mutations) => {
if (location.href !== lastUrl) {
lastUrl = location.href;
scheduleInject(800);
} else {
const hasRelevant = mutations.some(m => {
const target = m.target;
if (target.closest?.(
'details, .dropdown-menu, .js-menu-container, [data-view-component]'))
return true;
for (const node of m.addedNodes) {
if (node.nodeType !== 1) continue;
const html = (node.outerHTML || node.textContent || '').toLowerCase();
if (html.includes('download') || html.includes('release') || html.includes(
'archive') || html.includes('raw') || html.includes('codeload'))
return true;
}
return false;
});
if (hasRelevant) scheduleInject(300);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
setInterval(() => scheduleInject(), 5000);
}
function scheduleInject(delay) {
clearTimeout(injectTimer);
injectTimer = setTimeout(injectDownloadButtons, delay || 500);
}
function createDlBtn(githubUrl, filename) {
const btn = document.createElement('button');
btn.className = 'sg-dl-btn';
btn.title = '镜像下载';
btn.dataset.sgDlUrl = githubUrl;
btn.setAttribute('aria-label', '镜像下载');
btn.innerHTML = DL_SVG + '镜像下载';
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (isPrivateRepo(githubUrl)) {
showPrivateRepoNotice();
return;
}
openDownloadModal(githubUrl, filename || '');
});
return btn;
}
function tryInject(container, link, filename) {
if (!container || container.querySelector('.sg-dl-btn')) return;
if (!link || !link.href) return;
const href = link.href;
if (!href.includes('github.com') && !href.includes('githubusercontent.com') && !href.includes(
'codeload.github.com')) return;
const fname = filename || link.textContent.trim() || href.split('/').pop();
container.appendChild(createDlBtn(href, fname));
}
function injectDownloadButtons() {
const host = location.hostname;
// ===== 1. Release 源码压缩包 =====
if (injectConfig['release-zip']) {
document.querySelectorAll(
'a[href*="codeload.github.com"], a[href*="/archive/refs/"], a[href*="/zipball/"], a[href*="/tarball/"]'
).forEach(link => {
if (link.closest('.sg-dl-btn')) return;
const parent = link.closest('li, .Box-row, div, tr, .liststyle-none') || link.parentElement;
tryInject(parent, link, link.textContent.trim() || 'Source code');
});
document.querySelectorAll('section[class*="release"], .release, [data-test-id="release-body"]').forEach(
section => {
section.querySelectorAll('a[href*="/archive/"]').forEach(link => {
const parent = link.closest('li, .Box-row, div') || link.parentElement;
tryInject(parent, link);
});
});
}
// ===== 2. Release 附件 =====
if (injectConfig['release-asset']) {
document.querySelectorAll('a[href*="/releases/download/"]').forEach(link => {
if (link.closest('.sg-dl-btn')) return;
const row = link.closest('.Box-row, li, tr, div');
if (row) tryInject(row, link);
else tryInject(link.parentElement, link);
});
document.querySelectorAll('a[href*="/releases/expanded_assets/"]').forEach(link => {
const row = link.closest('li, .Box-row, div');
if (row) tryInject(row, link);
});
}
// ===== 3. Raw 原始文件 =====
if (injectConfig['raw-file']) {
document.querySelectorAll('#raw-url, a[id="raw-url"]').forEach(link => {
tryInject(link.parentElement, link);
});
document.querySelectorAll(
'.js-blob-dropdown-content a[href*="raw"], .btn-group a[href*="/raw/"], a[href*="/raw/"][href*="githubusercontent.com"]'
).forEach(link => {
tryInject(link.parentElement, link);
});
document.querySelectorAll('a[role="menuitem"][href*="/raw/"], a[data-ga-click*="raw"]').forEach(
link => {
tryInject(link.parentElement, link);
});
}
// ===== 4. Clone 弹框 Download ZIP =====
if (injectConfig['clone-zip']) {
document.querySelectorAll('details a[href*="codeload.github.com"], details a[href*="/archive/refs/"]')
.forEach(link => {
const details = link.closest('details');
if (!details) return;
const isClone = details.querySelector('[class*="clone"], input[value*="git clone"]');
if (!isClone) return;
const li = link.closest('li') || link.parentElement;
tryInject(li, link, 'Download ZIP');
});
document.querySelectorAll('[data-view-component] a[href*="codeload.github.com"]').forEach(link => {
const container = link.closest('[data-view-component]');
if (!container) return;
const li = link.closest('li, div') || link.parentElement;
tryInject(li, link, 'Download ZIP');
});
}
// ===== 5. 仓库主页 Code 按钮 =====
if (injectConfig['repo-archive']) {
document.querySelectorAll(
'details[data-view-component] a[href*="codeload.github.com"], details[data-view-component] a[href*="/archive/refs/"]'
).forEach(link => {
const li = link.closest('li, .liststyle-none, div') || link.parentElement;
tryInject(li, link, 'Source code');
});
document.querySelectorAll('a[data-ga-click*="download"]').forEach(link => {
const li = link.closest('li') || link.parentElement;
tryInject(li, link, 'Source code');
});
}
// ===== 6. LFS 大文件 =====
if (injectConfig['lfs-file']) {
document.querySelectorAll('a[href*="/info/lfs/objects/"], .lfs-object').forEach(link => {
const parent = link.closest('li, div, tr, .Box-row') || link.parentElement;
tryInject(parent, link);
});
document.querySelectorAll('table a[href*="lfs"]').forEach(link => {
const tr = link.closest('tr');
if (tr) tryInject(tr, link);
});
}
// ===== 7. Gist 原始文件 =====
if (host === 'gist.github.com' && injectConfig['gist-raw']) {
document.querySelectorAll('.gist a[href*="/raw/"], .gist .btn[href*="raw"]').forEach(link => {
tryInject(link.parentElement, link);
});
}
// ===== 8. Compare/Diff 补丁下载 =====
if (injectConfig['compare-diff']) {
document.querySelectorAll('a[href*=".patch"], a[href*=".diff"]').forEach(link => {
const parent = link.closest('li, div, .Box-row, .dropdown-item') || link.parentElement;
tryInject(parent, link, link.textContent.trim() || 'Patch');
});
}
// ===== 通用兜底 =====
document.querySelectorAll('a[download][href*="github"]').forEach(link => {
const parent = link.closest('li, div, .Box-row, tr') || link.parentElement;
tryInject(parent, link);
});
}
// ===== 刷新节点 =====
async function refreshNodes() {
const btn = document.getElementById('sg-refresh-btn');
btn.classList.add('sg-loading');
btn.querySelector('svg').classList.add('sg-spin');
await loadMirrorNodes('手动');
const validKeys = new Set(allNodes.map(n => nodeKey(n)));
visibleKeys = visibleKeys.filter(k => validKeys.has(k));
if (!visibleKeys.length && allNodes.length) visibleKeys = allNodes.slice(0, 10).map(n => nodeKey(n));
saveVisibleKeys();
refreshNodeList();
btn.classList.remove('sg-loading');
btn.querySelector('svg').classList.remove('sg-spin');
showToast(`已刷新,共 ${allNodes.length} 个节点`);
}
// ===== 渲染节点列表 =====
function refreshNodeList() {
const listEl = document.getElementById('sg-node-list');
const statusDot = document.getElementById('sg-dot');
const statusText = document.getElementById('sg-status-text');
if (allNodes.length > 0) {
statusDot.className = 'sg-status-dot sg-online';
const lats = allNodes.map(n => n.latency || 0);
statusText.textContent =
`${allNodes.length} 个节点 · 最低 ${Math.min(...lats)}ms · 平均 ${Math.round(lats.reduce((a, b) => a + b, 0) / lats.length)}ms`;
} else {
statusDot.className = 'sg-status-dot sg-offline';
statusText.textContent = '暂无可用节点,正在自动刷新...';
}
document.getElementById('sg-last-update').textContent = new Date().toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
if (!allNodes.length) {
listEl.innerHTML = '暂无可用镜像节点
正在自动刷新...
';
if (!_autoRefreshing) autoRefreshNodes();
return;
}
listEl.innerHTML = allNodes.map(node => {
const key = nodeKey(node);
const checked = visibleKeys.includes(key) ? 'checked' : '';
const lat = node.latency || 0;
const cls = delayClass(lat);
return `
`;
}).join('');
}
// ===== 单节点测速 =====
function testNode(url, btnEl) {
const orig = btnEl.textContent;
btnEl.textContent = '...';
btnEl.disabled = true;
const start = performance.now();
GM_xmlhttpRequest({
method: 'GET',
url: url,
timeout: 5000,
onload: () => {
const ms = Math.round(performance.now() - start);
btnEl.textContent = ms + 'ms';
btnEl.style.color = '#2da44e';
btnEl.style.borderColor = '#2da44e';
},
onerror: () => {
btnEl.textContent = '超时';
btnEl.style.color = '#f85149';
btnEl.style.borderColor = '#f85149';
},
ontimeout: () => {
btnEl.textContent = '超时';
btnEl.style.color = '#f85149';
btnEl.style.borderColor = '#f85149';
},
onfinally: () => {
setTimeout(() => {
btnEl.textContent = orig;
btnEl.style.color = '';
btnEl.style.borderColor = '';
btnEl.disabled = false;
}, 3000);
}
});
}
init();
})();