Greasy Fork

Greasy Fork is available in English.

HuggingFace镜像链接提取器

在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('✨ 修复:彻底解决搜索高亮影响文件名间距的问题');
})();