Greasy Fork is available in English.
Smartly highlight the best GitHub Release download matching your OS & architecture — combining accurate matching, conflict exclusion, and rich visual feedback.
// ==UserScript==
// @name:zh-CN GitHub Release 智能高亮
// @name GitHub Release Smart Highlighter
// @namespace https://github.com/leeflouring
// @version 1.0.0
// @description Smartly highlight the best GitHub Release download matching your OS & architecture — combining accurate matching, conflict exclusion, and rich visual feedback.
// @description:zh-CN 智能高亮最符合你系统架构的 GitHub Release 下载——融合精准匹配、互斥排除与丰富视觉反馈,支持 macOS WebGL 架构探测、手动按钮容错。
// @author leeflouring
// @license MIT
// @match https://github.com/*/*/releases/tag/*
// @match https://github.com/*/*/releases/latest
// @match https://github.com/*/*/releases
// @match https://github.com/*/releases
// @match https://github.com/*/releases/*
// ==/UserScript==
(function () {
'use strict';
// ═══════════════════════════════════════════════════════════
// §1 平台探测
// ═══════════════════════════════════════════════════════════
function detectArchitecture(os) {
// macOS:通过 WebGL GPU 渲染器精确区分 Apple Silicon / Intel
if (os === 'darwin') {
try {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (gl) {
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
if (debugInfo) {
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL).toLowerCase();
if (renderer.includes('apple') || renderer.includes('m1') || renderer.includes('m2') || renderer.includes('m3') || renderer.includes('m4')) {
return 'arm64';
}
}
}
} catch (e) {}
return 'amd64';
}
// 非 macOS:从 UA 推断
const ua = (navigator.userAgent || '').toLowerCase();
const plat = (navigator.platform || '').toLowerCase();
if (/aarch64|arm64/.test(ua) || /aarch64|arm64/.test(plat)) return 'arm64';
if (/loongarch64|loong64/.test(ua)) return 'loong64';
if (/riscv64/.test(ua)) return 'riscv64';
if (/[-_.]arm[-_.v]/.test(ua) || /[-_.]arm$/.test(ua)) return 'arm';
return 'amd64';
}
function detectPlatform() {
const ua = navigator.userAgent || '';
const plat = navigator.platform || '';
let os = '';
if (/Windows/.test(ua)) os = 'windows';
else if (/Mac OS X|Macintosh|MacIntel|MacPPC|Mac68K/.test(ua) || /Mac/.test(plat)) os = 'darwin';
else if (/Linux/.test(ua) || /Linux/.test(plat) || /X11/.test(ua)) os = 'linux';
else if (/CrOS/.test(ua)) os = 'linux';
// Windows 版本号(NT 6.1=Win7, 6.2=Win8, 6.3=Win8.1, 10+=Win10/11)
let winVersion = null;
if (os === 'windows') {
const ntMatch = ua.match(/Windows NT (\d+\.\d+)/);
if (ntMatch) {
const ntVer = parseFloat(ntMatch[1]);
if (ntVer <= 6.1) winVersion = '7';
else if (ntVer <= 6.3) winVersion = '8';
else winVersion = '10+';
}
}
const arch = detectArchitecture(os);
return { os, arch, winVersion };
}
// ═══════════════════════════════════════════════════════════
// §2 匹配与评分
// ═══════════════════════════════════════════════════════════
// 综合忽略后缀(融合两份列表)
const IGNORED_EXTS = /\.(sha256|sha512|sha1|md5|asc|sig|txt|dgst|sbom|pem|key|blockmap)$/;
const IGNORED_KEYWORDS = ['source code', 'source.tar'];
// 互斥关键词:文件名含其他平台关键词时直接排除
const CONFLICT_KEYWORDS = {
darwin: ['windows', 'win64', 'win32', 'win-', 'linux', 'ubuntu', 'debian', 'rhel', 'fedora', '.rpm', '.deb'],
windows: ['macos', 'darwin', 'osx', 'mac-', 'linux', 'ubuntu', 'debian', '.appimage', '.deb', '.rpm'],
linux: ['windows', 'win64', 'win32', 'win-', 'macos', 'darwin', 'osx', 'mac-', '.exe', '.msi']
};
// OS 专属后缀
const OS_SPECIFIC_EXTS = {
darwin: ['.dmg', '.app', '.pkg'],
windows: ['.exe', '.msi'],
linux: ['.appimage', '.deb', '.rpm', '.run']
};
// 通用后缀
const UNIVERSAL_EXTS = ['.zip', '.tar.gz', '.tgz', '.7z', '.gz', '.xz', '.rar'];
// 架构关键词映射
const ARCH_KEYWORDS = {
arm64: ['aarch64', 'arm64', 'armv8', 'm1', 'm2', 'm3', 'm4', 'apple silicon'],
amd64: ['x86_64', 'amd64', 'x64', 'win64', 'intel'],
loong64: ['loong64', 'loongarch64'],
riscv64: ['riscv64'],
x86: ['x86', 'i386', 'i686', 'win32', '32-bit'],
arm: ['armv7', 'armhf', 'armv6']
};
function calculateScore(filename, profile) {
const lower = filename.toLowerCase();
// A. 忽略项
if (IGNORED_KEYWORDS.some(k => lower.includes(k))) return -1;
if (IGNORED_EXTS.test(lower)) return -1;
// B. 互斥关键词排除
if (CONFLICT_KEYWORDS[profile.os] && CONFLICT_KEYWORDS[profile.os].some(k => lower.includes(k))) return -1;
// C. 后缀分类
const isOsSpecific = OS_SPECIFIC_EXTS[profile.os] && OS_SPECIFIC_EXTS[profile.os].some(ext => lower.endsWith(ext));
const isUniversal = UNIVERSAL_EXTS.some(ext => lower.endsWith(ext));
if (!isOsSpecific && !isUniversal) return -1;
// D. 基础分:OS专属格式权重更高
let score = isOsSpecific ? 100 : 60;
// E. 架构匹配加分/减分
const isArchMatch = ARCH_KEYWORDS[profile.arch] && ARCH_KEYWORDS[profile.arch].some(k => lower.includes(k));
const isAmd64File = ARCH_KEYWORDS.amd64.some(k => lower.includes(k));
const isArm64File = ARCH_KEYWORDS.arm64.some(k => lower.includes(k));
const isX86File = ARCH_KEYWORDS.x86.some(k => lower.includes(k) && !lower.includes('x86_64'));
if (isArchMatch) {
score += 80; // 完全匹配
} else if (profile.arch === 'arm64' && isAmd64File) {
score += 20; // arm64 设备跑 amd64 可用但非最优
} else if (profile.arch === 'amd64' && isX86File) {
score += 10; // amd64 设备跑 x86 可用但非最优
} else if (profile.arch === 'amd64' && isArm64File) {
return -1; // amd64 设备不能跑 arm64
} else if (profile.arch === 'arm64' && isX86File) {
return -1; // arm64 设备不能跑 x86
} else if (!isAmd64File && !isArm64File && !isX86File) {
score += 30; // 无架构标记的通用包(如 xxx.zip)
}
// F. 文件格式偏好微调
if (profile.os === 'windows') {
if (lower.endsWith('.exe')) score += 10;
else if (lower.endsWith('.msi')) score += 5;
}
if (profile.os === 'darwin' && lower.endsWith('.dmg')) score += 5;
if (profile.os === 'linux') {
if (lower.endsWith('.deb')) score += 5;
else if (lower.endsWith('.appimage')) score += 4;
else if (lower.endsWith('.rpm')) score += 3;
}
// G. Windows 7 特殊处理
if (profile.os === 'windows' && /win(?:dows)?[-_]?7/i.test(lower)) {
score += (profile.winVersion === '7') ? 40 : -80;
}
// H. desktop 版本略降权重(通常非核心包)
if (lower.includes('-desktop')) score -= 3;
return score;
}
// ═══════════════════════════════════════════════════════════
// §3 视觉反馈
// ═══════════════════════════════════════════════════════════
function injectStyles() {
if (document.getElementById('gh-smart-style')) return;
const style = document.createElement('style');
style.id = 'gh-smart-style';
style.textContent = `
@keyframes smart-glow {
0%,100% { box-shadow: 0 0 4px rgba(46,160,67,.3) }
50% { box-shadow: 0 0 16px rgba(46,160,67,.7) }
}
.gh-smart-highlight {
background: rgba(46,160,67,.1) !important;
border-left: 4px solid #2ea043 !important;
border-radius: 6px !important;
animation: smart-glow 2s ease-in-out 4;
transition: background .3s ease;
}
.gh-smart-badge {
display: inline-block;
margin-left: 8px;
padding: 2px 10px;
font-size: 12px;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #2ea043, #238636);
border-radius: 12px;
vertical-align: middle;
line-height: 18px;
letter-spacing: .3px;
}
body[data-color-mode="dark"] .gh-smart-highlight {
background: rgba(46,160,67,.18) !important;
}
.gh-smart-btn {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: 8px;
padding: 2px 10px;
font-size: 12px;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #238636, #1a7f37);
border: 1px solid rgba(255,255,255,.15);
border-radius: 12px;
cursor: pointer;
line-height: 20px;
vertical-align: middle;
transition: all .15s ease;
white-space: nowrap;
}
.gh-smart-btn:hover {
background: linear-gradient(135deg, #2ea043, #238636);
box-shadow: 0 0 8px rgba(46,160,67,.4);
transform: scale(1.05);
}
.gh-smart-btn:active { transform: scale(.97) }
.gh-smart-btn.done {
background: linear-gradient(135deg, #1a7f37, #156d2e);
opacity: .7;
cursor: default;
}
.gh-smart-btn.warn {
background: linear-gradient(135deg, #bd561d, #9e4216);
opacity: .8;
}
`;
document.head.appendChild(style);
}
// ═══════════════════════════════════════════════════════════
// §4 高亮核心逻辑(防抖 + 清除旧标记 + 评分排序)
// ═══════════════════════════════════════════════════════════
function clearHighlight(container) {
const rows = container.querySelectorAll('li.Box-row');
rows.forEach(row => {
row.classList.remove('gh-smart-highlight');
const badge = row.querySelector('.gh-smart-badge');
if (badge) badge.remove();
});
}
function highlightInContainer(container, shouldScroll = false) {
const profile = detectPlatform();
const rows = container.querySelectorAll('li.Box-row');
if (rows.length === 0) return 0;
clearHighlight(container);
let bestRow = null, bestScore = -1;
rows.forEach(row => {
const link = row.querySelector('a');
if (!link) return;
const name = link.textContent.trim();
const score = calculateScore(name, profile.os, profile.arch, profile.winVersion);
if (score > bestScore) { bestScore = score; bestRow = row; }
});
if (bestRow && bestScore > 0) {
bestRow.classList.add('gh-smart-highlight');
const a = bestRow.querySelector('a');
if (a && !a.querySelector('.gh-smart-badge')) {
const badge = document.createElement('span');
badge.className = 'gh-smart-badge';
badge.textContent = `✔ ${profile.os} ${profile.arch}`;
a.appendChild(badge);
}
// 只有用户主动操作时才滚动,避免自动监听器不断抢夺滚动位置
if (shouldScroll) {
bestRow.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
syncButtonState(container);
return rows.length;
}
return 0;
}
// ═══════════════════════════════════════════════════════════
// §5 手动按钮
// ═══════════════════════════════════════════════════════════
function createManualButton(details) {
const profile = detectPlatform();
const btn = document.createElement('span');
btn.className = 'gh-smart-btn';
btn.textContent = `📥 ${profile.os}/${profile.arch}`;
btn.title = '手动高亮推荐下载(自动检测失败时使用)';
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const count = highlightInContainer(details, true);
if (count > 0) {
btn.textContent = '✔ Done';
btn.classList.add('done');
} else {
btn.textContent = '⚠ 无匹配文件';
btn.classList.add('warn');
setTimeout(() => {
btn.textContent = `📥 ${profile.os}/${profile.arch}`;
btn.classList.remove('warn');
}, 2000);
}
});
return btn;
}
function syncButtonState(container) {
const btn = container.querySelector('.gh-smart-btn');
if (!btn) return;
if (container.querySelector('.gh-smart-highlight')) {
btn.textContent = '✔ Done';
btn.classList.add('done');
}
}
// ═══════════════════════════════════════════════════════════
// §6 容器注册(按钮 + 多层监听 + 即时尝试)
// ═══════════════════════════════════════════════════════════
function setupDetails(details) {
injectStyles();
// 挂载手动按钮
if (!details.querySelector('.gh-smart-btn')) {
const summary = details.querySelector('summary');
if (summary) {
const assetsSpan = summary.querySelector('.f3.text-bold');
if (assetsSpan) {
assetsSpan.parentElement.appendChild(createManualButton(details));
}
}
}
// 监听 details[open]
openObserver.observe(details, { attributes: true, attributeFilter: ['open'] });
// include-fragment 加载监听
const frag = details.querySelector('include-fragment[src*="expanded_assets"]');
if (frag) setupFragmentListener(frag, details);
// 即时尝试
highlightInContainer(details);
}
function setupFragmentListener(fragment, details) {
if (details.querySelectorAll('li.Box-row').length > 0) {
highlightInContainer(details);
return;
}
fragment.addEventListener('load', () => {
setTimeout(() => highlightInContainer(details), 100);
});
}
// ═══════════════════════════════════════════════════════════
// §7 全局监听(防抖 + 多事件覆盖)
// ═══════════════════════════════════════════════════════════
// details[open] 变化
const openObserver = new MutationObserver((mutations) => {
mutations.forEach(m => {
const details = m.target;
if (details.open) {
const frag = details.querySelector('include-fragment[src*="expanded_assets"]');
if (frag) setupFragmentListener(frag, details);
debouncedHighlight(details, 500);
debouncedHighlight(details, 2000);
}
});
});
// DOM 变化(防抖版)
let domTimer = null;
const domObserver = new MutationObserver((mutations) => {
if (mutations.some(m => m.addedNodes.length > 0)) {
if (domTimer) clearTimeout(domTimer);
domTimer = setTimeout(() => {
const allDetails = document.querySelectorAll('details[data-target="details-toggle.detailsTarget"]');
allDetails.forEach(d => highlightInContainer(d));
}, 500);
}
// 单独处理新增的 include-fragment 和 details
mutations.forEach(m => {
m.addedNodes.forEach(node => {
if (node.nodeType !== 1) return;
if (node.tagName === 'INCLUDE-FRAGMENT' &&
node.getAttribute('src') && node.getAttribute('src').includes('expanded_assets')) {
const parentDetails = node.closest('details');
if (parentDetails) setupFragmentListener(node, parentDetails);
}
if (node.tagName === 'DETAILS' && node.dataset.target === 'details-toggle.detailsTarget') {
setupDetails(node);
}
});
});
});
function debouncedHighlight(container, delay) {
setTimeout(() => highlightInContainer(container), delay);
}
// ═══════════════════════════════════════════════════════════
// §8 主入口
// ═══════════════════════════════════════════════════════════
function init() {
injectStyles();
const allDetails = document.querySelectorAll('details[data-target="details-toggle.detailsTarget"]');
allDetails.forEach(d => setupDetails(d));
}
init();
// GitHub SPA 导航全覆盖
document.addEventListener('pjax:end', init);
document.addEventListener('turbo:load', init);
document.addEventListener('turbo:render', init);
// 启动 DOM 监听
domObserver.observe(document.body, { childList: true, subtree: true });
})();