触发浏览器下载事件。
* 关键:Gopeed 浏览器扩展监听的就是这条原生事件——
* 因此脚本只此一条路径,不中转、不接字节,确保下载工具能正常接管。
*/
function clickAnchor(href, filename) {
const a = document.createElement('a');
a.href = href;
if (filename) a.download = filename;
a.rel = 'noopener';
a.style.display = 'none';
document.body.appendChild(a);
a.click();
setTimeout(() => a.remove(), 1000);
}
/** HEAD 预检:确认镜像真的能给文件,再把直链交给浏览器 */
function precheck(url, timeoutMs) {
return gmRequest({ url, method: 'HEAD', timeout: timeoutMs || HEAD_TIMEOUT })
.then((res) => {
const m = /content-length:\s*(\d+)/i.exec(res.responseHeaders || '');
return { ok: true, size: m ? parseInt(m[1], 10) : -1 };
})
.catch((e) => ({ ok: false, error: (e && e.message) || '预检失败' }));
}
const Downloader = {
/** 直链交付:原生 a[download] 点击,发射后即交还浏览器,脚本不再介入 */
deliver(githubUrl, filename) {
clickAnchor(githubUrl, filename);
},
/**
* 自动模式两段式(先快后稳):
* ① fast-path:fresh 候选(5 分钟内预检成功过)直接 fire,跳过预检 → 0ms
* ② 慢路径:逐个短预检(2.5s)轮换,失败换下一个
* ③ 兜底:全部失败时对最快节点直接放行一次(仍是 clickAnchor 原生通道)
*
* 注意:fast-path 故意不刷新 lastOk 时间戳——采用固定窗口而非滑动窗口,
* 保证持续使用时仍会每 5 分钟重新验证一次,避免镜像悄悄失效却一直拿坏链接。
* @returns Promise<{ok, nodeUrl?, blind?, size?, error?, trace:string[]}>
*/
async runAuto(githubUrl, filename, hooks) {
hooks = hooks || {};
if (!NodeStore.nodes.length) await loadNodes('自动下载');
const trace = [];
// fast-path:fresh 候选直接 fire,跳过预检
const fresh = NodeStore.freshCandidates().slice(0, NODE_RETRY_MAX);
if (fresh.length) {
const best = fresh[0];
if (hooks.onNode) hooks.onNode(best, 1, 1, false);
this.deliver(mirrorUrl(githubUrl, best.url), filename);
return { ok: true, nodeUrl: best.url, blind: false, trace };
}
const list = NodeStore.candidates().slice(0, NODE_RETRY_MAX);
if (!list.length) return { ok: false, error: '没有可用镜像节点', trace: [] };
for (let i = 0; i < list.length; i++) {
const node = list[i];
if (hooks.onNode) hooks.onNode(node, i + 1, list.length, false);
const target = mirrorUrl(githubUrl, node.url);
const head = await precheck(target, HEAD_TIMEOUT_FAST);
if (head.ok) {
NodeStore.markOk(node.url);
this.deliver(target, filename);
return { ok: true, nodeUrl: node.url, size: head.size, blind: false, trace };
}
NodeStore.markFail(node.url);
trace.push(Utils.shortDomain(node.url) + ' ✗ ' + head.error);
Log.warn('镜像预检失败 →', Utils.shortDomain(node.url), head.error);
}
// 轮转耗尽:预检可能误判(部分镜像不支持 HEAD),对最快节点直接放行
const best = list[0];
if (hooks.onNode) hooks.onNode(best, list.length, list.length, true);
this.deliver(mirrorUrl(githubUrl, best.url), filename);
return { ok: true, nodeUrl: best.url, blind: true, trace };
}
};
/* ======================================================================
* L5-b · CAPABILITY —— Injector:规则表驱动,无硬编码 if/else 分支
* ==================================================================== */
const Injector = {
timer: null,
activeScenarios() {
const cfg = Settings.get().inject;
const host = location.hostname;
return SCENARIOS.filter((s) => {
if (!cfg[s.key]) return false;
if (s.hosts && !s.hosts.includes(host)) return false;
return true;
});
},
build(githubUrl, filename) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'ghb-dl-btn';
btn.title = '镜像加速下载';
btn.dataset.ghbUrl = githubUrl;
btn.setAttribute('aria-label', '镜像加速下载');
btn.innerHTML = Icons.download + '镜像下载';
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
View.download(githubUrl, filename || Utils.filenameFromUrl(githubUrl));
});
return btn;
},
attach(container, link, scenario) {
if (!container || !link || !link.href) return;
if (container.querySelector(':scope > .ghb-dl-btn')) return;
// selector 已限定 github 域名(含 codeload.github.com),此处不再二次过滤
const href = link.href;
const name = scenario.name(link) || Utils.filenameFromUrl(href);
container.appendChild(this.build(href, name));
},
run() {
this.activeScenarios().forEach((s) => {
let links;
try {
links = document.querySelectorAll(s.selector);
} catch {
return;
}
links.forEach((link) => {
this.attach(s.container(link), link, s);
});
});
},
schedule(delay) {
clearTimeout(this.timer);
this.timer = setTimeout(() => this.run(), delay || 0);
},
start() {
this.schedule(400);
// GitHub 是 SPA:监听路由变化与相关 DOM 增量
let lastHref = location.href;
const mo = new MutationObserver((records) => {
if (location.href !== lastHref) {
lastHref = location.href;
this.schedule(700);
return;
}
const relevant = records.some((m) => {
for (const node of m.addedNodes) {
if (node.nodeType !== 1) continue;
const html = (node.outerHTML || '').toLowerCase();
if (html.includes('download') || html.includes('release') ||
html.includes('archive') || html.includes('raw') ||
html.includes('codeload')) return true;
}
return false;
});
if (relevant) this.schedule(INJECT_DEBOUNCE);
});
mo.observe(document.body, { childList: true, subtree: true });
setInterval(() => this.schedule(), INJECT_INTERVAL);
}
};
/* ======================================================================
* L5-c · CAPABILITY —— Enhancer:Release 增强显示
* Release 增强显示核心能力(DOM 编排层),四大能力均可独立开关、持久化于 Settings.enhance:
* groupSort 文件按平台分组、按当前系统/架构排序、推荐最可能安装包
* downloadCount 通过 GitHub API 显示各文件下载量
* replaceTime 相对时间 → 精确时间
* collapsibleNotes 更新日志可折叠
* 主动排除与原脚本冲突/冗余的部分:代理下拉(依赖 XIU2 脚本)、回到顶部按钮。
* 纯计算在 L2-b(Arch.*),本层只做 DOM 编排与数据获取。
* ==================================================================== */
const Enhancer = {
_os: null, // 用户手动覆盖的 OS
_arch: null, // 用户手动覆盖的架构
_boxes: [], // 已登记处理的 release details
_scanTimer: null,
active(key) { return !!Settings.get().enhance[key]; },
/* ---- 页面级扫描:releases 页做分组/下载量;全局做时间/日志 ---- */
scan() {
if (this.active('replaceTime')) this.replaceTimes();
if (this.active('collapsibleNotes')) this.processNotes();
if (!/^\/[^\/]+\/[^\/]+\/releases/.test(location.pathname)) return;
const repo = Arch.getRepoInfo();
if (!repo) return;
if (!(this.active('groupSort') || this.active('downloadCount'))) return;
document.querySelectorAll('details').forEach((details) => {
const summary = details.querySelector('summary');
if (summary && /Assets/i.test(summary.textContent)) {
const tag = this.findTagName(details);
if (tag) this.processBox(details, repo, tag);
}
});
},
init() {
this.scan();
let lastHref = location.href;
const mo = new MutationObserver((records) => {
if (location.href !== lastHref) { lastHref = location.href; this.scan(); return; }
const relevant = records.some((m) => {
for (const n of m.addedNodes) {
if (n.nodeType !== 1) continue;
const h = (n.outerHTML || '').toLowerCase();
if (h.includes('releases/download') || h.includes('relative-time') ||
h.includes('markdown-body') || h.includes('assets') || h.includes('release')) return true;
}
return false;
});
if (relevant) {
clearTimeout(this._scanTimer);
this._scanTimer = setTimeout(() => this.scan(), 350);
}
});
mo.observe(document.body, { childList: true, subtree: true });
document.addEventListener('turbo:load', () => this.scan());
document.addEventListener('pjax:end', () => this.scan());
},
/* ---- 相对时间替换(兼容 Shadow DOM 与实际渲染层) ---- */
replaceTimes() {
document.querySelectorAll('relative-time:not([data-gh-replaced])').forEach((el) => {
const dt = el.getAttribute('datetime');
if (!dt) return;
const d = new Date(dt);
if (isNaN(d.getTime())) return;
const pad = (n) => String(n).padStart(2, '0');
const s = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' +
pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
el.setAttribute('data-gh-replaced', '1');
if (el.shadowRoot) el.shadowRoot.textContent = s;
el.textContent = s;
el.style.cssText = 'font-variant-numeric: tabular-nums;';
});
},
/* ---- 更新日志手动折叠(默认展开) ---- */
processNotes() {
document.querySelectorAll('.markdown-body.tmp-my-3:not(.gh-notes-processed)').forEach((el) => {
el.classList.add('gh-notes-processed');
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'gh-notes-toggle';
btn.innerHTML = '更新日志';
btn.addEventListener('click', (e) => {
e.preventDefault();
const collapsed = el.classList.toggle('gh-notes-collapsed');
btn.classList.toggle('is-collapsed', collapsed);
});
el.parentNode.insertBefore(btn, el);
});
},
findTagName(details) {
const m = location.pathname.match(/\/releases\/tag\/([^/?]+)/);
if (m) return decodeURIComponent(m[1]);
const box = details.closest('section, .Box, .js-details-container, div[data-test-selector="release-card"]');
if (box) {
const link = box.querySelector('a[href*="/releases/tag/"]');
if (link) {
const mm = link.getAttribute('href').match(/\/releases\/tag\/([^/?]+)/);
if (mm) return decodeURIComponent(mm[1]);
}
}
return null;
},
/* ---- 处理单个 release 折叠块(幂等:首次登记监听,之后每次按需重排/重注控件) ---- */
processBox(details, repo, tag) {
const first = details.dataset.ghBox !== '1';
details.dataset.ghBox = '1';
if (first) {
this._boxes.push(details);
details.addEventListener('toggle', () => {
if (details.open && this.active('groupSort')) this.formatAndSort(details);
});
const mo = new MutationObserver(() => {
if (details.open && this.active('groupSort')) this.formatAndSort(details);
if (details._assets) this.injectCounts(details, details._assets);
});
mo.observe(details, { childList: true, subtree: true });
}
this.injectControls(details, repo, tag);
if (details.open && this.active('groupSort')) this.formatAndSort(details);
if (details._assets) this.injectCounts(details, details._assets);
},
/* ---- 注入/移除摘要区的控件(按开关实时生效,可关闭即移除) ---- */
injectControls(details, repo, tag) {
const summary = details.querySelector('summary');
const titleSpan = summary ? (summary.querySelector('.d-inline-flex.flex-items-center') || summary) : null;
if (!titleSpan) return;
const dlBtn = titleSpan.querySelector('.gh-fetch-dl-btn');
// 下载量按钮
if (this.active('downloadCount') && !summary.dataset.ghDlBtn) {
summary.dataset.ghDlBtn = '1';
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'Button Button--secondary Button--small ml-3 gh-fetch-dl-btn';
btn.innerHTML = '显示下载量';
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (details.dataset.ghFetching === '1') return;
const refresh = !!details._assets;
btn.querySelector('.Button-label').textContent = '获取中…';
btn.disabled = true;
this.fetchCounts(repo, tag).then((assets) => {
details._assets = assets;
this.injectCounts(details, assets);
btn.querySelector('.Button-label').textContent = '刷新下载量';
btn.disabled = false;
}).catch(() => {
btn.querySelector('.Button-label').textContent = '获取失败(限流)';
btn.disabled = false;
});
});
titleSpan.appendChild(btn);
if (details._assets) this.injectCounts(details, details._assets);
} else if (!this.active('downloadCount')) {
if (dlBtn) { dlBtn.remove(); summary.dataset.ghDlBtn = ''; }
// 同时清掉已注入的计数
details.querySelectorAll('.gh-dl-count').forEach((el) => el.remove());
}
// OS / 架构 下拉 + 架构说明(与 groupSort 绑定)
if (this.active('groupSort')) {
if (!summary.dataset.ghOsSel) {
summary.dataset.ghOsSel = '1';
const sel = document.createElement('select');
sel.className = 'gh-os-select';
const cur = Arch.getCurrentOS();
Arch.OS_OPTIONS.forEach((o) => {
const opt = document.createElement('option');
opt.value = o.value; opt.textContent = o.label;
if (o.value === (this._os || cur)) opt.selected = true;
sel.appendChild(opt);
});
sel.addEventListener('click', (e) => e.stopPropagation());
sel.addEventListener('mousedown', (e) => e.stopPropagation());
sel.addEventListener('change', () => {
this._os = sel.value;
document.querySelectorAll('.gh-os-select').forEach((s) => { s.value = this._os; });
this._boxes.forEach((d) => { if (this.active('groupSort')) this.formatAndSort(d); });
});
titleSpan.appendChild(sel);
}
if (!summary.dataset.ghArchSel) {
summary.dataset.ghArchSel = '1';
const sel = document.createElement('select');
sel.className = 'gh-arch-select';
const cur = Arch.getCurrentArch();
Arch.ARCH_OPTIONS.forEach((o) => {
const opt = document.createElement('option');
opt.value = o.value; opt.textContent = o.label;
if (o.value === (this._arch || cur)) opt.selected = true;
sel.appendChild(opt);
});
sel.addEventListener('click', (e) => e.stopPropagation());
sel.addEventListener('mousedown', (e) => e.stopPropagation());
sel.addEventListener('change', () => {
this._arch = sel.value;
document.querySelectorAll('.gh-arch-select').forEach((s) => { s.value = this._arch; });
this._boxes.forEach((d) => { if (this.active('groupSort')) this.formatAndSort(d); });
});
titleSpan.appendChild(sel);
const help = document.createElement('button');
help.type = 'button';
help.className = 'gh-arch-help-btn';
help.textContent = '?';
help.title = '架构说明';
help.addEventListener('mousedown', (e) => e.stopPropagation());
help.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); this.showArchHelp(); });
titleSpan.appendChild(help);
}
} else {
const os = titleSpan.querySelector('.gh-os-select');
if (os) { os.remove(); summary.dataset.ghOsSel = ''; }
const arch = titleSpan.querySelector('.gh-arch-select');
if (arch) { arch.remove(); summary.dataset.ghArchSel = ''; }
const help = titleSpan.querySelector('.gh-arch-help-btn');
if (help) help.remove();
}
},
/* ---- 分组排序核心 ---- */
formatAndSort(details) {
const rows = Array.from(details.querySelectorAll('li')).filter((r) =>
r.querySelector('a[href*="/releases/download/"], a[href*="/archive/"], a[href*="/attestations/"]'));
if (!rows.length) return;
const parent = rows[0].parentNode;
const os = this._os || Arch.getCurrentOS();
const arch = this._arch || Arch.getCurrentArch();
// 幂等签名:行数 或 用户所选 OS/架构 任一变化才重排,避免 MutationObserver 死循环,
// 同时保证切换 OS/架构下拉时文件顺序会真正重新排序(仅比较行数会导致切换失效)。
const sig = os + '|' + arch + '|' + rows.length;
if (details.dataset.ghSortSig === sig) return;
details.dataset.ghSortSig = sig;
rows.forEach((row) => {
const link = row.querySelector('a[href*="/releases/download/"], a[href*="/archive/"], a[href*="/attestations/"]');
let group = { id: 'other', showTag: false };
let score = -10000;
if (link) {
const href = link.getAttribute('href') || '';
const fn = Arch.getFileNameFromLink(link);
group = href.includes('/archive/') ? { id: 'source', showTag: false }
: href.includes('/attestations/') ? { id: 'meta', showTag: false }
: Arch.parseFileGroup(fn);
score = Arch.calculateMatchScore(fn, os, group.id, arch);
}
row.dataset.matchScore = String(score);
row._group = group;
row._link = link;
});
rows.forEach((r) => r.remove());
parent.querySelectorAll('.gh-meta-files-wrapper, .gh-group-aux-wrapper').forEach((w) => w.remove());
rows.sort((a, b) => parseInt(b.dataset.matchScore) - parseInt(a.dataset.matchScore));
const normal = rows.filter((r) => r._group.id !== 'meta');
const meta = rows.filter((r) => r._group.id === 'meta');
const styleRow = (row) => {
row.style.borderTop = '';
row.style.borderLeft = '';
row.style.backgroundColor = '';
row.className = row.className.replace(/gh-group-\S+/g, '');
row.classList.add(Arch.getGroupClass(row._group.id));
let mc = row.querySelector('.gh-meta-container');
if (!mc) {
mc = document.createElement('div');
mc.className = 'gh-meta-container d-flex flex-items-center flex-shrink-0 mr-3';
const right = row.querySelector('.col-md-6') || row.querySelector('.flex-auto.flex-justify-end');
const sha = right ? right.querySelector('.flex-1') : null;
if (sha) sha.insertBefore(mc, sha.firstChild);
else if (right) right.insertBefore(mc, right.firstChild);
else { const left = row.querySelector('.col-lg-6'); if (left) left.appendChild(mc); else row.appendChild(mc); }
}
row.querySelectorAll('.gh-platform-tag').forEach((t) => t.remove());
if (row._group.showTag) {
const tag = document.createElement('span');
tag.className = 'Label gh-platform-tag ' + Arch.getTagClass(row._group.id) + ' mr-2';
tag.textContent = row._group.name;
mc.appendChild(tag);
}
};
let gid = null, seg = [];
const flush = () => {
if (!seg.length) return;
const g = seg[0]._group.id;
const main = [], aux = [];
seg.forEach((r) => {
const n = r._link ? Arch.getFileNameFromLink(r._link) : '';
(Arch.isAuxiliaryFile(n) ? aux : main).push(r);
});
main.forEach((r) => { parent.appendChild(r); styleRow(r); });
if (aux.length) {
const w = document.createElement('details');
w.className = 'gh-group-aux-wrapper ' + Arch.getGroupClass(g);
const s = document.createElement('summary');
s.textContent = '校验/增量文件 (' + aux.length + ')';
w.appendChild(s);
aux.forEach((r) => { w.appendChild(r); styleRow(r); });
parent.appendChild(w);
}
seg = [];
};
normal.forEach((r) => { if (r._group.id !== gid) flush(); gid = r._group.id; seg.push(r); });
flush();
if (meta.length) {
const w = document.createElement('details');
w.className = 'gh-meta-files-wrapper';
const s = document.createElement('summary');
s.textContent = '签名 / 校验文件 (' + meta.length + ')';
w.appendChild(s);
meta.forEach((r) => { w.appendChild(r); styleRow(r); });
parent.appendChild(w);
}
const showAll = Array.from(parent.children).find((c) =>
!c.querySelector('a[href*="/releases/download/"], a[href*="/archive/"]') &&
!c.classList.contains('gh-meta-files-wrapper') &&
!c.classList.contains('gh-group-aux-wrapper') &&
/show all/i.test(c.textContent));
if (showAll) parent.appendChild(showAll);
},
/* ---- 下载量:GitHub API ---- */
async fetchCounts(repo, tag) {
const res = await gmRequest({
url: 'https://api.github.com/repos/' + repo.owner + '/' + repo.repo + '/releases/tags/' + encodeURIComponent(tag),
headers: { 'Accept': 'application/vnd.github+json', 'User-Agent': 'GitHub-Boost-Helper/1.0.0' },
timeout: 12000
});
const data = typeof res.response === 'string' ? JSON.parse(res.response) : res.response;
return data.assets || [];
},
injectCounts(details, assets) {
if (!assets) return;
const rows = Array.from(details.querySelectorAll('li')).filter((r) =>
r.querySelector('a[href*="/releases/download/"], a[href*="/archive/"]'));
rows.forEach((row) => {
const link = row.querySelector('a[href*="/releases/download/"], a[href*="/archive/"]');
if (!link) return;
const fn = Arch.getFileNameFromLink(link);
const asset = assets.find((a) => a.name === fn);
if (asset && !row.querySelector('.gh-dl-count')) {
const span = document.createElement('span');
span.className = 'color-fg-muted flex-shrink-0 d-flex flex-items-center mr-2 gh-dl-count';
span.style.whiteSpace = 'nowrap';
span.innerHTML =
'' + Arch.formatCount(asset.download_count) + '';
let mc = row.querySelector('.gh-meta-container');
if (!mc) {
mc = document.createElement('div');
mc.className = 'gh-meta-container d-flex flex-items-center flex-shrink-0 mr-3';
const right = row.querySelector('.col-md-6') || row.querySelector('.flex-auto.flex-justify-end');
const sha = right ? right.querySelector('.flex-1') : null;
if (sha) sha.insertBefore(mc, sha.firstChild);
else if (right) right.insertBefore(mc, right.firstChild);
else row.appendChild(mc);
}
mc.appendChild(span);
}
});
},
/* ---- 架构说明弹窗(全局复用) ---- */
_archModal: null,
showArchHelp() {
if (!this._archModal) {
const overlay = document.createElement('div');
overlay.className = 'gh-arch-help-overlay';
overlay.innerHTML =
'' +
'
' +
'
设备架构说明
' +
'
架构指 CPU 指令集架构(ISA),决定软件能否运行,不同架构一般不兼容。
' +
'
' +
' | 架构 | 别名 | 常见设备 | 说明 |
' +
' ' +
' | x86 | i386、i686、32 位 | 老电脑、旧工控机 | Intel/AMD 32 位,已逐渐淘汰 |
' +
' | x86_64 | amd64、x64 | 大多数 Windows/Linux PC | 桌面与服务器最常见 |
' +
' | ARM32 | armv7、armeabi-v7a | 老安卓手机、树莓派早期 | 32 位 ARM |
' +
' | ARM64 | aarch64、arm64 | 新安卓手机、Apple Silicon、树莓派 3/4/5 | 移动设备主流 |
' +
' ' +
'
' +
'
';
overlay.querySelector('.gh-arch-help-close').addEventListener('click', () => { overlay.style.display = 'none'; });
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.style.display = 'none'; });
overlay.style.display = 'none';
document.body.appendChild(overlay);
this._archModal = overlay;
}
this._archModal.style.display = 'flex';
}
};
/* ======================================================================
* L5-d · CAPABILITY —— Tools:仓库页工具注入(当前:DeepWiki 跳转)
* scan() 按开关与页面类型决定「注入 / 更新 / 移除」按钮,幂等可重入;
* inject() 锚点降级:旧版 ul.pagehead-actions → 新版仓库头 → 留待重扫;
* init() 复用 turbo/pjax/MutationObserver 的 SPA 重扫模式。
* ==================================================================== */
const Tools = {
id: 'gh-deepwiki-li',
_scanTimer: null,
enabled() {
const t = Settings.get().tools;
return !!(t && t.deepwiki);
},
scan() {
const el = document.getElementById(this.id);
const repo = this.enabled() ? Route.parseRepo(location.pathname) : null;
if (!repo) {
if (el) el.remove();
return false;
}
const url = Route.deepWikiUrl(repo);
if (el) {
// 幂等:已在页面则只校正 href(SPA 路由切换到另一仓库时跟随更新)
const a = el.querySelector('a');
if (a && a.getAttribute('href') !== url) a.setAttribute('href', url);
return true;
}
return this.inject(url);
},
inject(url) {
const li = document.createElement('li');
li.id = this.id;
// 关键属性走 DOM API(可测、不受 innerHTML 解析影响),图标内容走 innerHTML
const a = document.createElement('a');
a.setAttribute('href', url);
a.setAttribute('target', '_blank');
a.setAttribute('rel', 'noopener noreferrer');
a.setAttribute('title', '在 DeepWiki 打开该仓库(AI 生成的 Wiki 文档,新标签页)');
a.style.cssText =
'display:inline-flex;align-items:center;gap:6px;' +
'background:var(--button-default-bgColor-rest,var(--color-btn-bg));' +
'color:var(--button-default-fgColor-rest,var(--color-btn-text));' +
'border:1px solid var(--button-default-borderColor-rest,var(--color-btn-border));' +
'border-radius:6px;padding:3px 12px;font-size:12px;font-weight:500;' +
'line-height:20px;text-decoration:none;cursor:pointer;' +
'transition:background-color 80ms cubic-bezier(0.65,0,0.35,1);';
a.innerHTML =
'' +
'DeepWiki';
// 悬停态用 JS 挂(GitHub CSP 不保证允许内联 onmouseover 属性)
a.addEventListener('mouseenter', () => {
a.style.backgroundColor = 'var(--button-default-bgColor-hover,var(--color-btn-hover-bg))';
});
a.addEventListener('mouseleave', () => { a.style.backgroundColor = ''; });
li.appendChild(a);
// 锚点降级:旧版仓库头操作栏 → 新版仓库头容器;都找不到则返回 false 留待重扫
const oldBar = document.querySelector('ul.pagehead-actions');
if (oldBar) { oldBar.prepend(li); return true; }
const newHead = document.querySelector('#repository-container-header');
if (newHead) {
li.style.listStyle = 'none';
const star = newHead.querySelector('#repo-stars-counter-star');
const row = star ? star.closest('div, section') : null;
if (row && row !== newHead) {
row.appendChild(li);
} else {
li.style.position = 'relative';
li.style.marginLeft = 'auto';
newHead.appendChild(li);
}
return true;
}
li.remove();
return false;
},
init() {
this.scan();
let lastHref = location.href;
const mo = new MutationObserver(() => {
if (location.href !== lastHref) {
lastHref = location.href;
clearTimeout(this._scanTimer);
this._scanTimer = setTimeout(() => this.scan(), 200);
return;
}
// 同一页面内按钮被 GitHub 重渲染吞掉时补挂(已存在则零开销)
if (!document.getElementById(this.id) && this.enabled() && Route.parseRepo(location.pathname)) {
clearTimeout(this._scanTimer);
this._scanTimer = setTimeout(() => this.scan(), 500);
}
});
mo.observe(document.body, { childList: true, subtree: true });
document.addEventListener('turbo:load', () => this.scan());
document.addEventListener('pjax:end', () => this.scan());
}
};
/* ======================================================================
* L6 · VIEW —— 样式表 / 启动器 / 面板 / 下载弹窗 / Toast
* ==================================================================== */
const CSS = `
/* 主题色挂在 html 上:注入到页面里的按钮与 toast 不在 .ghb-scope 内,
同样需要读到这些变量;明暗切换直接跟随 GitHub 的 data-color-mode */
html{
--ghb-accent:#2da44e; --ghb-accent-2:#1a7f37; --ghb-accent-fg:#ffffff;
--ghb-good:#2da44e; --ghb-warn:#d29922; --ghb-bad:#f85149;
}
html[data-color-mode="light"]{
--ghb-accent:#1a7f37; --ghb-accent-2:#116329;
}
.ghb-scope{
--ghb-bg:#0d1117; --ghb-bg-2:#161b22; --ghb-bg-3:#21262d;
--ghb-bd:#30363d; --ghb-bd-2:#21262d;
--ghb-fg:#e6edf3; --ghb-fg-2:#8b949e; --ghb-fg-3:#6e7681;
--ghb-shadow:0 16px 44px rgba(0,0,0,.5);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
color:var(--ghb-fg); font-size:13px; line-height:1.5;
}
.ghb-scope.ghb-light{
--ghb-bg:#ffffff; --ghb-bg-2:#f6f8fa; --ghb-bg-3:#eaeef2;
--ghb-bd:#d0d7de; --ghb-bd-2:#d8dee4;
--ghb-fg:#1f2328; --ghb-fg-2:#59636e; --ghb-fg-3:#818b98;
--ghb-shadow:0 16px 44px rgba(31,35,40,.16);
}
.ghb-scope svg{width:1em;height:1em;fill:currentColor;flex:none;vertical-align:-.125em;}
/* ---------- 启动器:右侧中部圆形按钮,可拖拽,默认贴右 ---------- */
#ghb-launcher{
position:fixed; right:0; top:50%; transform:translateY(-50%);
margin-right:10px; z-index:2147483000;
width:44px; height:44px; padding:0;
display:flex; align-items:center; justify-content:center;
border:none; border-radius:50%;
background:var(--ghb-accent); color:#fff; cursor:pointer;
box-shadow:0 6px 20px rgba(0,0,0,.32);
transition:background .18s, box-shadow .18s, transform .18s, filter .18s;
font-family:inherit; line-height:1;
}
#ghb-launcher:hover{background:var(--ghb-accent-2); box-shadow:0 8px 26px rgba(0,0,0,.4); transform:translateY(-50%) scale(1.06);}
#ghb-launcher.ghb-dragging{cursor:grabbing; filter:brightness(1.08); user-select:none; transform:translateY(-50%) scale(.96);}
#ghb-launcher .ghb-lau-mark{width:26px;height:26px;display:block;}
#ghb-launcher .ghb-lau-mark svg{width:26px;height:26px;}
/* ---------- 遮罩 ---------- */
#ghb-overlay{
position:fixed; inset:0; z-index:2147483001;
background:rgba(0,0,0,.5); opacity:0; pointer-events:none; transition:opacity .2s;
}
#ghb-overlay.ghb-open{opacity:1; pointer-events:auto;}
/* ---------- 面板:居中,三 Tab ---------- */
#ghb-panel{
position:fixed; left:50%; top:50%; z-index:2147483002;
width:460px; max-width:calc(100vw - 32px); max-height:84vh;
background:var(--ghb-bg); border:1px solid var(--ghb-bd);
border-radius:14px; box-shadow:var(--ghb-shadow);
display:flex; flex-direction:column; overflow:hidden;
opacity:0; transform:translate(-50%,-46%) scale(.97); pointer-events:none;
transition:opacity .22s, transform .22s cubic-bezier(.4,0,.2,1);
}
#ghb-panel.ghb-open{opacity:1; transform:translate(-50%,-50%) scale(1); pointer-events:auto;}
.ghb-head{display:flex; align-items:center; gap:10px; padding:14px 16px; border-bottom:1px solid var(--ghb-bd-2); flex:none;}
.ghb-head .ghb-mark{width:24px;height:24px;}
.ghb-head .ghb-mark svg{width:24px;height:24px;}
.ghb-head h2{margin:0; font-size:15px; font-weight:600; color:var(--ghb-fg);}
.ghb-head .ghb-ver{font-size:11px; color:var(--ghb-fg-2); border:1px solid var(--ghb-bd); border-radius:999px; padding:1px 7px;}
.ghb-head .ghb-spacer{flex:1;}
.ghb-icon-btn{
width:28px; height:28px; display:flex; align-items:center; justify-content:center;
border:none; border-radius:6px; background:transparent; color:var(--ghb-fg-2);
cursor:pointer; font-size:16px; transition:background .15s, color .15s;
}
.ghb-icon-btn:hover{background:var(--ghb-bg-3); color:var(--ghb-fg);}
.ghb-tabs{display:flex; border-bottom:1px solid var(--ghb-bd-2); flex:none; background:var(--ghb-bg-2);}
.ghb-tab{
flex:1; padding:10px 0; border:none; background:transparent; cursor:pointer;
font-family:inherit; font-size:13px; color:var(--ghb-fg-2);
border-bottom:2px solid transparent; transition:color .15s, background .15s;
}
.ghb-tab:hover{color:var(--ghb-fg); background:var(--ghb-bg-3);}
.ghb-tab.ghb-on{color:var(--ghb-fg); font-weight:600; border-bottom-color:var(--ghb-accent);}
.ghb-body{flex:1; overflow-y:auto; min-height:180px;}
.ghb-body::-webkit-scrollbar{width:8px;}
.ghb-body::-webkit-scrollbar-thumb{background:var(--ghb-bd); border-radius:4px;}
.ghb-page{display:none;}
.ghb-page.ghb-on{display:block;}
.ghb-status{
display:flex; align-items:center; gap:8px; padding:10px 16px;
font-size:12px; color:var(--ghb-fg-2); border-bottom:1px solid var(--ghb-bd-2);
background:var(--ghb-bg-2);
}
.ghb-dot{width:8px;height:8px;border-radius:50%;background:var(--ghb-fg-3);flex:none;}
.ghb-dot.ghb-online{background:var(--ghb-good);}
.ghb-dot.ghb-offline{background:var(--ghb-bad);}
.ghb-status .ghb-tail{margin-left:auto; color:var(--ghb-fg-3);}
.ghb-toolbar{display:flex; flex-wrap:wrap; gap:6px; padding:10px 16px; border-bottom:1px solid var(--ghb-bd-2);}
.ghb-btn{
display:inline-flex; align-items:center; gap:5px; padding:5px 10px;
border:1px solid var(--ghb-bd); border-radius:6px; background:var(--ghb-bg-3);
color:var(--ghb-fg); font-family:inherit; font-size:12px; cursor:pointer;
white-space:nowrap; transition:background .15s, border-color .15s, opacity .15s;
}
.ghb-btn:hover{background:var(--ghb-bd); border-color:var(--ghb-fg-3);}
.ghb-btn[disabled]{opacity:.5; pointer-events:none;}
.ghb-btn.ghb-primary{background:var(--ghb-accent); border-color:var(--ghb-accent); color:var(--ghb-accent-fg);}
.ghb-btn.ghb-primary:hover{background:var(--ghb-accent-2);}
.ghb-btn.ghb-danger:hover{background:var(--ghb-bad); border-color:var(--ghb-bad); color:#fff;}
.ghb-btn svg{font-size:14px;}
.ghb-spin{animation:ghb-spin 1s linear infinite;}
@keyframes ghb-spin{to{transform:rotate(360deg);}}
.ghb-field{display:flex; align-items:center; gap:8px; padding:6px 16px 10px;}
.ghb-input{
flex:1; min-width:0; padding:6px 9px; border:1px solid var(--ghb-bd);
border-radius:6px; background:var(--ghb-bg-2); color:var(--ghb-fg);
font-family:inherit; font-size:13px;
}
.ghb-input:focus{outline:none; border-color:var(--ghb-accent);}
.ghb-list{padding:4px 0;}
.ghb-row{display:flex; align-items:center; gap:10px; padding:8px 16px; transition:background .12s;}
.ghb-row:hover{background:var(--ghb-bg-2);}
.ghb-cb{position:relative; width:16px; height:16px; flex:none; cursor:pointer;}
.ghb-cb input{position:absolute; inset:0; width:100%; height:100%; margin:0; opacity:0; cursor:pointer; z-index:1;}
.ghb-cb span{
display:block; width:16px; height:16px; border:1.5px solid var(--ghb-bd);
border-radius:4px; background:var(--ghb-bg); transition:background .15s, border-color .15s;
}
.ghb-cb input:checked + span{background:var(--ghb-accent); border-color:var(--ghb-accent);}
.ghb-cb input:checked + span::after{
content:''; display:block; width:4px; height:8px; margin:1px 0 0 4.5px;
border:solid #fff; border-width:0 2px 2px 0; transform:rotate(45deg);
}
.ghb-main{flex:1; min-width:0;}
.ghb-name{font-size:13px; color:var(--ghb-fg); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.ghb-meta{display:flex; align-items:center; gap:6px; margin-top:3px; font-size:11px; color:var(--ghb-fg-2);}
.ghb-bar{height:3px; width:64px; border-radius:2px; background:var(--ghb-bg-3); overflow:hidden; flex:none;}
.ghb-bar i{display:block; height:100%; border-radius:2px;}
.ghb-tag{font-size:11px; padding:1px 6px; border-radius:999px; border:1px solid var(--ghb-bd); color:var(--ghb-fg-2);}
/* 文字色与进度条填充色分开,避免 .ghb-tag 被染成同色背景 */
.ghb-t-fast{color:var(--ghb-good);}
.ghb-t-mid{color:var(--ghb-warn);}
.ghb-t-slow{color:var(--ghb-bad);}
.ghb-f-fast{background:var(--ghb-good);}
.ghb-f-mid{background:var(--ghb-warn);}
.ghb-f-slow{background:var(--ghb-bad);}
.ghb-empty{padding:32px 16px; text-align:center; color:var(--ghb-fg-2); font-size:13px;}
.ghb-empty small{display:block; margin-top:6px; color:var(--ghb-fg-3);}
.ghb-hint{padding:10px 16px; font-size:12px; color:var(--ghb-fg-2); border-bottom:1px solid var(--ghb-bd-2); background:var(--ghb-bg-2);}
.ghb-setting{display:flex; align-items:center; gap:12px; padding:12px 16px; border-bottom:1px solid var(--ghb-bd-2);}
.ghb-setting:last-child{border-bottom:none;}
.ghb-setting .ghb-label{flex:1; min-width:0;}
.ghb-setting .ghb-lt{display:block; font-size:13px; font-weight:500; color:var(--ghb-fg);}
.ghb-setting .ghb-ld{display:block; font-size:11px; color:var(--ghb-fg-2); margin-top:2px;}
.ghb-switch{position:relative; width:40px; height:22px; flex:none; cursor:pointer;}
.ghb-switch input{position:absolute; inset:0; width:100%; height:100%; margin:0; opacity:0; cursor:pointer; z-index:1;}
.ghb-switch i{
display:block; width:40px; height:22px; border-radius:11px;
background:var(--ghb-bd); transition:background .22s; position:relative;
}
.ghb-switch i::after{
content:''; position:absolute; top:3px; left:3px; width:16px; height:16px;
border-radius:50%; background:#fff; transition:transform .22s cubic-bezier(.4,0,.2,1);
}
.ghb-switch input:checked + i{background:var(--ghb-accent);}
.ghb-switch input:checked + i::after{transform:translateX(18px);}
.ghb-select, .ghb-num{
padding:5px 8px; border:1px solid var(--ghb-bd); border-radius:6px;
background:var(--ghb-bg-2); color:var(--ghb-fg); font-family:inherit; font-size:12px;
}
.ghb-num{width:72px; text-align:right;}
.ghb-select:focus, .ghb-num:focus{outline:none; border-color:var(--ghb-accent);}
.ghb-inline{display:flex; align-items:center; gap:6px; flex:none;}
.ghb-about{padding:14px 16px; font-size:12px; color:var(--ghb-fg-2); border-top:1px solid var(--ghb-bd-2);}
.ghb-about b{color:var(--ghb-fg); font-weight:600;}
.ghb-foot{
display:flex; align-items:center; justify-content:space-between; gap:8px;
padding:8px 16px; border-top:1px solid var(--ghb-bd-2);
background:var(--ghb-bg-2); font-size:11px; color:var(--ghb-fg-3); flex:none;
}
/* ---------- 下载弹窗 ---------- */
#ghb-dl{
position:fixed; inset:0; z-index:2147483003; display:flex;
align-items:center; justify-content:center;
opacity:0; pointer-events:none; transition:opacity .2s;
}
#ghb-dl.ghb-open{opacity:1; pointer-events:auto;}
#ghb-dl .ghb-dl-bg{position:absolute; inset:0; background:rgba(0,0,0,.6);}
#ghb-dl .ghb-card{
position:relative; width:440px; max-width:calc(100vw - 32px); max-height:80vh;
background:var(--ghb-bg); border:1px solid var(--ghb-bd); border-radius:14px;
box-shadow:var(--ghb-shadow); display:flex; flex-direction:column; overflow:hidden;
transform:translateY(10px) scale(.98); transition:transform .22s cubic-bezier(.4,0,.2,1);
}
#ghb-dl.ghb-open .ghb-card{transform:none;}
.ghb-file{padding:10px 16px; background:var(--ghb-bg-2); border-bottom:1px solid var(--ghb-bd-2); flex:none;}
.ghb-file .ghb-f1{font-size:11px; color:var(--ghb-fg-2);}
.ghb-file .ghb-f2{font-size:13px; font-weight:600; color:var(--ghb-fg); word-break:break-all; margin-top:2px;}
.ghb-file .ghb-f3{display:flex; align-items:center; gap:8px; margin-top:8px; flex-wrap:wrap;}
.ghb-dl-sub{display:flex; justify-content:flex-end; padding:8px 16px 0; font-size:12px; color:var(--ghb-fg-2); flex:none;}
.ghb-nodes{flex:1; overflow-y:auto; padding:4px 0; min-height:120px;}
.ghb-nodes::-webkit-scrollbar{width:8px;}
.ghb-nodes::-webkit-scrollbar-thumb{background:var(--ghb-bd); border-radius:4px;}
.ghb-nrow{display:flex; align-items:center; gap:8px; padding:8px 16px;}
.ghb-nrow:hover{background:var(--ghb-bg-2);}
.ghb-nrow .ghb-nd{flex:1; min-width:0; font-size:13px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
/* ---------- 页面注入的下载按钮 ---------- */
.ghb-dl-btn{
display:inline-flex; align-items:center; gap:4px; margin-left:6px;
padding:2px 8px; border:1px solid var(--ghb-accent); border-radius:5px;
background:transparent; color:var(--ghb-accent); font-family:inherit;
font-size:12px; font-weight:500; line-height:1.5; cursor:pointer;
vertical-align:middle; white-space:nowrap; flex:none; transition:background .15s, color .15s;
}
.ghb-dl-btn:hover{background:var(--ghb-accent); color:var(--ghb-accent-fg);}
.ghb-dl-btn svg{width:12px; height:12px; fill:currentColor;}
/* ---------- Toast ---------- */
#ghb-toasts{position:fixed; bottom:24px; left:50%; transform:translateX(-50%); z-index:2147483004; display:flex; flex-direction:column; gap:8px; align-items:center; pointer-events:none;}
.ghb-toast{
display:flex; align-items:center; gap:8px; max-width:min(560px, calc(100vw - 32px));
padding:9px 14px; border-radius:8px; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
font-size:13px; color:#fff; box-shadow:0 6px 20px rgba(0,0,0,.35);
opacity:0; transform:translateY(12px); transition:opacity .22s, transform .22s;
}
.ghb-toast.ghb-show{opacity:1; transform:none;}
.ghb-toast.ghb-info{background:#1f6feb;}
.ghb-toast.ghb-ok{background:var(--ghb-good);}
.ghb-toast.ghb-warn{background:#9e6a03;}
/* ---------- 增强显示:Release 分组 / 平台标签 / OS·架构下拉 / 折叠 / 架构说明 ---------- */
.gh-group-win { border-left: 4px solid var(--color-accent-emphasis, #1f6feb) !important; background-color: var(--color-accent-subtle, rgba(56,139,253,0.1)) !important; }
.gh-group-win:hover { background-color: var(--color-accent-muted, rgba(56,139,253,0.15)) !important; }
.gh-group-win a[href*="/releases/download/"], .gh-group-win a[href*="/archive/"] { color: var(--color-accent-emphasis, #1f6feb) !important; }
.gh-group-mac { border-left: 4px solid var(--color-done-emphasis, #8957e5) !important; background-color: var(--color-done-subtle, rgba(137,87,229,0.1)) !important; }
.gh-group-mac:hover { background-color: var(--color-done-muted, rgba(137,87,229,0.15)) !important; }
.gh-group-mac a[href*="/releases/download/"], .gh-group-mac a[href*="/archive/"] { color: var(--color-done-emphasis, #8957e5) !important; }
.gh-group-mobile { border-left: 4px solid #e3b341 !important; background-color: rgba(227,179,65,0.12) !important; }
.gh-group-mobile:hover { background-color: rgba(227,179,65,0.18) !important; }
.gh-group-mobile a[href*="/releases/download/"], .gh-group-mobile a[href*="/archive/"] { color: #e3b341 !important; }
.gh-group-linux-rpm { border-left: 4px solid var(--color-danger-emphasis, #f85149) !important; background-color: var(--color-danger-subtle, rgba(248,81,73,0.1)) !important; }
.gh-group-linux-rpm:hover { background-color: var(--color-danger-muted, rgba(248,81,73,0.15)) !important; }
.gh-group-linux-rpm a[href*="/releases/download/"], .gh-group-linux-rpm a[href*="/archive/"] { color: var(--color-danger-emphasis, #f85149) !important; }
.gh-group-linux-deb { border-left: 4px solid var(--color-severe-emphasis, #db6d28) !important; background-color: var(--color-severe-subtle, rgba(219,109,40,0.1)) !important; }
.gh-group-linux-deb:hover { background-color: var(--color-severe-muted, rgba(219,109,40,0.15)) !important; }
.gh-group-linux-deb a[href*="/releases/download/"], .gh-group-linux-deb a[href*="/archive/"] { color: var(--color-severe-emphasis, #db6d28) !important; }
.gh-group-linux-arch { border-left: 4px solid var(--color-sponsors-emphasis, #bf4b8a) !important; background-color: var(--color-sponsors-subtle, rgba(191,75,138,0.1)) !important; }
.gh-group-linux-arch:hover { background-color: var(--color-sponsors-muted, rgba(191,75,138,0.15)) !important; }
.gh-group-linux-arch a[href*="/releases/download/"], .gh-group-linux-arch a[href*="/archive/"] { color: var(--color-sponsors-emphasis, #bf4b8a) !important; }
.gh-group-linux-appimage { border-left: 4px solid #20c997 !important; background-color: rgba(32,201,151,0.1) !important; }
.gh-group-linux-appimage:hover { background-color: rgba(32,201,151,0.15) !important; }
.gh-group-linux-appimage a[href*="/releases/download/"], .gh-group-linux-appimage a[href*="/archive/"] { color: #20c997 !important; }
.gh-group-linux-flatpak { border-left: 4px solid #0abda0 !important; background-color: rgba(10,189,160,0.1) !important; }
.gh-group-linux-flatpak:hover { background-color: rgba(10,189,160,0.15) !important; }
.gh-group-linux-flatpak a[href*="/releases/download/"], .gh-group-linux-flatpak a[href*="/archive/"] { color: #0abda0 !important; }
.gh-group-linux-other { border-left: 4px solid var(--color-attention-emphasis, #9e6a03) !important; background-color: var(--color-attention-subtle, rgba(210,153,34,0.1)) !important; }
.gh-group-linux-other:hover { background-color: var(--color-attention-muted, rgba(210,153,34,0.15)) !important; }
.gh-group-linux-other a[href*="/releases/download/"], .gh-group-linux-other a[href*="/archive/"] { color: var(--color-attention-emphasis, #9e6a03) !important; }
.gh-group-other { border-left: 4px solid transparent !important; }
.gh-platform-tag { background-color: transparent !important; }
.gh-platform-tag.gh-tag-win { color: var(--color-accent-emphasis, #1f6feb) !important; border-color: var(--color-accent-emphasis, #1f6feb) !important; }
.gh-platform-tag.gh-tag-mac { color: var(--color-done-emphasis, #8957e5) !important; border-color: var(--color-done-emphasis, #8957e5) !important; }
.gh-platform-tag.gh-tag-mobile { color: #e3b341 !important; border-color: #e3b341 !important; }
.gh-platform-tag.gh-tag-linux-rpm { color: var(--color-danger-emphasis, #f85149) !important; border-color: var(--color-danger-emphasis, #f85149) !important; }
.gh-platform-tag.gh-tag-linux-deb { color: var(--color-severe-emphasis, #db6d28) !important; border-color: var(--color-severe-emphasis, #db6d28) !important; }
.gh-platform-tag.gh-tag-linux-arch { color: var(--color-sponsors-emphasis, #bf4b8a) !important; border-color: var(--color-sponsors-emphasis, #bf4b8a) !important; }
.gh-platform-tag.gh-tag-linux-appimage { color: #20c997 !important; border-color: #20c997 !important; }
.gh-platform-tag.gh-tag-linux-flatpak { color: #0abda0 !important; border-color: #0abda0 !important; }
.gh-platform-tag.gh-tag-linux-other { color: var(--color-attention-emphasis, #9e6a03) !important; border-color: var(--color-attention-emphasis, #9e6a03) !important; }
.gh-os-select, .gh-arch-select {
appearance: auto; cursor: pointer; font-size: 12px; font-weight: 500; line-height: 20px;
padding: 3px 8px; margin-left: 8px;
background-color: var(--button-default-bgColor-rest, var(--color-btn-bg, #21262d));
border: 1px solid var(--button-default-borderColor-rest, var(--color-btn-border, rgba(240,246,252,0.1)));
border-radius: 6px; color: var(--button-default-fgColor-rest, var(--color-btn-text, #c9d1d9));
}
.gh-os-select:hover, .gh-arch-select:hover {
background-color: var(--button-default-bgColor-hover, var(--color-btn-hover-bg, #30363d));
border-color: var(--button-default-borderColor-hover, var(--color-btn-hover-border, rgba(240,246,252,0.3)));
}
.gh-meta-files-wrapper > summary, .gh-group-aux-wrapper > summary {
cursor: pointer; padding: 8px 16px; font-size: 12px; color: var(--fgColor-muted, var(--color-fg-muted, #8b949e));
border-top: 1px solid var(--borderColor-muted, var(--color-border-muted, #30363d));
}
.gh-meta-files-wrapper > summary:hover, .gh-group-aux-wrapper > summary:hover { color: var(--fgColor-default, var(--color-fg-default, #e6edf3)); }
.gh-group-aux-wrapper > li { border-left: none !important; background-color: transparent !important; }
.gh-notes-toggle {
appearance: none; background: none; border: none; padding: 0; margin: 0 0 8px;
display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none;
color: var(--fgColor-muted, var(--color-fg-muted, #8b949e)); font-size: 14px; font-weight: 600;
}
.gh-notes-toggle:hover { color: var(--fgColor-default, var(--color-fg-default, #e6edf3)); }
.markdown-body.gh-notes-collapsed { display: none !important; }
.gh-notes-chevron { display: inline-block; width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 6px solid currentColor; transition: transform 0.12s ease; transform-origin: 50% 40%; }
.gh-notes-toggle.is-collapsed .gh-notes-chevron { transform: rotate(-90deg); }
.gh-arch-help-btn {
appearance: none; width: 22px; height: 22px; line-height: 1; padding: 0; margin-left: 4px; flex-shrink: 0;
display: inline-flex; align-items: center; justify-content: center; cursor: pointer; font-size: 12px; font-weight: 600;
background-color: var(--button-default-bgColor-rest, var(--color-btn-bg, #21262d));
border: 1px solid var(--button-default-borderColor-rest, var(--color-btn-border, rgba(240,246,252,0.1)));
border-radius: 6px; color: var(--button-default-fgColor-rest, var(--color-btn-text, #c9d1d9));
}
.gh-arch-help-btn:hover { background-color: var(--button-default-bgColor-hover, var(--color-btn-hover-bg, #30363d)); border-color: var(--button-default-borderColor-hover, var(--color-btn-hover-border, rgba(240,246,252,0.3))); }
.gh-arch-help-overlay { position: fixed; inset: 0; z-index: 2147482999; background-color: rgba(1,4,9,0.6); display: flex; align-items: center; justify-content: center; }
.gh-arch-help-modal {
background-color: var(--color-canvas-default, #0d1117); border: 1px solid var(--color-border-default, #30363d);
border-radius: 8px; padding: 16px 20px; max-width: 92vw; max-height: 85vh; overflow: auto; position: relative;
box-shadow: var(--shadow-floating-large, 0 8px 24px rgba(0,0,0,0.4));
}
.gh-arch-help-modal h3 { margin: 0 0 8px; font-size: 16px; }
.gh-arch-help-tip { margin: 0 0 12px; font-size: 12px; color: var(--color-fg-muted, #8b949e); }
.gh-arch-help-modal table { border-collapse: collapse; width: 100%; font-size: 13px; }
.gh-arch-help-modal th, .gh-arch-help-modal td { border: 1px solid var(--color-border-default, #30363d); padding: 6px 10px; text-align: left; vertical-align: top; }
.gh-arch-help-modal th { background-color: var(--color-canvas-subtle, #161b22); font-weight: 600; }
.gh-arch-help-close {
position: absolute; top: 6px; right: 10px; background: none; border: none; color: var(--color-fg-muted, #8b949e);
cursor: pointer; font-size: 22px; line-height: 1; padding: 0;
}
.gh-arch-help-close:hover { color: var(--color-fg-default, #e6edf3); }
.gh-dl-count { font-size: 12px; }
.ghb-toast.ghb-err{background:#b62324;}
`;
const View = {
el: {},
/* ---------- 主题跟随 GitHub 明暗模式 ---------- */
theme() {
const mode = document.documentElement.getAttribute('data-color-mode');
if (mode === 'light' || mode === 'dark') return mode;
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
},
applyTheme() {
const light = this.theme() === 'light';
[this.el.launcher, this.el.panel, this.el.dl].forEach((n) => {
if (n) n.classList.toggle('ghb-light', light);
});
},
mount() {
GM_addStyle(CSS);
const launcher = document.createElement('button');
launcher.id = 'ghb-launcher';
launcher.className = 'ghb-scope';
launcher.type = 'button';
launcher.title = 'GitHub 加速助手(点击打开面板,按住可拖拽)';
launcher.innerHTML =
'' + Icons.mark + '';
document.body.appendChild(launcher);
const overlay = document.createElement('div');
overlay.id = 'ghb-overlay';
document.body.appendChild(overlay);
const panel = document.createElement('div');
panel.id = 'ghb-panel';
panel.className = 'ghb-scope';
document.body.appendChild(panel);
const dl = document.createElement('div');
dl.id = 'ghb-dl';
dl.className = 'ghb-scope';
dl.innerHTML =
'' +
'' +
'
' +
' ' + Icons.mark + '' +
'
镜像加速下载
' +
' ' +
' ' +
' ' +
' ' +
'
' +
'
下载文件
' +
'
-
' +
'
' +
' ' +
' ' +
'
' +
'
' +
'
' +
'
' +
' ' +
'
';
document.body.appendChild(dl);
Object.assign(this.el, { launcher, overlay, panel, dl });
this.applyTheme();
const mo = new MutationObserver(() => this.applyTheme());
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-color-mode', 'data-dark-theme', 'data-light-theme'] });
this.Panel.mount(panel);
this.DlModal.mount(dl);
this.bindLauncher(launcher, overlay);
},
bindLauncher(launcher, overlay) {
let dragged = false;
launcher.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
const r = launcher.getBoundingClientRect();
const sx = e.clientX, sy = e.clientY, ox = r.left, oy = r.top;
let moved = false;
launcher.classList.add('ghb-dragging');
const onMove = (ev) => {
const dx = ev.clientX - sx, dy = ev.clientY - sy;
if (!moved && Math.abs(dx) < 4 && Math.abs(dy) < 4) return;
moved = true;
launcher.style.right = 'auto';
launcher.style.transform = 'none';
launcher.style.left = Math.max(0, Math.min(window.innerWidth - r.width, ox + dx)) + 'px';
launcher.style.top = Math.max(0, Math.min(window.innerHeight - r.height, oy + dy)) + 'px';
};
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
launcher.classList.remove('ghb-dragging');
if (moved) {
dragged = true;
Settings.set({ launcherPos: { left: launcher.style.left, top: launcher.style.top } });
setTimeout(() => { dragged = false; }, 0);
}
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
e.preventDefault();
});
launcher.addEventListener('click', () => {
if (dragged) return;
this.Panel.toggle();
});
overlay.addEventListener('click', () => this.Panel.toggle());
},
restoreLauncherPos() {
const pos = Settings.get().launcherPos;
const l = this.el.launcher;
if (pos && pos.left && pos.top) {
// 必须把 right 置 auto,否则 CSS 的 right:0 会与 inline left 冲突导致贴右
l.style.transform = 'none';
l.style.right = 'auto';
l.style.left = pos.left;
l.style.top = pos.top;
}
this.applyLauncherVisible();
},
/** 启动器显示状态的唯一写入口:设置项、DOM、面板勾选框三者同步 */
setLauncherVisible(on) {
Settings.set({ showLauncher: !!on });
this.applyLauncherVisible();
},
applyLauncherVisible() {
const on = !!Settings.get().showLauncher;
if (this.el.launcher) this.el.launcher.style.display = on ? 'flex' : 'none';
const cb = document.getElementById('ghb-s-launcher');
if (cb) cb.checked = on;
},
/** 统一下载入口:由 autoDownload 分流——开=全自动最快节点直发(无感);关=弹窗手选(脚本不接管下载) */
download(githubUrl, filename) {
if (!githubUrl) { this.Toast.err('链接无效'); return; }
const name = filename || Utils.filenameFromUrl(githubUrl);
if (Settings.get().autoDownload) this.autoDownload(githubUrl, name);
else this.DlModal.open(githubUrl, name);
},
/**
* 全自动:fresh 候选直接 fire,否则轮换预检。
* 两个体验保障:
* ① 延迟展示 sticky —— fast-path 常在 150ms 内完成,立刻弹 toast 会一闪而过反而碍眼
* ② try/finally 释放 _autoBusy —— 异常时若不释放,自动下载将永久卡死只能刷新页面
*/
async autoDownload(githubUrl, filename) {
if (this._autoBusy) { this.Toast.warn('已有下载任务在进行中'); return; }
this._autoBusy = true;
let sticky = null;
const timer = setTimeout(() => {
sticky = this.Toast.sticky('正在选择最快镜像…');
}, 150);
const done = (kind, msg) => { sticky ? sticky[kind](msg) : this.Toast[kind](msg); };
try {
const r = await Downloader.runAuto(githubUrl, filename, {
onNode: (node, i, n, last) => {
if (sticky) sticky.update('(' + i + '/' + n + ') 预检 ' + Utils.shortDomain(node.url) + (last ? '(兜底直连)' : '') + '…');
}
});
clearTimeout(timer);
if (r.ok && r.blind) done('warn', '镜像预检均失败,已对最快节点直接放行 · ' + filename);
else if (r.ok) done('ok', '已交给浏览器下载 · ' + filename + '(' + Utils.shortDomain(r.nodeUrl) + ')');
else done('err', r.error + '|可点「复制链接」手动下载');
} catch (e) {
clearTimeout(timer);
done('err', '下载失败:' + ((e && e.message) || '未知错误'));
Log.error('自动下载异常', e);
} finally {
clearTimeout(timer);
this._autoBusy = false; // 异常路径也必须释放,否则功能永久卡死
}
},
/* ---------- Toast ---------- */
Toast: {
host() {
let h = document.getElementById('ghb-toasts');
if (!h) {
h = document.createElement('div');
h.id = 'ghb-toasts';
document.body.appendChild(h);
}
return h;
},
show(msg, kind, ms) {
// 懒创建:初始化完成前的提示不会被静默丢弃
const host = this.host();
const t = document.createElement('div');
t.className = 'ghb-toast ghb-' + (kind || 'info');
t.textContent = msg;
host.appendChild(t);
requestAnimationFrame(() => t.classList.add('ghb-show'));
setTimeout(() => {
t.classList.remove('ghb-show');
setTimeout(() => t.remove(), 260);
}, ms || (kind === 'err' ? 4200 : 2400));
},
/** 常驻状态提示:自动下载过程的反馈载体,结束时收敛为普通 toast */
sticky(text) {
const t = document.createElement('div');
t.className = 'ghb-toast ghb-info';
t.textContent = text;
this.host().appendChild(t);
requestAnimationFrame(() => t.classList.add('ghb-show'));
let gone = false;
const close = () => {
if (gone) return;
gone = true;
t.classList.remove('ghb-show');
setTimeout(() => t.remove(), 260);
};
return {
update(msg) { if (!gone) t.textContent = msg || text; },
ok(msg) { close(); View.Toast.ok(msg); },
warn(msg) { close(); View.Toast.warn(msg); },
err(msg) { close(); View.Toast.err(msg); }
};
},
ok: (m) => View.Toast.show(m, 'ok'),
warn: (m) => View.Toast.show(m, 'warn'),
err: (m) => View.Toast.show(m, 'err'),
info: (m) => View.Toast.show(m, 'info')
},
/* ---------- 管理面板 ---------- */
Panel: {
open: false,
tab: 'nodes',
root: null,
mount(root) {
this.root = root;
root.innerHTML =
'' +
' ' + Icons.mark + '' +
'
GitHub 加速助手
' +
' ' + VERSION + '' +
' ' +
' ' +
'' +
'' +
' ' +
' ' +
' ' +
' ' +
' ' +
'
' +
'' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'';
root.querySelector('#ghb-panel-close').addEventListener('click', () => this.toggle());
root.querySelectorAll('.ghb-tab').forEach((btn) => {
btn.addEventListener('click', () => this.switch(btn.dataset.tab));
});
this.renderNodesPage();
this.renderInjectPage();
this.renderEnhancePage();
this.renderToolsPage();
this.renderSettingsPage();
this.switch(Settings.get().lastTab || 'nodes');
},
toggle() {
this.open = !this.open;
View.el.panel.classList.toggle('ghb-open', this.open);
View.el.overlay.classList.toggle('ghb-open', this.open);
if (this.open) this.refresh();
},
switch(tab) {
this.tab = tab;
Settings.set({ lastTab: tab });
this.root.querySelectorAll('.ghb-tab').forEach((b) => b.classList.toggle('ghb-on', b.dataset.tab === tab));
this.root.querySelectorAll('.ghb-page').forEach((p) => p.classList.toggle('ghb-on', p.id === 'ghb-page-' + tab));
},
refresh() {
this.renderNodesPage();
this.renderInjectPage();
this.renderEnhancePage();
this.renderToolsPage();
this.renderSettingsPage();
},
renderNodesPage() {
const page = this.root.querySelector('#ghb-page-nodes');
const nodes = NodeStore.nodes;
const online = nodes.length > 0;
const lats = nodes.map((n) => n.latency || 0);
const summary = online
? nodes.length + ' 个节点 · 最快 ' + Math.min.apply(null, lats) +
'ms · 平均 ' + Math.round(lats.reduce((a, b) => a + b, 0) / lats.length) + 'ms'
: '暂无可用节点';
page.innerHTML =
'' +
' ' +
' ' + Utils.esc(summary) + '' +
' 更新 ' + Utils.clock(NodeStore.updatedAt) + '' +
'
' +
'' +
' ' +
' ' +
' ' +
' ' +
' ' +
'
' +
'' +
'';
page.querySelector('#ghb-n-refresh').addEventListener('click', (e) => this.onRefresh(e.currentTarget));
page.querySelector('#ghb-n-probe').addEventListener('click', (e) => this.onProbe(e.currentTarget));
page.querySelector('#ghb-n-all').addEventListener('click', () => {
NodeStore.setVisible(NodeStore.nodes.map((n) => n.url));
View.Toast.info('已全选 ' + NodeStore.nodes.length + ' 个节点');
});
page.querySelector('#ghb-n-none').addEventListener('click', () => {
NodeStore.setVisible([]);
View.Toast.info('已取消全部勾选');
});
page.querySelector('#ghb-n-top').addEventListener('click', () => {
NodeStore.setVisible(NodeStore.nodes.slice(0, 10).map((n) => n.url));
View.Toast.info('已恢复延迟最低的 10 个节点');
});
const filter = page.querySelector('#ghb-n-filter');
filter.addEventListener('input', () => { this.filter = filter.value; this.renderList(); });
this.renderList();
},
renderList() {
const list = this.root.querySelector('#ghb-n-list');
if (!list) return;
const kw = (this.filter || '').trim().toLowerCase();
const nodes = NodeStore.nodes.filter((n) => !kw || n.url.toLowerCase().includes(kw));
if (!nodes.length) {
list.innerHTML = '没有匹配的节点' +
(NodeStore.nodes.length ? '' : '
正在后台自动获取…') +
'也可点击「刷新节点」手动重试
';
return;
}
list.innerHTML = nodes.map((n) => {
const ms = n.latency || 0;
const lv = Utils.level(ms);
const on = NodeStore.visible.includes(n.url);
return '' +
'
' +
'
' +
'
' + Utils.esc(Utils.shortDomain(n.url)) + '
' +
'
' + ms + 'ms
' +
'
' +
'
' +
'
';
}).join('');
list.querySelectorAll('.ghb-n-cb').forEach((cb) => {
cb.addEventListener('change', () => {
const next = new Set(NodeStore.visible);
cb.checked ? next.add(cb.dataset.url) : next.delete(cb.dataset.url);
NodeStore.setVisible(Array.from(next));
});
});
list.querySelectorAll('.ghb-n-test').forEach((b) => {
b.addEventListener('click', () => this.onTest(b));
});
const count = this.root.querySelector('#ghb-panel-count');
if (count) count.textContent = '已启用 ' + NodeStore.visible.length + ' / ' + NodeStore.nodes.length;
},
async onRefresh(btn) {
btn.disabled = true;
btn.querySelector('svg').classList.add('ghb-spin');
const ok = await loadNodes('手动');
btn.disabled = false;
btn.querySelector('svg').classList.remove('ghb-spin');
ok ? View.Toast.ok('已刷新,共 ' + NodeStore.nodes.length + ' 个节点')
: View.Toast.err('刷新失败,请检查网络或稍后重试');
},
async onProbe(btn) {
if (!NodeStore.nodes.length) { View.Toast.warn('暂无节点可测速'); return; }
btn.disabled = true;
btn.querySelector('svg').classList.add('ghb-spin');
const list = await probeMany(NodeStore.nodes.map((n) => n.url));
btn.disabled = false;
btn.querySelector('svg').classList.remove('ghb-spin');
if (!list.length) { View.Toast.err('全部节点均不可达'); return; }
NodeStore.markOkMany(list.map((n) => n.url));
NodeStore.setNodes(list);
View.Toast.ok('测速完成,' + list.length + ' 个节点已就绪');
},
onTest(btn) {
const url = btn.dataset.url;
const old = btn.textContent;
btn.disabled = true;
btn.textContent = '…';
probeOne(url).then((r) => {
btn.textContent = r.ok ? r.ms + 'ms' : '不可达';
btn.style.color = r.ok ? 'var(--ghb-good)' : 'var(--ghb-bad)';
const node = NodeStore.nodes.find((n) => n.url === url);
if (node && r.ok) {
node.latency = r.ms;
NodeStore.setNodes(NodeStore.nodes.slice().sort((a, b) => a.latency - b.latency));
}
setTimeout(() => {
btn.textContent = old;
btn.style.color = '';
btn.disabled = false;
}, 2600);
});
},
renderInjectPage() {
const page = this.root.querySelector('#ghb-page-inject');
const cfg = Settings.get().inject;
page.innerHTML =
'控制各位置「镜像下载」按钮的显示。改动立即生效,已渲染的按钮需刷新页面才移除。
' +
SCENARIOS.map((s) =>
'' +
' ' +
' ' +
' ' +
' ' +
'
').join('');
page.querySelectorAll('.ghb-switch input').forEach((cb) => {
cb.addEventListener('change', () => {
Settings.setInject(cb.dataset.key, cb.checked);
const s = SCENARIOS.find((x) => x.key === cb.dataset.key);
View.Toast.info((cb.checked ? '已开启「' : '已关闭「') + (s ? s.label : cb.dataset.key) + '」');
Injector.schedule(150);
});
});
},
renderSettingsPage() {
const page = this.root.querySelector('#ghb-page-settings');
const s = Settings.get();
page.innerHTML =
'' +
' ' +
' ' +
'
' +
'' +
' ' +
' ' +
'
' +
'' +
' ' +
' ' +
'
' +
'' +
' ' +
' ' +
'
' +
'' +
' 恢复默认设置' +
' 清空节点缓存与全部偏好' +
' ' +
'
' +
'' +
' ' + VERSION + ' · 作者 ' + AUTHOR + '
' +
' 本脚本不接管下载:只负责挑选可用镜像并生成直链,下载一律走浏览器原生通道,Gopeed / IDM 等工具可正常接管。
' +
' 若浏览器无反应,点「复制链接」粘贴进下载工具即可。' +
'
';
page.querySelector('#ghb-s-autorefresh').addEventListener('change', (e) => {
Settings.set({ refreshOnStart: e.target.checked });
});
page.querySelector('#ghb-s-auto').addEventListener('change', (e) => {
Settings.set({ autoDownload: e.target.checked });
View.Toast.info(e.target.checked
? '已开启全自动:自动选最快镜像,失败自动换下一个'
: '已切换为手动:每次下载先弹窗手选镜像');
});
page.querySelector('#ghb-s-pagebtn').addEventListener('change', (e) => {
Settings.set({ showPageButtons: e.target.checked });
View.Toast.info('页面内镜像按钮已' + (e.target.checked ? '开启' : '关闭') + ',刷新页面后生效');
});
page.querySelector('#ghb-s-launcher').addEventListener('change', (e) => {
View.setLauncherVisible(e.target.checked);
View.Toast.info('侧边启动器已' + (e.target.checked ? '显示' : '隐藏'));
});
page.querySelector('#ghb-s-reset').addEventListener('click', () => {
resetAll();
View.restoreLauncherPos();
this.refresh();
View.Toast.ok('已恢复默认设置');
});
},
renderEnhancePage() {
const page = this.root.querySelector('#ghb-page-enhance');
if (!page) return;
const cfg = Settings.get().enhance;
const rows = [
{ key: 'groupSort', t: '文件分组排序', d: '按平台分组、按当前系统/架构排序,高亮最可能安装的包,并提供 OS / 架构手动切换' },
{ key: 'downloadCount', t: '显示下载量', d: '通过 GitHub API 显示每个 Release 文件的下载次数(受 API 限流影响)' },
{ key: 'replaceTime', t: '相对时间转精确时间', d: '将「2 days ago」替换为「YYYY-MM-DD HH:MM:SS」' },
{ key: 'collapsibleNotes', t: '更新日志可折叠', d: '给 Release 更新日志增加折叠开关,默认展开' }
];
page.innerHTML =
'Release 增强显示:文件分组排序、下载量、精确时间、日志折叠,均可独立开关。改动即时生效,部分需刷新页面。
' +
rows.map((r) =>
'' +
' ' +
' ' +
'
').join('');
page.querySelectorAll('.ghb-switch input').forEach((cb) => {
cb.addEventListener('change', () => {
const key = cb.dataset.key;
Settings.get().enhance[key] = cb.checked;
Store.write(K.settings, Settings.get());
View.Toast.info((cb.checked ? '已开启「' : '已关闭「') + (rows.find((x) => x.key === key).t) + '」');
Enhancer.scan();
});
});
},
renderToolsPage() {
const page = this.root.querySelector('#ghb-page-tools');
if (!page) return;
const cfg = Settings.get().tools;
const rows = [
{ key: 'deepwiki', t: 'DeepWiki 跳转按钮', d: '在仓库页顶部操作区注入 DeepWiki 入口,新标签打开该仓库的 AI 生成 Wiki 文档;非仓库页(设置、组织等)不会注入' }
];
page.innerHTML =
'仓库页辅助工具:页面级小功能,改动即时生效,无需刷新。
' +
rows.map((r) =>
'' +
' ' +
' ' +
'
').join('');
page.querySelectorAll('.ghb-switch input').forEach((cb) => {
cb.addEventListener('change', () => {
const key = cb.dataset.key;
Settings.get().tools[key] = cb.checked;
Store.write(K.settings, Settings.get());
View.Toast.info((cb.checked ? '已开启「' : '已关闭「') + (rows.find((x) => x.key === key).t) + '」');
Tools.scan();
});
});
},
},
/* ---------- 下载弹窗 ---------- */
DlModal: {
url: '',
name: '',
root: null,
mount(root) {
this.root = root;
root.querySelector('.ghb-dl-bg').addEventListener('click', () => this.close());
root.querySelector('#ghb-dl-close').addEventListener('click', () => this.close());
root.querySelector('#ghb-dl-refresh').addEventListener('click', (e) => this.onRefresh(e.currentTarget));
root.querySelector('#ghb-dl-copy').addEventListener('click', () => this.onCopy());
root.querySelector('#ghb-dl-fast').addEventListener('click', () => this.onFastest());
root.querySelector('#ghb-dl-nodes').addEventListener('click', (e) => {
const b = e.target.closest('.ghb-dl-go');
if (b) this.onDownload(b.dataset.node, b);
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && root.classList.contains('ghb-open')) this.close();
});
},
open(githubUrl, filename) {
this.url = githubUrl || '';
this.name = filename || Utils.filenameFromUrl(githubUrl);
this.root.querySelector('#ghb-dl-name').textContent = this.name;
this.renderNodes();
this.root.classList.add('ghb-open');
if (!NodeStore.nodes.length) loadNodes('弹窗').then(() => this.renderNodes());
},
close() {
this.root.classList.remove('ghb-open');
},
nodes() { return NodeStore.candidates(); },
renderNodes() {
const host = this.root.querySelector('#ghb-dl-nodes');
const list = this.nodes();
const count = this.root.querySelector('#ghb-dl-count');
if (count) count.textContent = list.length + ' 个节点';
if (!list.length) {
host.innerHTML = '暂无可用节点
正在后台自动获取…' +
'可点击右上角刷新按钮重试
';
return;
}
host.innerHTML = list.map((n) => {
const ms = n.latency || 0;
return '' +
'' + Utils.esc(Utils.shortDomain(n.url)) + '' +
'' + ms + 'ms' +
'' +
'
';
}).join('');
},
async onRefresh(btn) {
btn.querySelector('svg').classList.add('ghb-spin');
btn.disabled = true;
await loadNodes('弹窗');
btn.disabled = false;
btn.querySelector('svg').classList.remove('ghb-spin');
this.renderNodes();
View.Toast.info('节点已刷新');
},
async onCopy() {
const list = this.nodes();
if (!list.length) { View.Toast.warn('暂无节点,无法生成镜像链接'); return; }
const url = mirrorUrl(this.url, list[0].url);
const ok = await Utils.copy(url);
View.Toast.show(ok ? '已复制最快节点链接,可粘贴进下载工具' : '复制失败,请手动复制', ok ? 'ok' : 'err');
},
onFastest() {
const list = this.nodes();
if (!list.length) { View.Toast.warn('暂无可用节点'); return; }
this.onDownload(list[0].url);
},
async onDownload(nodeUrl, btn) {
const target = mirrorUrl(this.url, nodeUrl);
if (!target) { View.Toast.err('链接拼装失败'); return; }
// fast-path:fresh 候选直接 fire
if (NodeStore.isFresh(nodeUrl)) {
this.close();
Downloader.deliver(target, this.name);
View.Toast.ok('已交给浏览器下载 · ' + this.name + '|Gopeed 等工具会自动接管');
return;
}
if (btn) { btn.disabled = true; btn.textContent = '预检中…'; }
const head = await precheck(target, HEAD_TIMEOUT_FAST);
if (!head.ok) {
NodeStore.markFail(nodeUrl);
if (btn) { btn.disabled = false; btn.innerHTML = Icons.download + '下载'; }
View.Toast.err('该节点预检失败(' + head.error + '),已记入健康度,试试其他节点');
return;
}
NodeStore.markOk(nodeUrl);
this.close();
Downloader.deliver(target, this.name);
View.Toast.ok('已交给浏览器下载 · ' + this.name + '|Gopeed 等工具会自动接管');
}
}
};
/* ======================================================================
* L7 · BOOTSTRAP
* ==================================================================== */
function toggleEnhance(key, label) {
const s = Settings.get();
const next = !s.enhance[key];
s.enhance[key] = next;
Store.write(K.settings, s);
View.Toast.info('「' + label + '」已' + (next ? '启用' : '禁用') + ',刷新页面后生效');
Enhancer.scan();
refreshMenu();
}
function toggleTool(key, label) {
const s = Settings.get();
const next = !s.tools[key];
s.tools[key] = next;
Store.write(K.settings, s);
View.Toast.info('「' + label + '」已' + (next ? '启用' : '禁用') + ',页面即时生效');
Tools.scan();
refreshMenu();
}
/* ---- 分组菜单:面板|节点|下载|增强|工具|设置 ----
* 组名行(fn 为 null)仅作视觉分组;开关项标签带「开/关」状态,
* 任何切换后 refreshMenu() 注销重注册,让菜单文字始终与实际一致。 */
let menuCmds = [];
function menuStateTag(v) { return v ? ':开 ✓' : ':关'; }
function buildMenuItems() {
const st = Settings.get();
const eh = st.enhance, th = st.tools;
return [
['打开加速面板', () => { if (!View.Panel.open) View.Panel.toggle(); }],
['─── 节点 ───', null],
['刷新镜像节点', () => loadNodes('菜单').then((ok) =>
ok ? View.Toast.ok('已刷新 ' + NodeStore.nodes.length + ' 个节点') : View.Toast.err('刷新失败'))],
['显示 / 隐藏侧边启动器', () => {
const on = !st.showLauncher;
View.setLauncherVisible(on);
View.Toast.info('侧边启动器已' + (on ? '显示' : '隐藏'));
refreshMenu();
}],
['─── 下载 ───', null],
['全自动下载' + menuStateTag(st.autoDownload), () => {
Settings.set({ autoDownload: !Settings.get().autoDownload });
View.Toast.info('全自动下载已' + (Settings.get().autoDownload ? '开启' : '关闭'));
refreshMenu();
}],
['─── 增强 ───', null],
['★ 分组排序' + menuStateTag(eh.groupSort), () => toggleEnhance('groupSort', '文件分组排序')],
['★ 显示下载量' + menuStateTag(eh.downloadCount), () => toggleEnhance('downloadCount', '显示下载量')],
['★ 精确时间' + menuStateTag(eh.replaceTime), () => toggleEnhance('replaceTime', '相对时间替换')],
['★ 日志折叠' + menuStateTag(eh.collapsibleNotes), () => toggleEnhance('collapsibleNotes', '更新日志折叠')],
['─── 工具 ───', null],
['DeepWiki 跳转' + menuStateTag(th.deepwiki), () => toggleTool('deepwiki', 'DeepWiki 跳转')],
['─── 设置 ───', null],
['重置全部设置', () => {
resetAll();
View.restoreLauncherPos();
View.Toast.ok('已重置,刷新页面后生效');
}]
];
}
function refreshMenu() {
if (typeof GM_registerMenuCommand !== 'function') return;
if (typeof GM_unregisterMenuCommand === 'function') {
menuCmds.forEach((h) => { try { GM_unregisterMenuCommand(h); } catch (e) { /* 忽略 */ } });
}
menuCmds = [];
buildMenuItems().forEach(([label, fn]) => {
menuCmds.push(GM_registerMenuCommand(label, fn || (() => {})));
});
}
function registerMenu() { refreshMenu(); }
/** 清空全部持久化数据并恢复默认(油猴菜单与设置页共用,避免两处逻辑漂移) */
function resetAll() {
Settings.reset();
Store.remove(K.nodes); Store.remove(K.visible); Store.remove(K.updatedAt); Store.remove(K.fails); Store.remove(K.lastOk);
NodeStore.nodes = []; NodeStore.visible = [];
NodeStore.updatedAt = 0; NodeStore.fails = {}; NodeStore.lastOk = {};
NodeStore.emit();
}
function bootstrap() {
Settings.load();
NodeStore.hydrate();
View.mount();
View.restoreLauncherPos();
// 状态变更 → 面板/弹窗自动重绘,无需手工调用刷新
NodeStore.subscribe(() => {
View.Panel.renderList();
View.DlModal.renderNodes();
});
registerMenu();
if (Settings.get().showPageButtons) Injector.start();
Enhancer.init();
Tools.init();
if (NodeStore.isStale() && Settings.get().refreshOnStart) {
loadNodes('启动');
} else if (NodeStore.nodes.length) {
loadNodes('后台'); // 缓存可用,静默更新
}
setInterval(() => loadNodes('定时'), NODE_TTL);
}
try {
bootstrap();
} catch (err) {
Log.error('初始化失败', err);
}
})();