您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
在HuggingFace页面提取下载链接,同时显示原始链接和hf-mirror.com镜像链接。v1.3.3: 彻底修复搜索高亮间距问题,改用精确匹配和渐变背景
// ==UserScript== // @name HuggingFace镜像链接提取器 // @namespace http://tampermonkey.net/ // @version 1.3.3 // @description 在HuggingFace页面提取下载链接,同时显示原始链接和hf-mirror.com镜像链接。v1.3.3: 彻底修复搜索高亮间距问题,改用精确匹配和渐变背景 // @author AI Assistant // @match https://huggingface.co/* // @match https://hf-mirror.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=huggingface.co // @grant none // ==/UserScript== (function() { 'use strict'; // 创建样式 const style = document.createElement('style'); style.textContent = ` .hf-extractor-btn { position: fixed; top: 20px; left: 20px; width: 60px; height: 60px; background: linear-gradient(45deg, #ff6b6b, #feca57); color: white; border: none; border-radius: 50%; cursor: pointer; font-size: 12px; font-weight: bold; z-index: 10000; box-shadow: 0 4px 15px rgba(0,0,0,0.3); transition: all 0.3s ease; } .hf-extractor-btn:hover { transform: scale(1.1); box-shadow: 0 6px 20px rgba(0,0,0,0.4); } .hf-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999; display: none; justify-content: center; align-items: center; } .hf-modal-content { background: white; border-radius: 15px; padding: 20px; max-width: 95vw; max-height: 95vh; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.3); min-width: 800px; display: flex; flex-direction: column; } .hf-header { text-align: center; margin-bottom: 15px; color: #333; border-bottom: 2px solid #eee; padding-bottom: 10px; } .hf-stats { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 10px; border-radius: 8px; text-align: center; margin-bottom: 15px; font-weight: bold; font-size: 14px; } .hf-buttons { display: flex; gap: 8px; margin-bottom: 15px; flex-wrap: wrap; } .hf-sort-buttons { display: flex; gap: 5px; margin-bottom: 10px; justify-content: center; } .hf-search-container { margin-bottom: 15px; position: relative; } .hf-search-input { width: 100%; padding: 10px 40px 10px 15px; border: 2px solid #ddd; border-radius: 8px; font-size: 14px; outline: none; transition: all 0.3s ease; box-sizing: border-box; color: #2c3e50 !important; background: #ffffff !important; } .hf-search-input:focus { border-color: #3498db; box-shadow: 0 0 10px rgba(52, 152, 219, 0.3); } .hf-search-input::placeholder { color: #7f8c8d !important; } .hf-search-clear { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); background: none; border: none; font-size: 18px; cursor: pointer; color: #999; padding: 5px; border-radius: 50%; transition: all 0.2s ease; } .hf-search-clear:hover { background: #f0f0f0; color: #666; } .hf-search-stats { font-size: 12px; color: #2c3e50 !important; text-align: center; margin-top: 5px; font-weight: 500; } .hf-highlight { background: linear-gradient(to bottom, transparent 0%, transparent 20%, #ffeb3b 20%, #ffeb3b 80%, transparent 80%, transparent 100%) !important; color: inherit !important; font-weight: 600 !important; padding: 0 !important; margin: 0 !important; border: none !important; border-radius: 0 !important; font-size: inherit !important; font-family: inherit !important; display: inline !important; line-height: inherit !important; letter-spacing: inherit !important; word-spacing: inherit !important; text-decoration: none !important; vertical-align: baseline !important; box-shadow: none !important; outline: none !important; text-shadow: none !important; position: relative !important; } /* 针对黑暗模式的额外优化 */ @media (prefers-color-scheme: dark) { .hf-modal-content { background: #ffffff !important; color: #2c3e50 !important; } .hf-link-item { background: #f8f9fa !important; color: #2c3e50 !important; } .hf-link-row { background: #ffffff !important; color: #2c3e50 !important; } } .hf-sort-btn { padding: 5px 10px; border: 1px solid #ddd; border-radius: 5px; cursor: pointer; font-size: 12px; background: white; transition: all 0.2s ease; } .hf-sort-btn:hover { background: #f0f0f0; } .hf-sort-btn.active { background: #3498db; color: white; border-color: #3498db; } .hf-links-wrapper { flex: 1; overflow-y: auto; max-height: calc(95vh - 300px); } .hf-btn { padding: 8px 12px; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; transition: all 0.3s ease; flex: 1; min-width: 100px; font-size: 13px; } .hf-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.2); } .hf-btn-close { background: #e74c3c; color: white; } .hf-btn-copy-first { background: #3498db; color: white; } .hf-btn-copy-all-orig { background: #27ae60; color: white; } .hf-btn-copy-all-mirror { background: #f39c12; color: white; } .hf-link-item { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 8px; margin-bottom: 6px; transition: all 0.2s ease; } .hf-link-item.main-file { border: 2px solid #ff6b6b; background: linear-gradient(135deg, #fff5f5 0%, #ffe8e8 100%); } .hf-link-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); transform: translateY(-1px); } .hf-file-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; } .hf-file-name { font-weight: bold; color: #2c3e50 !important; font-size: 14px; display: flex; align-items: center; gap: 6px; flex: 1; } .hf-file-size { font-size: 12px; color: #2c3e50 !important; background: #e9ecef; padding: 2px 6px; border-radius: 4px; margin-left: 8px; font-weight: 500; } .hf-main-file { background: linear-gradient(45deg, #ff6b6b, #feca57); color: white; padding: 1px 6px; border-radius: 8px; font-size: 10px; font-weight: bold; } .hf-link-row { display: flex; align-items: center; margin-bottom: 2px; padding: 4px; background: white; border-radius: 4px; } .hf-link-label { font-weight: bold; min-width: 45px; margin-right: 6px; font-size: 11px; color: #2c3e50 !important; } .hf-link-url { flex: 1; font-family: monospace; font-size: 12px; word-break: break-all; margin-right: 6px; color: #2c3e50 !important; font-weight: 500; } .hf-copy-btn { padding: 3px 8px; border: none; border-radius: 4px; cursor: pointer; font-size: 10px; font-weight: bold; transition: all 0.2s ease; min-width: 40px; } .hf-copy-orig { background: #3498db; color: white; } .hf-copy-mirror { background: #f39c12; color: white; } .hf-copy-btn:hover { opacity: 0.8; } .hf-more-info { text-align: center; padding: 15px; color: #7f8c8d; font-style: italic; background: #ecf0f1; border-radius: 8px; } `; document.head.appendChild(style); // 创建提取按钮 const extractBtn = document.createElement('button'); extractBtn.className = 'hf-extractor-btn'; extractBtn.innerHTML = '🔗<br>提取'; document.body.appendChild(extractBtn); // 创建模态框 const modal = document.createElement('div'); modal.className = 'hf-modal'; modal.innerHTML = ` <div class="hf-modal-content"> <div class="hf-header"> <h2>🚀 HuggingFace 下载链接提取器</h2> <p>同时提供原始链接和镜像链接</p> </div> <div class="hf-stats" id="hf-stats"></div> <div class="hf-buttons"> <button class="hf-btn hf-btn-close" id="hf-close">❌ 关闭</button> <button class="hf-btn hf-btn-copy-first" id="hf-copy-first">📋 复制第一个</button> <button class="hf-btn hf-btn-copy-all-orig" id="hf-copy-all-orig">📄 复制全部原始</button> <button class="hf-btn hf-btn-copy-all-mirror" id="hf-copy-all-mirror">🚀 复制全部镜像</button> </div> <div class="hf-search-container"> <input type="text" class="hf-search-input" id="hf-search-input" placeholder="🔍 输入文件名进行模糊搜索..."> <button class="hf-search-clear" id="hf-search-clear" title="清除搜索">✕</button> <div class="hf-search-stats" id="hf-search-stats"></div> </div> <div class="hf-sort-buttons"> <button class="hf-sort-btn active" data-sort="default">🏷️ 默认排序</button> <button class="hf-sort-btn" data-sort="name">📝 按名称</button> <button class="hf-sort-btn" data-sort="size">📊 按大小</button> <button class="hf-sort-btn" data-sort="type">📁 按类型</button> </div> <div class="hf-links-wrapper"> <div id="hf-links-container"></div> </div> </div> `; document.body.appendChild(modal); // 清理文件名,移除查询参数 function cleanFileName(fileName) { return fileName.replace(/\?.*$/, ''); } // 判断是否为主要文件 function isMainFile(fileName) { const mainFilePatterns = [ /^README\.md$/i, /^config\.json$/i, /^model\.safetensors$/i, /^pytorch_model\.bin$/i, /^model\.onnx$/i, /^tokenizer\.json$/i, /^tokenizer_config\.json$/i, /^vocab\.txt$/i, /^merges\.txt$/i, /\.py$/i, /^requirements\.txt$/i, /^setup\.py$/i, /^__init__\.py$/i, /^Dockerfile$/i, /^\.dockerignore$/i ]; return mainFilePatterns.some(pattern => pattern.test(fileName)); } // 格式化文件大小 function formatFileSize(bytes) { if (!bytes || bytes === 0) return '未知'; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; } // 从元素中提取文件大小 - 针对HuggingFace页面优化 function extractFileSize(element) { let sizeText = ''; const fileName = element.getAttribute('download') || element.textContent.trim(); console.log(`🔍 开始提取文件大小: ${fileName}`); // 方法1: 查找HuggingFace特定的文件列表结构 let currentElement = element; for (let level = 0; level < 6; level++) { if (!currentElement) break; // 在当前层级查找所有文本元素 const allElements = currentElement.querySelectorAll('*'); for (const el of allElements) { const text = el.textContent.trim(); // 精确匹配文件大小格式 (如: "459 Bytes", "1 kB", "2.68 kB") const sizeMatch = text.match(/^(\d+(?:\.\d+)?)\s*(Bytes?|kB|KB|MB|GB)$/i); if (sizeMatch) { sizeText = sizeMatch[0]; console.log(`✅ 在层级 ${level} 找到精确文件大小: ${sizeText}`); break; } // 也匹配包含文件大小的文本 const sizeInText = text.match(/(\d+(?:\.\d+)?)\s*(Bytes?|kB|KB|MB|GB)\b/i); if (sizeInText && text.length < 50) { // 避免匹配到很长的文本 sizeText = sizeInText[0]; console.log(`✅ 在层级 ${level} 找到文件大小: ${sizeText}`); break; } } if (sizeText) break; currentElement = currentElement.parentElement; } // 方法2: 查找同一行的其他元素 if (!sizeText) { const parentRow = element.closest('li') || element.closest('tr') || element.closest('div'); if (parentRow) { const rowText = parentRow.textContent; const sizeMatch = rowText.match(/(\d+(?:\.\d+)?)\s*(Bytes?|kB|KB|MB|GB)\b/i); if (sizeMatch) { sizeText = sizeMatch[0]; console.log(`✅ 在同一行找到文件大小: ${sizeText}`); } } } // 方法3: 全局搜索与文件名相关的大小信息 if (!sizeText && fileName) { console.log(`🔍 全局搜索文件: ${fileName}`); // 查找页面中所有可能包含文件大小的元素 const allTextElements = document.querySelectorAll('span, div, td, li, p'); for (const el of allTextElements) { const text = el.textContent.trim(); // 检查是否包含文件名和大小信息 if (text.includes(fileName) || el.closest('*').textContent.includes(fileName)) { const sizeMatch = text.match(/(\d+(?:\.\d+)?)\s*(Bytes?|kB|KB|MB|GB)\b/i); if (sizeMatch) { sizeText = sizeMatch[0]; console.log(`✅ 全局搜索找到文件大小: ${sizeText}`); break; } } } } // 方法4: 查找相邻元素 if (!sizeText && element.parentElement) { const siblings = Array.from(element.parentElement.children); for (const sibling of siblings) { const text = sibling.textContent.trim(); const sizeMatch = text.match(/^(\d+(?:\.\d+)?)\s*(Bytes?|kB|KB|MB|GB)$/i); if (sizeMatch) { sizeText = sizeMatch[0]; console.log(`✅ 在兄弟元素找到文件大小: ${sizeText}`); break; } } } const result = sizeText || '未知'; console.log(`📊 文件 ${fileName} 最终大小: ${result}`); return result; } // 解析文件大小为字节数(用于排序) function parseSizeToBytes(sizeStr) { if (!sizeStr || sizeStr === '未知') return 0; // 支持更多格式的匹配 const match = sizeStr.match(/(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|kB|bytes|Bytes)\b/i); if (!match) return 0; const value = parseFloat(match[1]); const unit = match[2].toUpperCase(); const multipliers = { 'B': 1, 'BYTES': 1, 'KB': 1024, 'kB': 1024, // 小写k 'MB': 1024 * 1024, 'GB': 1024 * 1024 * 1024 }; return value * (multipliers[unit] || 1); } // 全局变量存储当前排序方式和搜索状态 let currentSortType = 'default'; let allLinks = []; let filteredLinks = []; let currentSearchTerm = ''; // 提取链接函数 function extractLinks() { const links = []; const elements = document.querySelectorAll('a[download][href]'); console.log(`🔍 找到 ${elements.length} 个下载链接`); elements.forEach((element, index) => { const href = element.getAttribute('href'); if (href) { const originalLink = href.startsWith('http') ? href : 'https://huggingface.co' + href; const mirrorLink = originalLink.replace('huggingface.co', 'hf-mirror.com'); const rawFileName = href.split('/').pop() || 'unknown'; const fileName = cleanFileName(rawFileName); const isMain = isMainFile(fileName); const fileSize = extractFileSize(element); const fileSizeBytes = parseSizeToBytes(fileSize); console.log(`📁 文件 ${index + 1}: ${fileName}, 大小: ${fileSize}, 字节: ${fileSizeBytes}`); links.push({ original: originalLink, mirror: mirrorLink, fileName: fileName, isMainFile: isMain, fileSize: fileSize, fileSizeBytes: fileSizeBytes }); } }); allLinks = links; filteredLinks = links; return sortLinks(links, 'default'); } // 排序函数 function sortLinks(links, sortType) { const sorted = [...links]; switch (sortType) { case 'name': return sorted.sort((a, b) => a.fileName.localeCompare(b.fileName)); case 'size': return sorted.sort((a, b) => b.fileSizeBytes - a.fileSizeBytes); case 'type': return sorted.sort((a, b) => { const extA = a.fileName.split('.').pop().toLowerCase(); const extB = b.fileName.split('.').pop().toLowerCase(); return extA.localeCompare(extB); }); case 'default': default: return sorted.sort((a, b) => { if (a.isMainFile && !b.isMainFile) return -1; if (!a.isMainFile && b.isMainFile) return 1; return a.fileName.localeCompare(b.fileName); }); } } // 模糊搜索功能 function fuzzySearch(links, searchTerm) { if (!searchTerm.trim()) { return links; } const term = searchTerm.toLowerCase().trim(); return links.filter(link => { const fileName = link.fileName.toLowerCase(); // 精确匹配 if (fileName.includes(term)) { return true; } // 模糊匹配:检查搜索词的每个字符是否按顺序出现在文件名中 let termIndex = 0; for (let i = 0; i < fileName.length && termIndex < term.length; i++) { if (fileName[i] === term[termIndex]) { termIndex++; } } return termIndex === term.length; }); } // 高亮搜索结果 - 使用更温和的高亮方式 function highlightSearchTerm(text, searchTerm) { if (!searchTerm.trim()) { return text; } const term = searchTerm.trim().toLowerCase(); const lowerText = text.toLowerCase(); // 只进行精确匹配高亮,避免模糊匹配造成的间距问题 const exactIndex = lowerText.indexOf(term); if (exactIndex !== -1) { const before = text.substring(0, exactIndex); const match = text.substring(exactIndex, exactIndex + term.length); const after = text.substring(exactIndex + term.length); return before + '<span class="hf-highlight">' + match + '</span>' + after; } // 如果没有精确匹配,就不高亮,保持原始文本 return text; } // 更新搜索统计 function updateSearchStats(filteredCount, totalCount, searchTerm) { const statsElement = document.getElementById('hf-search-stats'); if (searchTerm.trim()) { statsElement.textContent = `找到 ${filteredCount} / ${totalCount} 个文件`; statsElement.style.display = 'block'; } else { statsElement.style.display = 'none'; } } // 执行搜索和显示 function performSearch() { const searchTerm = document.getElementById('hf-search-input').value; currentSearchTerm = searchTerm; // 先搜索,再排序 filteredLinks = fuzzySearch(allLinks, searchTerm); const sortedLinks = sortLinks(filteredLinks, currentSortType); // 更新统计信息 updateSearchStats(filteredLinks.length, allLinks.length, searchTerm); // 显示结果 displayLinks(sortedLinks, searchTerm); } // 复制到剪贴板 function copyToClipboard(text, button) { navigator.clipboard.writeText(text).then(() => { const originalText = button.textContent; button.textContent = '✅ 已复制!'; button.style.background = '#27ae60'; setTimeout(() => { button.textContent = originalText; button.style.background = ''; }, 1500); }).catch(err => { console.error('复制失败:', err); alert('复制失败,请手动复制'); }); } // 显示链接 function displayLinks(links, searchTerm = '') { const container = document.getElementById('hf-links-container'); const stats = document.getElementById('hf-stats'); const mainFileCount = links.filter(link => link.isMainFile).length; const totalSize = links.reduce((sum, link) => sum + link.fileSizeBytes, 0); const totalSizeStr = formatFileSize(totalSize); const statsText = mainFileCount > 0 ? `📊 共 <strong>${links.length}</strong> 个文件,其中 <strong>${mainFileCount}</strong> 个主要文件,总大小: <strong>${totalSizeStr}</strong>` : `📊 共 <strong>${links.length}</strong> 个文件,总大小: <strong>${totalSizeStr}</strong>`; stats.innerHTML = statsText; if (links.length === 0) { container.innerHTML = '<div class="hf-more-info">❌ 未找到任何下载链接</div>'; return; } let html = ''; for (let i = 0; i < links.length; i++) { const link = links[i]; const mainFileClass = link.isMainFile ? ' main-file' : ''; const fileIcon = link.isMainFile ? '⭐' : '📁'; const mainFileTag = link.isMainFile ? '<span class="hf-main-file">主要</span>' : ''; const highlightedFileName = highlightSearchTerm(link.fileName, searchTerm); html += ` <div class="hf-link-item${mainFileClass}"> <div class="hf-file-header"> <div class="hf-file-name"> ${fileIcon} ${highlightedFileName} ${mainFileTag} </div> <div class="hf-file-size">${link.fileSize}</div> </div> <div class="hf-link-row"> <span class="hf-link-label" style="color: #3498db;">🔗</span> <span class="hf-link-url">${link.original}</span> <button class="hf-copy-btn hf-copy-orig" onclick="copyToClipboard('${link.original}', this)">复制</button> </div> <div class="hf-link-row"> <span class="hf-link-label" style="color: #f39c12;">🚀</span> <span class="hf-link-url">${link.mirror}</span> <button class="hf-copy-btn hf-copy-mirror" onclick="copyToClipboard('${link.mirror}', this)">复制</button> </div> </div> `; } container.innerHTML = html; } // 事件监听 extractBtn.addEventListener('click', () => { const links = extractLinks(); displayLinks(links); modal.style.display = 'flex'; // 更新按钮事件 document.getElementById('hf-close').onclick = () => { modal.style.display = 'none'; }; document.getElementById('hf-copy-first').onclick = () => { if (filteredLinks.length > 0) { const currentLinks = sortLinks(filteredLinks, currentSortType); copyToClipboard(currentLinks[0].original, document.getElementById('hf-copy-first')); } }; document.getElementById('hf-copy-all-orig').onclick = () => { const currentLinks = currentSearchTerm ? filteredLinks : allLinks; const allOriginal = currentLinks.map(link => link.original).join('\n'); copyToClipboard(allOriginal, document.getElementById('hf-copy-all-orig')); }; document.getElementById('hf-copy-all-mirror').onclick = () => { const currentLinks = currentSearchTerm ? filteredLinks : allLinks; const allMirror = currentLinks.map(link => link.mirror).join('\n'); copyToClipboard(allMirror, document.getElementById('hf-copy-all-mirror')); }; // 排序按钮事件 document.querySelectorAll('.hf-sort-btn').forEach(btn => { btn.onclick = () => { const sortType = btn.getAttribute('data-sort'); currentSortType = sortType; // 更新按钮状态 document.querySelectorAll('.hf-sort-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); // 重新排序并显示 const sortedLinks = sortLinks(filteredLinks, sortType); displayLinks(sortedLinks, currentSearchTerm); }; }); // 搜索功能事件监听 const searchInput = document.getElementById('hf-search-input'); const searchClear = document.getElementById('hf-search-clear'); // 实时搜索 searchInput.addEventListener('input', performSearch); // 清除搜索 searchClear.addEventListener('click', () => { searchInput.value = ''; currentSearchTerm = ''; filteredLinks = allLinks; updateSearchStats(0, 0, ''); const sortedLinks = sortLinks(filteredLinks, currentSortType); displayLinks(sortedLinks); searchInput.focus(); }); // 键盘快捷键 searchInput.addEventListener('keydown', (e) => { if (e.key === 'Escape') { searchInput.value = ''; currentSearchTerm = ''; filteredLinks = allLinks; updateSearchStats(0, 0, ''); const sortedLinks = sortLinks(filteredLinks, currentSortType); displayLinks(sortedLinks); } }); }); // 点击模态框外部关闭 modal.addEventListener('click', (e) => { if (e.target === modal) { modal.style.display = 'none'; } }); // 全局函数供内联事件使用 window.copyToClipboard = copyToClipboard; // 测试文件大小提取功能 window.testFileSizeExtraction = function() { console.log('🧪 开始测试文件大小提取...'); const downloadLinks = document.querySelectorAll('a[download][href]'); console.log(`找到 ${downloadLinks.length} 个下载链接`); downloadLinks.forEach((link, index) => { const fileName = link.getAttribute('download') || link.textContent.trim(); const fileSize = extractFileSize(link); console.log(`${index + 1}. ${fileName} -> ${fileSize}`); }); console.log('🧪 测试完成!'); }; console.log('🚀 HuggingFace镜像链接提取器v1.3.3已加载'); console.log('💡 在控制台运行 testFileSizeExtraction() 来测试文件大小提取'); console.log('🔍 新功能:支持模糊搜索,可以快速过滤文件列表'); console.log('🌙 优化:修复黑暗模式下的字体可读性问题'); console.log('✨ 修复:彻底解决搜索高亮影响文件名间距的问题'); })();