GitHub Downloader

在 GitHub 代码页面添加下载功能,支持选择性下载文件和目录为 ZIP 格式,支持递归下载子目录,以及可拖拽的智能按钮交互

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         GitHub Downloader
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  在 GitHub 代码页面添加下载功能,支持选择性下载文件和目录为 ZIP 格式,支持递归下载子目录,以及可拖拽的智能按钮交互
// @author       Penn (based on lly-ke's script)
// @match        https://github.com/*
// @icon         data:image/svg+xml;utf8,%3Csvg%20t%3D%221770727408678%22%20class%3D%22icon%22%20viewBox%3D%220%200%201024%201024%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20p-id%3D%2220636%22%20width%3D%22128%22%20height%3D%22128%22%3E%3Cpath%20d%3D%22M0%20525.027556C0%20635.747556%2031.829333%20735.288889%2095.488%20823.637333c63.658667%2088.348444%20146.147556%20150.215111%20247.466667%20185.571556%204.024889%200.668444%207.352889%200.995556%2010.026666%200.995555%202.673778%200%204.835556-0.497778%206.499556-1.507555%201.664-0.995556%202.986667-1.991111%203.982222-3.000889%201.009778-0.995556%201.692444-2.673778%202.033778-4.992%200.341333-2.332444%200.512-3.982222%200.512-4.992v-100.551111c-24.661333%202.659556-46.677333%202.488889-66.019556-0.512-19.342222-3.015111-34.517333-7.68-45.511111-14.023111a117.176889%20117.176889%200%200%201-28.984889-23.495111c-8.334222-9.329778-14.008889-17.834667-16.981333-25.514667-2.986667-7.68-5.987556-15.687111-9.002667-24.007111-3.000889-8.334222-4.835556-13.169778-5.504-14.506667-5.987556-10.012444-14.990222-19.171556-27.008-27.505778-12.003556-8.32-21.006222-14.990222-27.008-20.010666-5.987556-5.006222-6.656-9.841778-2.005333-14.506667%2033.336889-17.351111%2070.997333%204.679111%20112.981333%2066.048%2022.684444%2034.019556%2062.350222%2044.032%20119.011556%2030.037333a142.648889%20142.648889%200%200%201%2039.992889-70.044444c-77.326222-13.994667-134.656-42.680889-171.989334-86.044444-37.333333-43.377778-56.007111-96.071111-56.007111-158.08%200-58.026667%2018.346667-108.373333%2055.011556-151.068445-14.677333-43.349333-12.672-89.031111%206.016-137.045333%2019.328-1.351111%2040.988444%202.488889%2064.995555%2011.491555%2023.992889%209.002667%2040.817778%2016.682667%2050.488889%2023.011556%209.671111%206.343111%2018.161778%2012.188444%2025.500445%2017.521778%2038.001778-10.666667%2080.824889-16.014222%20128.512-16.014223%2047.672889%200%2090.837333%205.333333%20129.493333%2016.014223a757.845333%20757.845333%200%200%201%2028.999111-19.015111c10.666667-6.684444%2026.993778-13.852444%2048.981334-21.518223%2022.001778-7.665778%2042.325333-10.837333%2060.999111-9.514666%2018.005333%2047.36%2020.337778%2092.387556%206.997333%20135.068444%2037.333333%2042.680889%2056.007111%2093.041778%2056.007111%20151.068445%200%2062.008889-19.000889%20114.872889-56.988444%20158.549333-38.001778%2043.690667-95.331556%2072.206222-172.003556%2085.546667%2028.672%2028.672%2043.008%2063.36%2043.008%20104.035555v129.038222c0%200.668444%200.341333%201.664%200.995556%203.015111%200%204.010667%200.170667%207.011556%200.512%208.988445%200.341333%201.991111%201.848889%203.982222%204.508444%206.016%202.673778%202.033778%206.670222%203.029333%2012.003556%203.015111%20101.987556-34.673778%20185.486222-96.540444%20250.481777-185.571556C991.488%20736.597333%201024%20636.373333%201024%20524.999111c0-69.361778-13.511111-135.722667-40.504889-199.082667-27.022222-63.374222-63.345778-117.888-108.999111-163.555555-45.653333-45.667556-100.138667-82.019556-163.484444-109.027556A502.016%20502.016%200%200%200%20512.014222%2012.8c-69.333333%200-135.665778%2013.511111-198.997333%2040.533333-63.345778%2027.022222-117.831111%2063.36-163.484445%20109.027556-45.653333%2045.667556-81.976889%20100.181333-108.984888%20163.555555A502.599111%20502.599111%200%200%200%200.028444%20524.999111L0%20525.027556z%22%20fill%3D%22%23000000%22%20p-id%3D%2220637%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        unsafeWindow
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.9.1/jszip.min.js
// @run-at       document-end
// @license      MIT
// ==/UserScript==
// 
// 本脚本基于原作者 lly-ke 的开源脚本进行二次开发
// 原始脚本:https://greasyfork.org/scripts/556352
// 
// 主要修改:
// - 智能按钮交互(拖拽、边缘吸附、自动隐藏)
// - 调试模式及自动刷新
//
// This script is modified from lly-ke's original work
// Original: https://greasyfork.org/scripts/556352
//
// Major changes:
// - Smart button interaction (drag, edge snap, auto-hide)
// - Auto-refresh debug mode

