GitHub 仓库下载器

在 GitHub 代码页面添加下载功能,支持选择性下载文件和目录为 ZIP 格式,支持递归下载子目录

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         GitHub 仓库下载器
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  在 GitHub 代码页面添加下载功能,支持选择性下载文件和目录为 ZIP 格式,支持递归下载子目录
// @author       GitHub Downloader
// @match        https://github.com/*
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        unsafeWindow
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.9.1/jszip.min.js
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 全局日志开关(使用 window 对象,可在控制台动态切换)
    // 使用方法:在浏览器控制台输入 window.GITHUB_DOWNLOADER_DEBUG = true/false
    if (typeof window.GITHUB_DOWNLOADER_DEBUG === 'undefined') {
        window.GITHUB_DOWNLOADER_DEBUG = false;
    }

    const log = (msg) => {
        if (window.GITHUB_DOWNLOADER_DEBUG) {
            console.log(`[GitHub下载器] ${new Date().toLocaleTimeString()}: ${msg}`);
        }
    };

    const error = (msg) => {
        if (window.GITHUB_DOWNLOADER_DEBUG) {
            console.error(`[GitHub下载器] ${new Date().toLocaleTimeString()}: ${msg}`);
        }
    };

    // 检查是否是代码页面
    function isCodePage() {
        const url = window.location.href;
        // 检查是否是仓库代码页面(排除 issues, pulls, releases 等)
        // 匹配: github.com/owner/repo 或 github.com/owner/repo/tree/branch 或 github.com/owner/repo/blob/branch/path
        const isRepo = /github\.com\/[^\/]+\/[^\/]+(?:\/(?:tree|blob)\/[^\/]+)?(?:\/.*)?$/.test(url);
        const notSpecialPage = !/\/(issues|pulls|releases|wiki|discussions|projects|security|settings|actions)/.test(url);
        const result = isRepo && notSpecialPage;
        log(`isCodePage 检查: URL=${url}, isRepo=${isRepo}, notSpecialPage=${notSpecialPage}, result=${result}`);
        return result;
    }

    // 获取或提示输入 GitHub Token
    function getGitHubToken() {
        let token = GM_getValue('github_token', '');
        
        if (!token) {
            const input = prompt('请输入 GitHub Personal Access Token(可选,用于提高 API 速率限制):\n\n如果不输入,将使用未认证请求(限制 60 次/小时)\n\n获取 Token: https://github.com/settings/tokens');
            if (input) {
                GM_setValue('github_token', input);
                token = input;
                log(`GitHub Token 已保存`);
            }
        }
        
        return token;
    }

    // 解析 GitHub URL 获取仓库信息
    function parseGitHubUrl() {
        log('开始解析 GitHub URL');
        const url = window.location.href;
        log(`当前 URL: ${url}`);

        const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)(?:\/tree\/([^\/]+))?(?:\/(.*))?/);
        if (!match) {
            log('URL 不匹配 GitHub 仓库格式');
            return null;
        }

        const owner = match[1];
        const repo = match[2];
        let branch = match[3];
        const path = match[4] || '';

        // 如果 URL 中没有分支信息,尝试从页面中检测
        if (!branch) {
            log('URL 中未找到分支信息,尝试从页面检测');
            
            // 方法 1: 从页面的分支选择器按钮中获取当前分支
            const branchButton = document.querySelector('[data-testid="anchor-button"][aria-label*="branch"]');
            if (branchButton) {
                // 查找包含分支名的 span
                const branchSpan = branchButton.querySelector('.RefSelectorAnchoredOverlay-module__RefSelectorText--bxVhQ');
                if (branchSpan) {
                    const branchName = branchSpan.textContent.trim();
                    log(`从分支按钮检测到分支: ${branchName}`);
                    branch = branchName;
                } else {
                    // 备用:从 aria-label 中提取
                    const ariaLabel = branchButton.getAttribute('aria-label');
                    const labelMatch = ariaLabel.match(/(\w+)\s+branch/);
                    if (labelMatch) {
                        branch = labelMatch[1];
                        log(`从 aria-label 检测到分支: ${branch}`);
                    }
                }
            }
            
            // 方法 2: 如果方法 1 失败,尝试从旧的分支选择器获取
            if (!branch) {
                const branchSelector = document.querySelector('[data-testid="ref-selector"]');
                if (branchSelector) {
                    const branchText = branchSelector.textContent.trim();
                    const branchName = branchText.split('\n')[0].trim();
                    log(`从旧分支选择器检测到分支: ${branchName}`);
                    branch = branchName;
                }
            }
            
            // 方法 3: 如果都失败,尝试从 meta 标签获取
            if (!branch) {
                const headBranch = document.querySelector('meta[name="branch"]');
                if (headBranch) {
                    branch = headBranch.getAttribute('content');
                    log(`从 meta 标签检测到分支: ${branch}`);
                }
            }
            
            // 方法 4: 如果都失败,尝试从页面 HTML 中查找分支信息
            if (!branch) {
                const pageHtml = document.documentElement.innerHTML;
                // 查找 "branch":"xxx" 的模式
                const branchMatch = pageHtml.match(/"branch":"([^"]+)"/);
                if (branchMatch) {
                    branch = branchMatch[1];
                    log(`从页面 HTML 检测到分支: ${branch}`);
                }
            }
            
            // 最后的默认值
            if (!branch) {
                branch = 'main';
                log(`使用默认分支: ${branch}`);
            }
        }

        log(`解析结果 - 所有者: ${owner}, 仓库: ${repo}, 分支: ${branch}, 路径: ${path}`);

        return { owner, repo, branch, path };
    }

    // 创建控制面板
    function createControlPanel() {
        log('创建控制面板');

        const panelId = 'github-zip-downloader-panel';
        
        // 检查是否已存在
        if (document.getElementById(panelId)) {
            log('控制面板已存在,跳过创建');
            return;
        }

        // 创建展开/收缩按钮(始终显示)
        const toggleBtn = document.createElement('button');
        toggleBtn.id = 'github-zip-toggle-btn';
        toggleBtn.textContent = '📦';
        toggleBtn.style.cssText = `
            position: fixed;
            bottom: 30px;
            right: 20px;
            width: 50px;
            height: 50px;
            border-radius: 50%;
            background: #0366d6;
            color: white;
            border: none;
            cursor: pointer;
            font-size: 24px;
            z-index: 9999;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
            transition: all 0.3s ease;
            display: flex;
            align-items: center;
            justify-content: center;
        `;

        toggleBtn.onmouseover = () => {
            toggleBtn.style.background = '#0256c7';
            toggleBtn.style.transform = 'scale(1.1)';
        };
        toggleBtn.onmouseout = () => {
            toggleBtn.style.background = '#0366d6';
            toggleBtn.style.transform = 'scale(1)';
        };

        // 主面板(默认隐藏)
        const panel = document.createElement('div');
        panel.id = panelId;
        panel.style.cssText = `
            position: fixed;
            bottom: 100px;
            right: 20px;
            background: white;
            border: 2px solid #0366d6;
            border-radius: 8px;
            padding: 15px;
            z-index: 10000;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
            width: 300px;
            max-height: 600px;
            overflow-y: auto;
            display: none;
            animation: slideIn 0.3s ease;
        `;

        // 添加动画样式
        const style = document.createElement('style');
        style.textContent = `
            @keyframes slideIn {
                from {
                    opacity: 0;
                    transform: translateY(10px);
                }
                to {
                    opacity: 1;
                    transform: translateY(0);
                }
            }
        `;
        document.head.appendChild(style);

        // 面板头部(带关闭按钮)
        const panelHeader = document.createElement('div');
        panelHeader.style.cssText = `
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 12px;
            padding-bottom: 10px;
            border-bottom: 2px solid #e1e4e8;
        `;

        const title = document.createElement('div');
        title.style.cssText = `
            font-weight: bold;
            font-size: 14px;
            color: #24292e;
        `;
        title.textContent = 'GitHub 下载器';

        const closeBtn = document.createElement('button');
        closeBtn.textContent = '✕';
        closeBtn.style.cssText = `
            background: none;
            border: none;
            font-size: 18px;
            cursor: pointer;
            color: #666;
            padding: 0;
            width: 24px;
            height: 24px;
        `;
        closeBtn.onclick = () => {
            panel.style.display = 'none';
            toggleBtn.style.display = 'flex';
        };

        panelHeader.appendChild(title);
        panelHeader.appendChild(closeBtn);

        // 分支信息显示
        const branchInfo = document.createElement('div');
        branchInfo.id = 'branch-info';
        branchInfo.style.cssText = `
            font-size: 11px;
            color: #666;
            margin-bottom: 10px;
            padding: 6px;
            background: #f6f8fa;
            border-radius: 4px;
        `;
        branchInfo.textContent = '分支: 加载中...';

        // 选择文件的容器
        const fileListContainer = document.createElement('div');
        fileListContainer.id = 'file-list-container';
        fileListContainer.style.cssText = `
            max-height: 200px;
            overflow-y: auto;
            margin-bottom: 10px;
            border: 1px solid #e1e4e8;
            border-radius: 4px;
            padding: 8px;
            background: #f6f8fa;
        `;

        // 全选复选框
        const selectAllContainer = document.createElement('div');
        selectAllContainer.style.cssText = `
            margin-bottom: 10px;
            padding-bottom: 8px;
            border-bottom: 1px solid #e1e4e8;
        `;

        const selectAllCheckbox = document.createElement('input');
        selectAllCheckbox.type = 'checkbox';
        selectAllCheckbox.id = 'select-all-checkbox';
        selectAllCheckbox.style.marginRight = '8px';

        const selectAllLabel = document.createElement('label');
        selectAllLabel.htmlFor = 'select-all-checkbox';
        selectAllLabel.textContent = '全选';
        selectAllLabel.style.cssText = `
            cursor: pointer;
            font-size: 13px;
            color: #24292e;
        `;

        selectAllContainer.appendChild(selectAllCheckbox);
        selectAllContainer.appendChild(selectAllLabel);

        // 按钮容器
        const buttonContainer = document.createElement('div');
        buttonContainer.style.cssText = `
            display: flex;
            gap: 8px;
            margin-bottom: 10px;
        `;

        // 下载按钮
        const downloadBtn = document.createElement('button');
        downloadBtn.textContent = '📥 下载';
        downloadBtn.style.cssText = `
            flex: 1;
            padding: 8px 12px;
            background: #28a745;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
            font-weight: 600;
            transition: background 0.2s;
        `;

        downloadBtn.onmouseover = () => downloadBtn.style.background = '#218838';
        downloadBtn.onmouseout = () => downloadBtn.style.background = '#28a745';

        // 刷新按钮
        const refreshBtn = document.createElement('button');
        refreshBtn.textContent = '🔄 刷新';
        refreshBtn.style.cssText = `
            flex: 1;
            padding: 8px 12px;
            background: #6f42c1;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
            font-weight: 600;
            transition: background 0.2s;
        `;

        refreshBtn.onmouseover = () => refreshBtn.style.background = '#5a32a3';
        refreshBtn.onmouseout = () => refreshBtn.style.background = '#6f42c1';

        buttonContainer.appendChild(downloadBtn);
        buttonContainer.appendChild(refreshBtn);

        // Token 管理容器
        const tokenContainer = document.createElement('div');
        tokenContainer.style.cssText = `
            margin-top: 10px;
            padding-top: 10px;
            border-top: 1px solid #e1e4e8;
            font-size: 12px;
        `;

        // Token 头部(可收缩)
        const currentToken = GM_getValue('github_token', '');
        const hasToken = !!currentToken;
        
        const tokenHeader = document.createElement('div');
        tokenHeader.style.cssText = `
            display: flex;
            justify-content: space-between;
            align-items: center;
            cursor: pointer;
            padding: 8px 10px;
            border-radius: 4px;
            background: ${hasToken ? '#d4edda' : '#f8d7da'};
            margin-bottom: 8px;
            user-select: none;
            border: 1px solid ${hasToken ? '#c3e6cb' : '#f5c6cb'};
        `;

        tokenHeader.onmouseover = () => tokenHeader.style.background = hasToken ? '#c3e6cb' : '#f5c6cb';
        tokenHeader.onmouseout = () => tokenHeader.style.background = hasToken ? '#d4edda' : '#f8d7da';

        const tokenTitle = document.createElement('div');
        tokenTitle.style.cssText = `
            font-weight: 600;
            color: ${hasToken ? '#155724' : '#721c24'};
            display: flex;
            align-items: center;
            gap: 6px;
        `;
        tokenTitle.innerHTML = `<span style="font-size: 16px;">${hasToken ? '✅' : '⚠️'}</span> <span>${hasToken ? 'Token 已设置' : 'Token 未设置'}</span>`;

        const tokenToggleIcon = document.createElement('span');
        tokenToggleIcon.textContent = '▼';
        tokenToggleIcon.style.cssText = `
            font-size: 10px;
            color: ${hasToken ? '#155724' : '#721c24'};
            transition: transform 0.3s ease;
        `;

        tokenHeader.appendChild(tokenTitle);
        tokenHeader.appendChild(tokenToggleIcon);

        // Token 内容容器(可收缩)
        const tokenContent = document.createElement('div');
        tokenContent.style.cssText = `
            display: block;
            transition: all 0.3s ease;
            max-height: 500px;
            overflow: hidden;
        `;

        let isTokenExpanded = !hasToken; // 如果没有 Token,默认展开;有 Token 则默认收缩

        const tokenStatusDiv = document.createElement('div');
        tokenStatusDiv.style.cssText = `
            padding: 8px;
            background: #f6f8fa;
            border-radius: 4px;
            font-size: 11px;
            color: #666;
            margin-bottom: 8px;
            border-left: 3px solid ${currentToken ? '#28a745' : '#d73a49'};
        `;
        if (currentToken) {
            tokenStatusDiv.textContent = `✅ Token 已保存 (${currentToken.substring(0, 10)}...)`;
        } else {
            tokenStatusDiv.textContent = '❌ 未设置 Token';
        }

        const tokenInputContainer = document.createElement('div');
        tokenInputContainer.style.cssText = `
            display: flex;
            gap: 4px;
            margin-bottom: 6px;
            flex-wrap: wrap;
        `;

        const tokenInput = document.createElement('input');
        tokenInput.placeholder = '粘贴 Token';
        tokenInput.style.cssText = `
            flex: 1;
            min-width: 120px;
            padding: 6px;
            border: 1px solid #e1e4e8;
            border-radius: 4px;
            font-size: 11px;
        `;

        const tokenSaveBtn = document.createElement('button');
        tokenSaveBtn.textContent = '保存';
        tokenSaveBtn.style.cssText = `
            padding: 6px 12px;
            background: #0366d6;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 11px;
            font-weight: 600;
        `;

        tokenSaveBtn.onmouseover = () => tokenSaveBtn.style.background = '#0256c7';
        tokenSaveBtn.onmouseout = () => tokenSaveBtn.style.background = '#0366d6';

        tokenSaveBtn.onclick = () => {
            const token = tokenInput.value.trim();
            if (token) {
                GM_setValue('github_token', token);
                tokenInput.value = '';
                log(`GitHub Token 已保存`);
                alert('✅ Token 已保存');
                // 更新状态显示
                tokenStatusDiv.textContent = `✅ Token 已保存 (${token.substring(0, 10)}...)`;
                tokenStatusDiv.style.borderLeftColor = '#28a745';
                // 更新头部显示
                tokenTitle.innerHTML = `<span style="font-size: 16px;">✅</span> <span>Token 已设置</span>`;
                tokenTitle.style.color = '#155724';
                tokenHeader.style.background = '#d4edda';
                tokenHeader.style.borderColor = '#c3e6cb';
                tokenToggleIcon.style.color = '#155724';
                // 自动收缩
                isTokenExpanded = false;
                updateTokenUI();
            } else {
                alert('❌ Token 不能为空');
            }
        };

        const tokenApplyBtn = document.createElement('button');
        tokenApplyBtn.textContent = '申请';
        tokenApplyBtn.style.cssText = `
            padding: 6px 12px;
            background: #28a745;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 11px;
            font-weight: 600;
        `;

        tokenApplyBtn.onmouseover = () => tokenApplyBtn.style.background = '#218838';
        tokenApplyBtn.onmouseout = () => tokenApplyBtn.style.background = '#28a745';

        tokenApplyBtn.onclick = () => {
            window.open('https://github.com/settings/tokens/new?scopes=repo,read:user&description=GitHub%20Downloader', '_blank');
        };

        const tokenClearBtn = document.createElement('button');
        tokenClearBtn.textContent = '清除';
        tokenClearBtn.style.cssText = `
            padding: 6px 12px;
            background: #d73a49;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 11px;
            font-weight: 600;
        `;

        tokenClearBtn.onmouseover = () => tokenClearBtn.style.background = '#cb2431';
        tokenClearBtn.onmouseout = () => tokenClearBtn.style.background = '#d73a49';

        tokenClearBtn.onclick = () => {
            if (confirm('确定要清除保存的 Token 吗?')) {
                GM_setValue('github_token', '');
                log(`GitHub Token 已清除`);
                alert('✅ Token 已清除');
                tokenStatusDiv.textContent = '❌ 未设置 Token';
                tokenStatusDiv.style.borderLeftColor = '#d73a49';
                // 更新头部显示
                tokenTitle.innerHTML = `<span style="font-size: 16px;">⚠️</span> <span>Token 未设置</span>`;
                tokenTitle.style.color = '#721c24';
                tokenHeader.style.background = '#f8d7da';
                tokenHeader.style.borderColor = '#f5c6cb';
                tokenToggleIcon.style.color = '#721c24';
                // 展开以便用户重新设置
                isTokenExpanded = true;
                updateTokenUI();
            }
        };

        tokenInputContainer.appendChild(tokenInput);
        tokenInputContainer.appendChild(tokenSaveBtn);
        tokenInputContainer.appendChild(tokenApplyBtn);
        tokenInputContainer.appendChild(tokenClearBtn);

        tokenContent.appendChild(tokenStatusDiv);
        tokenContent.appendChild(tokenInputContainer);

        // 更新 Token UI 的函数
        const updateTokenUI = () => {
            if (isTokenExpanded) {
                tokenContent.style.maxHeight = '500px';
                tokenContent.style.opacity = '1';
                tokenToggleIcon.style.transform = 'rotate(0deg)';
            } else {
                tokenContent.style.maxHeight = '0px';
                tokenContent.style.opacity = '0';
                tokenToggleIcon.style.transform = 'rotate(-90deg)';
            }
        };

        // 初始化 Token UI
        updateTokenUI();

        // Token 头部点击事件
        tokenHeader.onclick = () => {
            isTokenExpanded = !isTokenExpanded;
            updateTokenUI();
        };

        tokenContainer.appendChild(tokenHeader);
        tokenContainer.appendChild(tokenContent);

        // 组装面板内容
        panel.appendChild(panelHeader);
        panel.appendChild(branchInfo);
        panel.appendChild(selectAllContainer);
        panel.appendChild(fileListContainer);
        panel.appendChild(buttonContainer);
        panel.appendChild(tokenContainer);

        // 添加到页面
        document.body.appendChild(panel);
        document.body.appendChild(toggleBtn);

        // 切换按钮事件
        toggleBtn.onclick = () => {
            panel.style.display = 'block';
            toggleBtn.style.display = 'none';
        };

        log('控制面板创建完成');

        return { panel, fileListContainer, downloadBtn, refreshBtn, selectAllCheckbox, branchInfo, toggleBtn };
    }

    // 获取当前页面的文件列表
    function getFileListFromPage() {
        log('从页面获取文件列表');

        const files = [];
        const processedHrefs = new Set();

        // 方法 1: 查找 react-directory-row 行(新版 GitHub)
        log('尝试方法 1: 查找 react-directory-row');
        const directoryRows = document.querySelectorAll('tr.react-directory-row');
        log(`找到 ${directoryRows.length} 个目录行`);

        if (directoryRows.length > 0) {
            directoryRows.forEach((row, index) => {
                try {
                    // 查找行内的链接
                    const link = row.querySelector('a[href*="/blob/"], a[href*="/tree/"]');
                    if (!link) {
                        log(`行 ${index}: 没有找到文件链接`);
                        return;
                    }

                    const href = link.getAttribute('href');
                    const fileName = link.textContent.trim();

                    // 基本验证
                    if (!href || !fileName || processedHrefs.has(href)) {
                        log(`行 ${index}: 跳过 (href=${href}, fileName=${fileName})`);
                        return;
                    }

                    // 跳过非标准链接
                    if (!href.includes('/blob/') && !href.includes('/tree/')) {
                        log(`行 ${index}: 跳过非标准格式 href="${href}"`);
                        return;
                    }

                    // 跳过包含查询参数的链接
                    if (href.includes('?')) {
                        log(`行 ${index}: 跳过包含查询参数的链接 href="${href}"`);
                        return;
                    }

                    processedHrefs.add(href);
                    const isDirectory = href.includes('/tree/');

                    log(`行 ${index}: 文件名="${fileName}", 是目录=${isDirectory}`);

                    files.push({
                        name: fileName,
                        href: href,
                        isDirectory: isDirectory,
                        fullUrl: `https://github.com${href}`
                    });
                } catch (e) {
                    error(`解析行 ${index} 时出错: ${e.message}`);
                }
            });
        }

        // 方法 2: 如果方法 1 没有找到,查找所有 /blob/ 和 /tree/ 链接
        if (files.length === 0) {
            log('方法 1 未找到文件,尝试方法 2: 查找所有 /blob/ 和 /tree/ 链接');
            const allLinks = document.querySelectorAll('a[href*="/blob/"], a[href*="/tree/"]');
            log(`找到 ${allLinks.length} 个文件/目录链接`);

            allLinks.forEach((link, index) => {
                try {
                    const href = link.getAttribute('href');
                    const fileName = link.textContent.trim();

                    if (!href || !fileName || processedHrefs.has(href)) {
                        return;
                    }

                    if (!href.includes('/blob/') && !href.includes('/tree/')) {
                        return;
                    }

                    if (href.includes('?')) {
                        return;
                    }

                    processedHrefs.add(href);
                    const isDirectory = href.includes('/tree/');

                    log(`链接 ${index}: 文件名="${fileName}", 是目录=${isDirectory}`);

                    files.push({
                        name: fileName,
                        href: href,
                        isDirectory: isDirectory,
                        fullUrl: `https://github.com${href}`
                    });
                } catch (e) {
                    error(`解析链接 ${index} 时出错: ${e.message}`);
                }
            });
        }

        log(`总共获取 ${files.length} 个文件/目录`);
        return files;
    }

    // 渲染文件列表到面板
    function renderFileList(files, container, selectAllCheckbox) {
        log('渲染文件列表到面板');

        container.innerHTML = '';

        if (files.length === 0) {
            log('文件列表为空');
            const emptyMsg = document.createElement('div');
            emptyMsg.textContent = '没有找到文件';
            emptyMsg.style.cssText = `
                padding: 10px;
                text-align: center;
                color: #666;
                font-size: 12px;
            `;
            container.appendChild(emptyMsg);
            return;
        }

        files.forEach((file, index) => {
            const checkboxContainer = document.createElement('div');
            checkboxContainer.style.cssText = `
                display: flex;
                align-items: center;
                padding: 6px 0;
                border-bottom: 1px solid #e1e4e8;
            `;

            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.className = 'file-checkbox';
            checkbox.value = file.href;
            checkbox.dataset.isDirectory = file.isDirectory;
            checkbox.style.marginRight = '8px';
            checkbox.checked = false;

            const label = document.createElement('label');
            label.style.cssText = `
                flex: 1;
                cursor: pointer;
                font-size: 12px;
                color: #24292e;
                display: flex;
                align-items: center;
            `;

            const icon = document.createElement('span');
            icon.textContent = file.isDirectory ? '📁 ' : '📄 ';
            icon.style.marginRight = '4px';

            const nameSpan = document.createElement('span');
            nameSpan.textContent = file.name;
            nameSpan.title = file.name;
            nameSpan.style.overflow = 'hidden';
            nameSpan.style.textOverflow = 'ellipsis';
            nameSpan.style.whiteSpace = 'nowrap';

            label.appendChild(icon);
            label.appendChild(nameSpan);

            checkboxContainer.appendChild(checkbox);
            checkboxContainer.appendChild(label);
            container.appendChild(checkboxContainer);

            log(`渲染文件 ${index + 1}/${files.length}: ${file.name}`);
        });

        // 全选逻辑
        selectAllCheckbox.onchange = () => {
            log(`全选状态改变: ${selectAllCheckbox.checked}`);
            const checkboxes = container.querySelectorAll('.file-checkbox');
            checkboxes.forEach(cb => cb.checked = selectAllCheckbox.checked);
        };
    }

    // 获取选中的文件
    function getSelectedFiles() {
        const checkboxes = document.querySelectorAll('.file-checkbox:checked');
        const selected = Array.from(checkboxes).map(cb => ({
            href: cb.value,
            isDirectory: cb.dataset.isDirectory === 'true'
        }));

        log(`获取选中文件: 共 ${selected.length} 个`);
        selected.forEach((file, index) => {
            log(`  ${index + 1}. href=${file.href}, isDirectory=${file.isDirectory}`);
        });

        return selected;
    }

    // 下载文件内容
    async function downloadFileContent(url) {
        return new Promise((resolve, reject) => {
            log(`下载文件内容: ${url}`);

            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                timeout: 10000,
                onload: (response) => {
                    log(`响应状态: ${response.status}, 大小: ${response.responseText.length} 字节`);
                    
                    if (response.status === 200) {
                        log(`文件下载成功: ${url}`);
                        resolve(response.responseText);
                    } else if (response.status === 404) {
                        error(`文件不存在 (404): ${url}`);
                        reject(new Error(`文件不存在: ${url}`));
                    } else {
                        error(`下载失败 (${response.status}): ${url}`);
                        reject(new Error(`HTTP ${response.status}`));
                    }
                },
                onerror: (err) => {
                    error(`文件下载出错: ${url}, 错误: ${err}`);
                    reject(err);
                },
                ontimeout: () => {
                    error(`文件下载超时: ${url}`);
                    reject(new Error(`下载超时: ${url}`));
                }
            });
        });
    }

    // 获取原始文件 URL
    function getRawUrl(githubUrl) {
        // 将 /blob/ 或 /tree/ 转换为原始 URL
        const rawUrl = githubUrl
            .replace('github.com', 'raw.githubusercontent.com')
            .replace('/blob/', '/')
            .replace('/tree/', '/');

        log(`转换 URL: ${githubUrl} -> ${rawUrl}`);
        return rawUrl;
    }

    // 递归获取目录中的所有文件(带自动重试机制和 Token 支持)
    async function getFilesFromDirectory(dirPath, repoInfo, retryBranch = null, token = null) {
        log(`获取目录内容: ${dirPath}`);
        
        const branch = retryBranch || repoInfo.branch;
        const dirUrl = `https://api.github.com/repos/${repoInfo.owner}/${repoInfo.repo}/contents/${dirPath}?ref=${branch}`;
        log(`API URL: ${dirUrl}`);
        
        return new Promise((resolve, reject) => {
            const headers = {};
            if (token) {
                headers['Authorization'] = `token ${token}`;
            }
            
            GM_xmlhttpRequest({
                method: 'GET',
                url: dirUrl,
                headers: headers,
                timeout: 10000,
                onload: (response) => {
                    if (response.status === 200) {
                        try {
                            const items = JSON.parse(response.responseText);
                            log(`目录 ${dirPath} 包含 ${items.length} 项`);
                            resolve(items);
                        } catch (e) {
                            error(`解析 API 响应失败: ${e.message}`);
                            reject(e);
                        }
                    } else if (response.status === 404 && !retryBranch && branch === 'main') {
                        // 如果是 main 分支返回 404,尝试用 master 分支
                        log(`分支 'main' 返回 404,尝试 'master' 分支`);
                        getFilesFromDirectory(dirPath, repoInfo, 'master', token)
                            .then(resolve)
                            .catch(reject);
                    } else if (response.status === 403) {
                        // 403 通常是速率限制或权限问题
                        error(`获取目录失败 (403): ${dirUrl}`);
                        error(`响应头: ${JSON.stringify(response.responseHeaders)}`);
                        reject(new Error(`API 速率限制或权限不足 (403)`));
                    } else {
                        error(`获取目录失败 (${response.status}): ${dirUrl}`);
                        reject(new Error(`HTTP ${response.status}`));
                    }
                },
                onerror: (err) => {
                    error(`获取目录出错: ${dirPath}, 错误: ${err}`);
                    reject(err);
                },
                ontimeout: () => {
                    error(`获取目录超时: ${dirPath}`);
                    reject(new Error(`超时: ${dirPath}`));
                }
            });
        });
    }

    // 递归收集所有文件(包括子目录中的文件)
    async function collectAllFiles(items, repoInfo, depth = 0, token = null) {
        const allFiles = [];
        const maxDepth = 10; // 防止无限递归
        
        if (depth > maxDepth) {
            log(`达到最大递归深度 ${maxDepth},停止递归`);
            return allFiles;
        }
        
        for (const item of items) {
            if (item.type === 'file') {
                allFiles.push(item);
            } else if (item.type === 'dir') {
                log(`[深度 ${depth}] 递归处理子目录: ${item.path}`);
                try {
                    const subItems = await getFilesFromDirectory(item.path, repoInfo, null, token);
                    const subFiles = await collectAllFiles(subItems, repoInfo, depth + 1, token);
                    allFiles.push(...subFiles);
                } catch (e) {
                    // 404 或其他错误时,记录但继续处理其他目录
                    log(`[深度 ${depth}] 跳过子目录 ${item.path}: ${e.message}`);
                }
            }
        }
        
        return allFiles;
    }

    // 创建 ZIP 文件并下载
    async function createAndDownloadZip(files, repoInfo) {
        log('开始创建 ZIP 文件');
        log(`总共需要处理 ${files.length} 个文件/目录`);

        try {
            // 检查 JSZip 是否已加载
            if (typeof JSZip === 'undefined') {
                error('JSZip 库未加载');
                throw new Error('JSZip 库未加载,请稍后重试');
            }

            // 获取 GitHub Token
            const token = getGitHubToken();
            if (token) {
                log(`使用 GitHub Token 进行认证请求`);
            } else {
                log(`未使用 Token,使用未认证请求(限制 60 次/小时)`);
            }

            const zip = new JSZip();
            let fileCount = 0;
            let skipCount = 0;
            let errorCount = 0;

            // 收集所有需要下载的文件
            const filesToDownload = [];

            for (let i = 0; i < files.length; i++) {
                const file = files[i];
                try {
                    log(`[${i + 1}/${files.length}] 处理: ${file.href}`);

                    if (file.isDirectory) {
                        log(`[${i + 1}/${files.length}] 递归获取目录内容...`);
                        
                        // 从 href 中提取目录路径
                        const dirMatch = file.href.match(/\/tree\/[^\/]+\/(.*)$/);
                        const dirPath = dirMatch ? dirMatch[1] : '';
                        
                        if (!dirPath) {
                            log(`[${i + 1}/${files.length}] 目录路径为空,跳过`);
                            skipCount++;
                            continue;
                        }
                        
                        try {
                            const items = await getFilesFromDirectory(dirPath, repoInfo, null, token);
                            
                            // 递归收集所有文件(包括子目录)
                            const allDirFiles = await collectAllFiles(items, repoInfo, 0, token);
                            log(`[${i + 1}/${files.length}] 递归找到 ${allDirFiles.length} 个文件`);
                            
                            if (allDirFiles.length > 0) {
                                filesToDownload.push(...allDirFiles.map(item => ({
                                    name: item.name,
                                    path: item.path,
                                    downloadUrl: item.download_url
                                })));
                            } else {
                                log(`[${i + 1}/${files.length}] 目录为空`);
                                skipCount++;
                            }
                        } catch (e) {
                            error(`[${i + 1}/${files.length}] 获取目录失败: ${e.message}`);
                            skipCount++;
                        }
                        continue;
                    }

                    // 单个文件
                    const blobMatch = file.href.match(/\/blob\/[^\/]+\/(.+)$/);
                    const filePath = blobMatch ? blobMatch[1] : file.href.split('/').pop();
                    
                    filesToDownload.push({
                        name: file.name,
                        path: filePath,
                        href: file.href
                    });

                } catch (e) {
                    errorCount++;
                    error(`[${i + 1}/${files.length}] 处理失败: ${e.message}`);
                }
            }

            log(`总共需要下载 ${filesToDownload.length} 个文件`);

            // 下载所有文件(限制并发数为 3)
            const maxConcurrent = 3;
            for (let i = 0; i < filesToDownload.length; i += maxConcurrent) {
                const batch = filesToDownload.slice(i, i + maxConcurrent);
                const promises = batch.map(async (file, batchIndex) => {
                    const globalIndex = i + batchIndex;
                    try {
                        log(`[下载 ${globalIndex + 1}/${filesToDownload.length}] ${file.path}`);

                        let content;
                        if (file.downloadUrl) {
                            // 使用 GitHub API 的下载 URL
                            content = await downloadFileContent(file.downloadUrl);
                        } else {
                            // 使用 raw.githubusercontent.com
                            const fullUrl = `https://github.com${file.href}`;
                            const rawUrl = getRawUrl(fullUrl);
                            content = await downloadFileContent(rawUrl);
                        }

                        zip.file(file.path, content);
                        fileCount++;
                        log(`[下载 ${globalIndex + 1}/${filesToDownload.length}] ✓ 已添加`);

                    } catch (e) {
                        errorCount++;
                        error(`[下载 ${globalIndex + 1}/${filesToDownload.length}] 失败: ${e.message}`);
                    }
                });

                await Promise.all(promises);
                
                // 批次之间延迟 100ms,避免过多并发
                if (i + maxConcurrent < filesToDownload.length) {
                    await new Promise(resolve => setTimeout(resolve, 100));
                }
            }

            log(`处理完成 - 成功: ${fileCount}, 失败: ${errorCount}`);

            if (fileCount === 0) {
                throw new Error('没有成功添加任何文件到 ZIP');
            }

            // 生成 ZIP 文件
            log('正在生成 ZIP 文件...');
            log(`ZIP 中包含 ${fileCount} 个文件`);
            
            let zipContent;
            try {
                // 使用异步方式生成 ZIP,使用流式处理
                log('开始异步生成 ZIP...');
                
                const generatePromise = zip.generateAsync({ 
                    type: 'blob',
                    compression: 'DEFLATE',
                    compressionOptions: { level: 1 },  // 降低压缩级别以加快速度
                    streamFiles: true  // 启用流式处理
                });

                // 添加超时保护
                const timeoutPromise = new Promise((_, reject) => {
                    setTimeout(() => {
                        log('ZIP 生成超时(超过 20 秒)');
                        reject(new Error('ZIP 生成超时'));
                    }, 20000);
                });

                zipContent = await Promise.race([generatePromise, timeoutPromise]);
                log(`ZIP 文件生成完成,大小: ${(zipContent.size / 1024).toFixed(2)} KB`);
            } catch (e) {
                error(`生成 ZIP 失败: ${e.message}`);
                throw new Error(`无法生成 ZIP 文件: ${e.message}`);
            }

            // 下载 ZIP 文件
            const zipName = `${repoInfo.repo}-${repoInfo.branch}-${Date.now()}.zip`;
            log(`准备下载 ZIP: ${zipName}`);
            
            try {
                const url = URL.createObjectURL(zipContent);
                log(`ObjectURL 创建成功`);

                const a = document.createElement('a');
                a.href = url;
                a.download = zipName;
                document.body.appendChild(a);
                
                log('触发下载...');
                a.click();
                
                document.body.removeChild(a);
                
                // 延迟释放 URL,确保下载完成
                setTimeout(() => {
                    URL.revokeObjectURL(url);
                    log('ObjectURL 已释放');
                }, 500);

                log(`ZIP 文件下载完成: ${zipName}`);
                alert(`✅ 下载完成!\n文件: ${zipName}\n成功: ${fileCount}, 失败: ${errorCount}`);
            } catch (downloadErr) {
                error(`下载失败: ${downloadErr.message}`);
                throw new Error(`下载失败: ${downloadErr.message}`);
            }

        } catch (e) {
            error(`创建 ZIP 文件失败: ${e.message}`);
            alert(`❌ 下载失败: ${e.message}`);
        }
    }


    // 初始化脚本
    function init() {
        log('=== 脚本初始化开始 ===');

        const repoInfo = parseGitHubUrl();
        if (!repoInfo) {
            log('不是有效的 GitHub 仓库页面,脚本退出');
            return;
        }

        log(`✅ 已解析仓库信息 - 所有者: ${repoInfo.owner}, 仓库: ${repoInfo.repo}, 分支: ${repoInfo.branch}`);

        const { panel, fileListContainer, downloadBtn, refreshBtn, selectAllCheckbox, branchInfo, toggleBtn } = createControlPanel();

        // 立即更新分支信息显示
        branchInfo.textContent = `📌 分支: ${repoInfo.branch}`;
        branchInfo.title = `仓库: ${repoInfo.owner}/${repoInfo.repo}`;

        let isRefreshing = false;
        let lastRefreshTime = 0;

        // 刷新函数
        const refresh = () => {
            const now = Date.now();
            // 防止频繁刷新(500ms 内不重复刷新)
            if (isRefreshing || (now - lastRefreshTime < 500)) {
                log('刷新被跳过(防止频繁刷新)');
                return;
            }

            isRefreshing = true;
            lastRefreshTime = now;

            log('执行刷新操作');
            const files = getFileListFromPage();
            renderFileList(files, fileListContainer, selectAllCheckbox);

            isRefreshing = false;
        };

        // 初始刷新
        refresh();

        // 下载按钮事件
        downloadBtn.onclick = async () => {
            log('点击下载按钮');
            downloadBtn.disabled = true;
            downloadBtn.textContent = '⏳ 处理中...';

            try {
                const selectedFiles = getSelectedFiles();
                if (selectedFiles.length === 0) {
                    alert('请选择至少一个文件');
                    log('没有选中任何文件');
                    return;
                }

                await createAndDownloadZip(selectedFiles, repoInfo);
            } catch (e) {
                error(`下载过程出错: ${e.message}`);
                alert(`错误: ${e.message}`);
            } finally {
                downloadBtn.disabled = false;
                downloadBtn.textContent = '📥 下载为 ZIP';
            }
        };

        // 刷新按钮事件
        refreshBtn.onclick = refresh;

        log('=== 脚本初始化完成 ===');
    }

    // 等待页面加载完成后初始化
    if (isCodePage()) {
        log('检测到代码页面,准备初始化');
        if (document.readyState === 'loading') {
            log('页面仍在加载,等待 DOMContentLoaded 事件');
            document.addEventListener('DOMContentLoaded', init);
        } else {
            log('页面已加载,直接初始化');
            init();
        }
    } else {
        log('不是代码页面,脚本不启动');
    }

})();