(function() {
    'use strict';

    // 全局日志开关(使用 GM_getValue 持久化存储,默认关闭)
    // 使用方法:通过右键菜单 "🔧 切换调试模式" 开启/关闭
    const DEBUG = GM_getValue('debug_enabled', false);

    const log = (msg) => {
        if (DEBUG) {
            console.log(`[GitHub Downloader] ${new Date().toLocaleTimeString()}: ${msg}`);
        }
    };

    const error = (msg) => {
        if (DEBUG) {
            console.error(`[GitHub Downloader] ${new Date().toLocaleTimeString()}: ${msg}`);
        }
    };

    // ==================== 智能按钮交互系统 ====================

    // 配置常量
    const CONFIG = {
        SNAP_THRESHOLD: 20,
        HIDE_DELAY: 400,
        ANIMATION_DURATION: 200,
        HIDE_RATIO: 0.5,
        BUTTON_SIZE: 50,
        PANEL_WIDTH: 300,
        PANEL_HEIGHT: 400,
        PANEL_GAP: 10,
        DRAG_THRESHOLD: 3,
        SUPPORTED_EDGES: ['left', 'right'],
        DEFAULT_RIGHT: 20,
        DEFAULT_BOTTOM: 30
    };

    // 状态对象
    const state = {
        x: null,
        y: null,
        docked: false,
        edge: null,
        hidden: false,
        isDragging: false,
        hasDragged: false,
        dragStartX: 0,
        dragStartY: 0,
        buttonStartX: 0,
        buttonStartY: 0,
        isMouseOver: false,
        panelOpen: false,
        panelDirection: null,
        autoHideEnabled: true,
        hideTimer: null
    };

    // 全局引用
    let toggleBtn = null;
    let controlPanel = null;

    /**
     * 保存按钮状态到持久化存储
     * @function saveState
     * @returns {void}
     */
    function saveState() {
        try {
            const stateToSave = {
                x: state.x,
                y: state.y,
                docked: state.docked,
                edge: state.edge,
                autoHideEnabled: state.autoHideEnabled
            };
            GM_setValue('buttonState', JSON.stringify(stateToSave));
            log('状态已保存');
        } catch (err) {
            error('保存状态失败: ' + err.message);
        }
    }

    /**
     * 从持久化存储加载按钮状态
     * @function loadState
     * @returns {void}
     */
    function loadState() {
        try {
            const savedState = GM_getValue('buttonState', '');
            if (savedState) {
                const parsed = JSON.parse(savedState);
                Object.assign(state, parsed);
                log('状态已加载: ' + JSON.stringify(parsed));
            } else {
                log('没有保存的状态,使用默认设置');
                state.autoHideEnabled = true;
            }
        } catch (err) {
            error('加载状态失败: ' + err.message);
            // 使用默认状态
            state.x = null;
            state.y = null;
            state.docked = false;
            state.edge = null;
            state.hidden = false;
            state.autoHideEnabled = true;
        }
    }

    /**
     * 应用加载的状态到按钮 DOM 元素
     * @function applyState
     * @returns {void}
     */
    function applyState() {
        if (!toggleBtn) return;

        if (state.x !== null && state.y !== null) {
            toggleBtn.style.left = `${state.x}px`;
            toggleBtn.style.top = `${state.y}px`;
            toggleBtn.style.right = 'auto';
            toggleBtn.style.bottom = 'auto';
            log(`应用状态 - 位置: (${state.x}, ${state.y})`);
        }

        if (state.docked && state.edge) {
            applyDocked(state.edge);
        }
    }

    /**
     * 重置按钮到默认位置和状态
     * @function resetButton
     * @returns {void}
     */
    function resetButton() {
        // 清除存储
        GM_setValue('buttonState', '');

        // 重置状态
        state.x = null;
        state.y = null;
        state.docked = false;
        state.edge = null;
        state.hidden = false;
        state.autoHideEnabled = true;

        if (toggleBtn) {
            // 重置按钮样式
            toggleBtn.style.left = 'auto';
            toggleBtn.style.top = 'auto';
            toggleBtn.style.right = `${CONFIG.DEFAULT_RIGHT}px`;
            toggleBtn.style.bottom = `${CONFIG.DEFAULT_BOTTOM}px`;
            toggleBtn.style.transform = 'translateX(0) translateY(0)';
            toggleBtn.classList.remove('docked');
        }

        // 关闭面板
        if (controlPanel) {
            controlPanel.style.display = 'none';
            state.panelOpen = false;
        }

        log('按钮已重置到默认位置');
    }

    /**
     * 限制按钮位置在可视区域内
     * @function clampPosition
     * @param {number} x - 目标水平位置
     * @param {number} y - 目标垂直位置
     * @returns {Object} 限制后的坐标 {x, y}
     */
    function clampPosition(x, y) {
        const viewport = {
            width: window.innerWidth,
            height: window.innerHeight
        };

        return {
            x: Math.max(0, Math.min(x, viewport.width - CONFIG.BUTTON_SIZE)),
            y: Math.max(0, Math.min(y, viewport.height - CONFIG.BUTTON_SIZE))
        };
    }

    /**
     * 限制面板位置在可视区域内
     * @function clampPanelPosition
     * @param {number} x - 目标水平位置
     * @param {number} y - 目标垂直位置
     * @returns {Object} 限制后的坐标 {x, y}
     */
    function clampPanelPosition(x, y) {
        const viewport = {
            width: window.innerWidth,
            height: window.innerHeight
        };

        return {
            x: Math.max(0, Math.min(x, viewport.width - CONFIG.PANEL_WIDTH)),
            y: Math.max(0, Math.min(y, viewport.height - CONFIG.PANEL_HEIGHT))
        };
    }

    /**
     * 检测按钮是否接近边缘并应吸附
     * @function checkEdgeSnap
     * @param {number} x - 当前水平位置
     * @param {number} y - 当前垂直位置
     * @returns {string|null} 吸附的边缘 ('left' 或 'right'),无吸附返回 null
     */
    function checkEdgeSnap(x, y) {
        const viewport = {
            width: window.innerWidth,
            height: window.innerHeight
        };

        const distances = {
            left: x,
            right: viewport.width - x - CONFIG.BUTTON_SIZE,
            top: y,
            bottom: viewport.height - y - CONFIG.BUTTON_SIZE
        };

        let snappedEdge = null;
        for (const edge of CONFIG.SUPPORTED_EDGES) {
            if (distances[edge] <= CONFIG.SNAP_THRESHOLD) {
                snappedEdge = edge;
                break;
            }
        }

        state.edge = snappedEdge;
        return snappedEdge;
    }

    /**
     * 应用停靠状态到按钮
     * @function applyDocked
     * @param {string} edge - 停靠边缘 ('left' 或 'right')
     * @returns {void}
     */
    function applyDocked(edge) {
        if (!toggleBtn) return;

        state.docked = true;
        state.edge = edge;
        toggleBtn.classList.add('docked');

        switch (edge) {
            case 'left':
                toggleBtn.style.left = '0';
                toggleBtn.style.right = 'auto';
                break;
            case 'right':
                toggleBtn.style.right = '0';
                toggleBtn.style.left = 'auto';
                break;
        }

        // 延迟启动自动隐藏
        if (state.autoHideEnabled) {
            cancelHide();
            setTimeout(() => {
                if (state.docked && !state.panelOpen) {
                    scheduleHide();
                }
            }, 100);
        }

        log(`按钮已停靠到: ${edge}`);
    }

    /**
     * 清除按钮的停靠状态
     * @function clearDocked
     * @returns {void}
     */
    function clearDocked() {
        if (!toggleBtn) return;

        state.docked = false;
        state.edge = null;
        state.hidden = false;

        toggleBtn.classList.remove('docked');
        toggleBtn.style.transform = 'translateX(0) translateY(0)';

        cancelHide();
        log('停靠状态已清除');
    }

    /**
     * 安排按钮自动隐藏(延迟执行)
     * @function scheduleHide
     * @returns {void}
     */
    function scheduleHide() {
        if (!state.autoHideEnabled || !state.docked || state.panelOpen || !toggleBtn) {
            return;
        }

        cancelHide();
        state.hideTimer = setTimeout(() => {
            hideButton();
        }, CONFIG.HIDE_DELAY);
    }

    /**
     * 取消待执行的自动隐藏
     * @function cancelHide
     * @returns {void}
     */
    function cancelHide() {
        if (state.hideTimer) {
            clearTimeout(state.hideTimer);
            state.hideTimer = null;
        }
    }

    /**
     * 隐藏按钮(向边缘平移50%)
     * @function hideButton
     * @returns {void}
     */
    function hideButton() {
        if (state.hidden || !state.docked || !toggleBtn) return;

        state.hidden = true;
        toggleBtn.style.transition = `transform ${CONFIG.ANIMATION_DURATION}ms ease`;

        switch (state.edge) {
            case 'left':
                toggleBtn.style.transform = `translateX(-${CONFIG.BUTTON_SIZE * CONFIG.HIDE_RATIO}px)`;
                break;
            case 'right':
                toggleBtn.style.transform = `translateX(${CONFIG.BUTTON_SIZE * CONFIG.HIDE_RATIO}px)`;
                break;
        }

        log('按钮已隐藏');
    }

    /**
     * 显示按钮(恢复到完全可见)
     * @function showButton
     * @returns {void}
     */
    function showButton() {
        if (!state.hidden || !toggleBtn) return;

        state.hidden = false;
        toggleBtn.style.transform = 'translateX(0) translateY(0)';
        log('按钮已显示');
    }

    /**
     * 计算面板的最佳展开方向
     * @function calculatePanelDirection
     * @returns {string} 方向标识符 ('up-left', 'up-right', 'down-left', 'down-right')
     */
    function calculatePanelDirection() {
        if (!toggleBtn) return 'up-left';

        const btnRect = toggleBtn.getBoundingClientRect();
        const viewport = {
            width: window.innerWidth,
            height: window.innerHeight
        };

        const centerX = btnRect.left + btnRect.width / 2;
        const centerY = btnRect.top + btnRect.height / 2;

        const viewCenterX = viewport.width / 2;
        const viewCenterY = viewport.height / 2;

        const inLeftHalf = centerX < viewCenterX;
        const inTopHalf = centerY < viewCenterY;

        const space = {
            left: btnRect.left,
            right: viewport.width - btnRect.right,
            top: btnRect.top,
            bottom: viewport.height - btnRect.bottom
        };

        // 水平方向选择
        let horizontal;
        if (inLeftHalf) {
            horizontal = space.right >= CONFIG.PANEL_WIDTH ? 'right' : 'left';
        } else {
            horizontal = space.left >= CONFIG.PANEL_WIDTH ? 'left' : 'right';
        }

        // 垂直方向选择
        let vertical;
        if (inTopHalf) {
            vertical = space.bottom >= CONFIG.PANEL_HEIGHT ? 'down' : 'up';
        } else {
            vertical = space.top >= CONFIG.PANEL_HEIGHT ? 'up' : 'down';
        }

        return `${vertical}-${horizontal}`;
    }

    /**
     * 更新控制面板的位置
     * @function updatePanelPosition
     * @returns {void}
     */
    function updatePanelPosition() {
        if (!controlPanel || !toggleBtn || !state.panelOpen) return;

        const direction = calculatePanelDirection();
        const btnRect = toggleBtn.getBoundingClientRect();

        let panelLeft, panelTop;

        switch (direction) {
            case 'up-left':
                panelLeft = btnRect.left - CONFIG.PANEL_WIDTH - CONFIG.PANEL_GAP;
                panelTop = btnRect.top - CONFIG.PANEL_HEIGHT - CONFIG.PANEL_GAP;
                break;
            case 'up-right':
                panelLeft = btnRect.right + CONFIG.PANEL_GAP;
                panelTop = btnRect.top - CONFIG.PANEL_HEIGHT - CONFIG.PANEL_GAP;
                break;
            case 'down-left':
                panelLeft = btnRect.left - CONFIG.PANEL_WIDTH - CONFIG.PANEL_GAP;
                panelTop = btnRect.bottom + CONFIG.PANEL_GAP;
                break;
            case 'down-right':
                panelLeft = btnRect.right + CONFIG.PANEL_GAP;
                panelTop = btnRect.bottom + CONFIG.PANEL_GAP;
                break;
        }

        const clamped = clampPanelPosition(panelLeft, panelTop);
        controlPanel.style.left = `${clamped.x}px`;
        controlPanel.style.top = `${clamped.y}px`;
        controlPanel.style.bottom = 'auto';
        controlPanel.style.right = 'auto';

        state.panelDirection = direction;
        log(`面板位置更新: ${direction}, (${clamped.x}, ${clamped.y})`);
    }

    /**
     * 鼠标按下事件处理(开始拖拽)
     * @function onMouseDown
     * @param {MouseEvent} e - 鼠标事件对象
     * @returns {void}
     */
    function onMouseDown(e) {
        if (e.button !== 0 || !toggleBtn) return;

        state.isDragging = true;
        state.hasDragged = false;
        state.dragStartX = e.clientX;
        state.dragStartY = e.clientY;
        state.buttonStartX = toggleBtn.offsetLeft;
        state.buttonStartY = toggleBtn.offsetTop;

        toggleBtn.classList.add('dragging');
        log('拖拽开始');
    }

    /**
     * 鼠标移动事件处理(拖拽中)
     * @function onMouseMove
     * @param {MouseEvent} e - 鼠标事件对象
     * @returns {void}
     */
    function onMouseMove(e) {
        if (!state.isDragging || !toggleBtn) return;

        e.preventDefault();

        const deltaX = e.clientX - state.dragStartX;
        const deltaY = e.clientY - state.dragStartY;

        // 检测是否发生了有效拖拽
        if (!state.hasDragged) {
            if (Math.abs(deltaX) > CONFIG.DRAG_THRESHOLD ||
                Math.abs(deltaY) > CONFIG.DRAG_THRESHOLD) {
                state.hasDragged = true;
                clearDocked();
                log('检测到有效拖拽');
            }
        }

        if (state.hasDragged) {
            let newX = state.buttonStartX + deltaX;
            let newY = state.buttonStartY + deltaY;
            const clamped = clampPosition(newX, newY);

            toggleBtn.style.left = `${clamped.x}px`;
            toggleBtn.style.top = `${clamped.y}px`;
            toggleBtn.style.right = 'auto';
            toggleBtn.style.bottom = 'auto';

            state.x = clamped.x;
            state.y = clamped.y;

            checkEdgeSnap(clamped.x, clamped.y);

            // 如果面板打开,实时更新位置
            if (state.panelOpen) {
                updatePanelPosition();
            }
        }
    }

    /**
     * 鼠标释放事件处理(结束拖拽)
     * @function onMouseUp
     * @param {MouseEvent} e - 鼠标事件对象
     * @returns {void}
     */
    function onMouseUp(e) {
        if (!state.isDragging || !toggleBtn) return;

        state.isDragging = false;
        toggleBtn.classList.remove('dragging');

        if (state.edge) {
            applyDocked(state.edge);
        }

        saveState();
        log('拖拽结束');

        // 延迟重置 hasDragged 标志,确保 click 事件能检测到拖拽
        setTimeout(() => {
            state.hasDragged = false;
        }, 0);
    }

    /**
     * 鼠标进入按钮区域处理
     * @function onMouseEnter
     * @returns {void}
     */
    function onMouseEnter() {
        state.isMouseOver = true;
        cancelHide();
        showButton();
    }

    /**
     * 鼠标离开按钮区域处理
     * @function onMouseLeave
     * @returns {void}
     */
    function onMouseLeave() {
        state.isMouseOver = false;
        if (state.docked && !state.panelOpen) {
            scheduleHide();
        }
    }

    /**
     * 窗口大小变化事件处理
     * @function onWindowResize
     * @returns {void}
     */
    function onWindowResize() {
        if (!toggleBtn) return;

        const rect = toggleBtn.getBoundingClientRect();
        const viewport = {
            width: window.innerWidth,
            height: window.innerHeight
        };

        // 检查按钮是否超出边界
        if (rect.right > viewport.width || rect.bottom > viewport.height ||
            rect.left < 0 || rect.top < 0) {
            log('窗口大小变化,按钮超出边界,重置到默认位置');
            resetButton();
        }

        // 如果面板打开,更新位置
        if (state.panelOpen) {
            updatePanelPosition();
        }
    }

    /**
     * 切换自动隐藏功能开关
     * @function toggleAutoHide
     * @returns {void}
     */
    function toggleAutoHide() {
        state.autoHideEnabled = !state.autoHideEnabled;

        if (state.autoHideEnabled) {
            alert('✅ 自动隐藏已启用');
            if (state.docked) {
                scheduleHide();
            }
        } else {
            alert('❌ 自动隐藏已禁用');
            cancelHide();
            showButton();
        }

        saveState();
        log(`自动隐藏已${state.autoHideEnabled ? '启用' : '禁用'}`);
    }

    /**
     * 显示当前按钮状态信息
     * @function showCurrentState
     * @returns {void}
     */
    function showCurrentState() {
        const info = `
📊 当前按钮状态
═══════════════════════════════════

位置:
  X: ${state.x !== null ? state.x + 'px' : '未设置'}
  Y: ${state.y !== null ? state.y + 'px' : '未设置'}

停靠:
  是否停靠: ${state.docked ? '是' : '否'}
  停靠边缘: ${state.edge || '无'}
  是否隐藏: ${state.hidden ? '是' : '否'}

面板:
  是否打开: ${state.panelOpen ? '是' : '否'}
  展开方向: ${state.panelDirection || '-'}

配置:
  自动隐藏: ${state.autoHideEnabled ? '启用' : '禁用'}

═══════════════════════════════════
        `.trim();

        alert(info);
    }

    /**
     * 切换调试日志模式
     * @function toggleDebug
     * @returns {void}
     */
    function toggleDebug() {
        const current = GM_getValue('debug_enabled', false);
        const next = !current;
        GM_setValue('debug_enabled', next);

        if (next) {
            alert('✅ 调试模式已开启\n\n页面将自动刷新以应用设置');
        } else {
            alert('❌ 调试模式已关闭\n\n页面将自动刷新以应用设置');
        }

        // 延迟 500ms 后自动刷新页面,使设置立即生效
        setTimeout(() => {
            window.location.reload();
        }, 500);
    }

    /**
     * 注册所有右键菜单命令
     * @function registerMenuCommands
     * @returns {void}
     */
    function registerMenuCommands() {
        if (typeof GM_registerMenuCommand !== 'function') {
            log('GM_registerMenuCommand 不可用,跳过菜单注册');
            return;
        }

        // 1. 重置按钮位置
        GM_registerMenuCommand('🔄 重置按钮位置', function() {
            if (confirm('确定要重置按钮到默认位置吗?')) {
                resetButton();
            }
        });

        // 2. 切换自动隐藏
        GM_registerMenuCommand('⏱️ 切换自动隐藏', function() {
            toggleAutoHide();
        });

        // 3. 查看当前状态
        GM_registerMenuCommand('📊 查看当前状态', function() {
            showCurrentState();
        });

        // 4. 清除状态存储
        GM_registerMenuCommand('🗑️ 清除状态存储', function() {
            if (confirm('确定要清除所有状态存储吗?\n\n这将删除按钮位置和设置,按钮将恢复到默认位置。')) {
                resetButton();
                alert('✅ 状态存储已清除');
            }
        });

        // 5. 切换调试模式
        GM_registerMenuCommand('🔧 切换调试模式', function() {
            toggleDebug();
        });

        log('菜单命令已注册');
    }

    /**
     * 打开控制面板
     * @function openPanel
     * @returns {void}
     */
    function openPanel() {
        if (!controlPanel || !toggleBtn) return;

        state.panelOpen = true;
        controlPanel.style.display = 'block';

        // 取消隐藏计时器
        cancelHide();
        showButton();

        // 更新面板位置
        updatePanelPosition();

        log('面板已打开');
    }

    /**
     * 关闭控制面板
     * @function closePanel
     * @returns {void}
     */
    function closePanel() {
        if (!controlPanel) return;

        state.panelOpen = false;
        controlPanel.style.display = 'none';

        // 如果需要,重新安排隐藏
        if (state.docked && !state.isMouseOver && state.autoHideEnabled) {
            scheduleHide();
        }

        log('面板已关闭');
    }

    /**
     * 按钮点击事件处理(打开/关闭面板)
     * @function onButtonClick
     * @param {MouseEvent} e - 鼠标事件对象
     * @returns {void}
     */
    function onButtonClick(e) {
        // 如果发生了拖拽,不触发点击
        if (state.hasDragged) {
            e.preventDefault();
            e.stopPropagation();
            log('拖拽后的点击被阻止');
            return;
        }

        // 切换面板
        if (state.panelOpen) {
            closePanel();
        } else {
            openPanel();
        }
    }

    // 检查是否是代码页面
    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;
        }

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

        // 添加拖拽相关事件
        toggleBtn.addEventListener('mousedown', onMouseDown);
        toggleBtn.addEventListener('mouseenter', onMouseEnter);
        toggleBtn.addEventListener('mouseleave', onMouseLeave);
        toggleBtn.addEventListener('click', onButtonClick);

        // 添加全局事件监听
        document.addEventListener('mousemove', onMouseMove);
        document.addEventListener('mouseup', onMouseUp);
        window.addEventListener('resize', onWindowResize);

        toggleBtn.onmouseover = () => {
            if (!state.isDragging) {
                toggleBtn.style.background = '#0256c7';
                toggleBtn.style.transform = state.hidden ? 'scale(1.1)' : 'scale(1.1)';
            }
        };
        toggleBtn.onmouseout = () => {
            if (!state.isDragging) {
                toggleBtn.style.background = '#0366d6';
                if (!state.hidden) {
                    toggleBtn.style.transform = 'scale(1)';
                }
            }
        };

        // 主面板(默认隐藏)
        controlPanel = document.createElement('div');
        controlPanel.id = panelId;
        controlPanel.style.cssText = `
            position: fixed;
            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: ${CONFIG.PANEL_WIDTH}px;
            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);
                }
            }

            /* 拖拽状态 */
            #github-zip-toggle-btn.dragging {
                cursor: grabbing;
                box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
                transform: scale(1.1);
                transition: none;
            }

            /* 停靠状态 */
            #github-zip-toggle-btn.docked {
                transition: transform 0.2s ease;
            }

            /* 悬停效果 - 停靠时不改变transform */
            #github-zip-toggle-btn.docked:hover {
                transform: translateX(0) translateY(0) scale(1.1);
            }

            #github-zip-toggle-btn.docked[style*="translateX(-25px)"]:hover,
            #github-zip-toggle-btn.docked[style*="translateX(25px)"]:hover {
                transform: translateX(0) translateY(0) scale(1.1);
            }
        `;
        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 = () => {
            closePanel();
        };

        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);

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

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

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

        return { panel: controlPanel, 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();

        // 加载并应用保存的状态
        loadState();
        applyState();

        // 注册菜单命令
        registerMenuCommands();

        // 立即更新分支信息显示
        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('不是代码页面,脚本不启动');
    }

})();