Greasy Fork

WHUT教学平台UMOOC PDF下载器

【正式版】一款专为武汉理工大学教学平台(jxpt.whut.edu.cn)设计的PDF下载助手。拥有现代化UI、全自动后台扫描、智能识别页面类型、精准解析真实下载地址、支持批量下载和自由拖拽等特性,提供极致的用户体验。

// ==UserScript==
// @name         WHUT教学平台UMOOC PDF下载器
// @name:en      WHUT JXPT PDF Downloader
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  【正式版】一款专为武汉理工大学教学平台(jxpt.whut.edu.cn)设计的PDF下载助手。拥有现代化UI、全自动后台扫描、智能识别页面类型、精准解析真实下载地址、支持批量下载和自由拖拽等特性,提供极致的用户体验。
// @description:en [Official Release] A PDF download helper for WHUT's teaching platform (jxpt.whut.edu.cn). Features a modern UI, automatic background scanning, smart page type detection, accurate real URL parsing, batch download support, and a draggable button for the ultimate user experience.
// @author       毫厘
// @match        https://jxpt.whut.edu.cn/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=whut.edu.cn
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    let isInitialized = false;
    let scanningInProgress = false;
    let cachedFiles = [];
    let isDragging = false;
    let dragOffset = { x: 0, y: 0 };

    // 防抖函数,避免重复初始化
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    // 优化的监控逻辑
    const checkAndInit = debounce(() => {
        try {
            const mainFrame = document.querySelector('frame[name="mainFrame"]');
            if (!mainFrame || isInitialized) return;

            const doc = mainFrame.contentDocument;
            if (!doc || !doc.body || doc.getElementById('pdf-floating-btn')) return;

            isInitialized = true;
            initializeModernDownloader(mainFrame);
        } catch (error) {
            // Silently handle cross-origin errors
        }
    }, 500);

    setInterval(checkAndInit, 2000);

    /**
     * 现代化UI和智能扫描系统
     */
    async function initializeModernDownloader(frame) {
        const doc = frame.contentDocument;

        // 1. 注入现代化CSS样式
        injectModernStyles(doc);

        // 2. 创建现代化UI组件
        createModernUI(doc);

        // 3. 绑定交互事件
        bindUIEvents(doc, frame);

        // 4. 启动智能扫描
        await performIntelligentScan(frame, doc);
    }

    function injectModernStyles(doc) {
        const style = doc.createElement('style');
        style.textContent = `
            /* 主按钮 - 现代化设计,自由拖拽 */
            #pdf-floating-btn {
                position: fixed;
                top: 30px;
                right: 30px;
                z-index: 10000;
                width: 60px;
                height: 60px;
                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                color: white;
                border: none;
                border-radius: 50%;
                cursor: grab;
                font-size: 28px;
                box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
                transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
                display: flex;
                align-items: center;
                justify-content: center;
                user-select: none;
                backdrop-filter: blur(10px);
                border: 2px solid rgba(255, 255, 255, 0.1);
            }

            #pdf-floating-btn:hover {
                transform: scale(1.05);
                box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5);
                background: linear-gradient(135deg, #7c8ef7 0%, #8a5fb8 100%);
            }

            #pdf-floating-btn.dragging {
                cursor: grabbing;
                transform: scale(1.1);
                box-shadow: 0 10px 30px rgba(102, 126, 234, 0.7);
                transition: none;
                z-index: 10001;
            }

            #pdf-floating-btn.scanning {
                animation: pulse 2s infinite;
            }

            @keyframes pulse {
                0% { box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3); }
                50% { box-shadow: 0 8px 30px rgba(102, 126, 234, 0.6); }
                100% { box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3); }
            }

            /* 计数标记 - 优化位置和样式 */
            #pdf-count-badge {
                position: absolute;
                top: -5px;
                right: -5px;
                background: linear-gradient(135deg, #ff4757 0%, #ff3838 100%);
                color: white;
                border-radius: 50%;
                min-width: 22px;
                height: 22px;
                font-size: 11px;
                font-weight: bold;
                display: flex;
                align-items: center;
                justify-content: center;
                transform: scale(0);
                transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
                pointer-events: none;
                border: 2px solid white;
                box-shadow: 0 2px 8px rgba(255, 71, 87, 0.4);
            }

            #pdf-count-badge.show {
                transform: scale(1);
            }

            /* 侧边栏 - 重新设计布局 */
            #pdf-modern-sidebar {
                position: fixed;
                top: 0;
                right: -450px;
                width: 420px;
                height: 100vh;
                z-index: 9999;
                background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
                box-shadow: -8px 0 40px rgba(0, 0, 0, 0.12);
                transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
                display: flex;
                flex-direction: column;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                border-left: 1px solid rgba(226, 232, 240, 0.8);
            }

            #pdf-modern-sidebar.visible {
                transform: translateX(-450px);
            }

            /* 遮罩层 */
            #pdf-overlay {
                display: none;
                position: fixed;
                top: 0;
                left: 0;
                width: 100vw;
                height: 100vh;
                background: rgba(15, 23, 42, 0.5);
                z-index: 9998;
                backdrop-filter: blur(4px);
                transition: opacity 0.3s ease;
            }

            #pdf-modern-sidebar.visible + #pdf-overlay {
                display: block;
            }

            /* 头部区域 - 重新设计 */
            .pdf-header {
                padding: 28px 24px 24px;
                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                color: white;
                position: relative;
                border-bottom: 1px solid rgba(255, 255, 255, 0.1);
            }

            .pdf-header::before {
                content: '';
                position: absolute;
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                background: radial-gradient(circle at 30% 20%, rgba(255,255,255,0.1) 0%, transparent 50%);
            }

            .pdf-header h2 {
                margin: 0 0 8px 0;
                font-size: 22px;
                font-weight: 700;
                position: relative;
                letter-spacing: -0.5px;
            }

            .pdf-status {
                font-size: 14px;
                opacity: 0.95;
                position: relative;
                font-weight: 500;
            }

            .pdf-close-btn {
                position: absolute;
                top: 24px;
                right: 24px;
                background: rgba(255, 255, 255, 0.15);
                border: none;
                color: white;
                width: 36px;
                height: 36px;
                border-radius: 50%;
                cursor: pointer;
                font-size: 20px;
                transition: all 0.3s;
                display: flex;
                align-items: center;
                justify-content: center;
                backdrop-filter: blur(10px);
            }

            .pdf-close-btn:hover {
                background: rgba(255, 255, 255, 0.25);
                transform: rotate(90deg);
            }

            /* 进度条区域 - 更清晰的视觉层次 */
            .pdf-progress {
                background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
                color: #92400e;
                padding: 16px 24px;
                font-size: 14px;
                border-left: 4px solid #f59e0b;
                margin: 0;
                font-weight: 500;
                display: flex;
                align-items: center;
                border-bottom: 1px solid rgba(245, 158, 11, 0.2);
            }

            /* 内容区域 - 优化滚动和间距 */
            .pdf-content {
                flex: 1;
                overflow-y: auto;
                padding: 0;
                background: #fafbfc;
            }

            .pdf-content::-webkit-scrollbar {
                width: 6px;
            }

            .pdf-content::-webkit-scrollbar-track {
                background: transparent;
            }

            .pdf-content::-webkit-scrollbar-thumb {
                background: #cbd5e1;
                border-radius: 3px;
            }

            .pdf-content::-webkit-scrollbar-thumb:hover {
                background: #94a3b8;
            }

            /* 文件列表 - 更好的视觉组织 */
            .pdf-file-list {
                list-style: none;
                margin: 0;
                padding: 16px 0;
            }

            .pdf-file-item {
                display: flex;
                align-items: center;
                padding: 18px 24px;
                margin: 0 16px 12px;
                background: white;
                border-radius: 12px;
                box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
                transition: all 0.3s;
                border: 1px solid rgba(226, 232, 240, 0.6);
            }

            .pdf-file-item:hover {
                transform: translateY(-2px);
                box-shadow: 0 8px 25px rgba(15, 23, 42, 0.12);
                border-color: rgba(102, 126, 234, 0.3);
            }

            .pdf-file-icon {
                width: 44px;
                height: 44px;
                background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
                border-radius: 10px;
                display: flex;
                align-items: center;
                justify-content: center;
                color: white;
                font-weight: bold;
                font-size: 12px;
                margin-right: 16px;
                flex-shrink: 0;
                box-shadow: 0 4px 12px rgba(239, 68, 68, 0.25);
            }

            .pdf-file-info {
                flex: 1;
                min-width: 0;
                margin-right: 12px;
            }

            .pdf-file-name {
                font-weight: 600;
                color: #1e293b;
                margin-bottom: 6px;
                font-size: 14px;
                line-height: 1.4;
                display: -webkit-box;
                -webkit-line-clamp: 2;
                -webkit-box-orient: vertical;
                overflow: hidden;
            }

            .pdf-file-size {
                font-size: 12px;
                color: #64748b;
                font-weight: 500;
            }

            .pdf-download-single {
                background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
                color: white;
                border: none;
                padding: 10px 18px;
                border-radius: 8px;
                font-size: 12px;
                font-weight: 600;
                cursor: pointer;
                transition: all 0.3s;
                box-shadow: 0 3px 10px rgba(59, 130, 246, 0.3);
                min-width: 70px;
                display: flex;
                align-items: center;
                justify-content: center;
            }

            .pdf-download-single:hover:not(:disabled) {
                transform: translateY(-1px);
                box-shadow: 0 6px 16px rgba(59, 130, 246, 0.4);
                background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
            }

            .pdf-download-single:disabled {
                background: #94a3b8;
                cursor: not-allowed;
                transform: none;
                box-shadow: none;
            }

            /* 操作按钮区域 - 更突出的CTA */
            .pdf-actions {
                padding: 24px;
                background: white;
                border-top: 1px solid #e2e8f0;
                box-shadow: 0 -4px 20px rgba(15, 23, 42, 0.05);
            }

            .pdf-download-all {
                width: 100%;
                padding: 16px;
                background: linear-gradient(135deg, #10b981 0%, #059669 100%);
                color: white;
                border: none;
                border-radius: 12px;
                font-size: 16px;
                font-weight: 700;
                cursor: pointer;
                transition: all 0.3s;
                box-shadow: 0 4px 16px rgba(16, 185, 129, 0.3);
                letter-spacing: 0.5px;
                text-transform: uppercase;
                position: relative;
                overflow: hidden;
            }

            .pdf-download-all::before {
                content: '';
                position: absolute;
                top: 0;
                left: -100%;
                width: 100%;
                height: 100%;
                background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
                transition: left 0.5s;
            }

            .pdf-download-all:hover:not(:disabled)::before {
                left: 100%;
            }

            .pdf-download-all:hover:not(:disabled) {
                transform: translateY(-2px);
                box-shadow: 0 8px 25px rgba(16, 185, 129, 0.4);
                background: linear-gradient(135deg, #059669 0%, #047857 100%);
            }

            .pdf-download-all:disabled {
                background: linear-gradient(135deg, #94a3b8 0%, #64748b 100%);
                cursor: not-allowed;
                transform: none;
                box-shadow: none;
            }

            /* 加载状态 */
            .loading-spinner {
                display: inline-block;
                width: 16px;
                height: 16px;
                border: 2px solid rgba(255, 255, 255, 0.3);
                border-radius: 50%;
                border-top-color: #ffffff;
                animation: spin 1s ease-in-out infinite;
                margin-right: 8px;
            }

            @keyframes spin {
                to { transform: rotate(360deg); }
            }

            /* 空状态 - 更友好的设计 */
            .pdf-empty-state {
                text-align: center;
                padding: 60px 24px;
                color: #64748b;
            }

            .pdf-empty-icon {
                font-size: 64px;
                margin-bottom: 20px;
                opacity: 0.6;
                filter: grayscale(0.3);
            }

            .pdf-empty-text {
                font-size: 16px;
                font-weight: 500;
                margin-bottom: 8px;
                color: #475569;
            }

            .pdf-empty-hint {
                font-size: 14px;
                color: #94a3b8;
                line-height: 1.5;
            }

            /* 响应式适配 */
            @media (max-width: 480px) {
                #pdf-modern-sidebar {
                    width: 100vw;
                    right: -100vw;
                }

                #pdf-modern-sidebar.visible {
                    transform: translateX(-100vw);
                }

                .pdf-file-item {
                    margin: 0 8px 8px;
                    padding: 14px 16px;
                }

                .pdf-header {
                    padding: 20px 16px 16px;
                }

                .pdf-actions {
                    padding: 16px;
                }
            }
        `;
        doc.head.appendChild(style);
    }

    function createModernUI(doc) {
        // 主浮动按钮
        const floatingBtn = doc.createElement('button');
        floatingBtn.id = 'pdf-floating-btn';
        floatingBtn.innerHTML = '📄';
        floatingBtn.className = 'scanning';
        floatingBtn.title = '点击打开PDF下载器,拖拽可移动位置';
        doc.body.appendChild(floatingBtn);

        // 计数标记
        const countBadge = doc.createElement('div');
        countBadge.id = 'pdf-count-badge';
        countBadge.textContent = '0';
        floatingBtn.appendChild(countBadge);

        // 侧边栏
        const sidebar = doc.createElement('div');
        sidebar.id = 'pdf-modern-sidebar';
        sidebar.innerHTML = `
            <div class="pdf-header">
                <h2>📄 PDF 文档中心</h2>
                <div class="pdf-status" id="pdf-status">正在扫描...</div>
                <button class="pdf-close-btn" id="pdf-close-btn">×</button>
            </div>
            <div class="pdf-progress" id="pdf-progress" style="display: none;">
                <span class="loading-spinner"></span>
                <span id="progress-text">扫描中...</span>
            </div>
            <div class="pdf-content">
                <ul class="pdf-file-list" id="pdf-file-list"></ul>
            </div>
            <div class="pdf-actions">
                <button class="pdf-download-all" id="pdf-download-all" disabled>
                    📥 下载全部文档
                </button>
            </div>
        `;
        doc.body.appendChild(sidebar);

        // 遮罩层
        const overlay = doc.createElement('div');
        overlay.id = 'pdf-overlay';
        doc.body.appendChild(overlay);
    }

    function bindUIEvents(doc, frame) {
        const floatingBtn = doc.getElementById('pdf-floating-btn');
        const sidebar = doc.getElementById('pdf-modern-sidebar');
        const overlay = doc.getElementById('pdf-overlay');
        const closeBtn = doc.getElementById('pdf-close-btn');
        const downloadAllBtn = doc.getElementById('pdf-download-all');
        const fileList = doc.getElementById('pdf-file-list');

        // 简化的拖拽功能 - 移除吸附逻辑
        let clickStartTime = 0;
        let hasMoved = false;

        function getEventPos(e) {
            return {
                x: e.type.includes('touch') ? e.touches[0].clientX : e.clientX,
                y: e.type.includes('touch') ? e.touches[0].clientY : e.clientY
            };
        }

        function startDrag(e) {
            clickStartTime = Date.now();
            hasMoved = false;
            isDragging = true;

            const pos = getEventPos(e);
            const rect = floatingBtn.getBoundingClientRect();

            dragOffset.x = pos.x - rect.left;
            dragOffset.y = pos.y - rect.top;

            floatingBtn.classList.add('dragging');
            e.preventDefault();
        }

        function doDrag(e) {
            if (!isDragging) return;

            hasMoved = true;
            const pos = getEventPos(e);

            let newX = pos.x - dragOffset.x;
            let newY = pos.y - dragOffset.y;

            // 边界限制
            const margin = 10;
            const maxX = window.innerWidth - floatingBtn.offsetWidth - margin;
            const maxY = window.innerHeight - floatingBtn.offsetHeight - margin;

            newX = Math.max(margin, Math.min(newX, maxX));
            newY = Math.max(margin, Math.min(newY, maxY));

            floatingBtn.style.left = newX + 'px';
            floatingBtn.style.top = newY + 'px';
            floatingBtn.style.right = 'auto';
            floatingBtn.style.bottom = 'auto';

            e.preventDefault();
        }

        function endDrag(e) {
            if (!isDragging) return;

            isDragging = false;
            floatingBtn.classList.remove('dragging');

            // 如果是点击而非拖拽,则打开侧边栏
            const clickDuration = Date.now() - clickStartTime;
            if (!hasMoved && clickDuration < 300) {
                sidebar.classList.toggle('visible');
            }
        }

        // 事件绑定
        floatingBtn.addEventListener('mousedown', startDrag);
        doc.addEventListener('mousemove', doDrag);
        doc.addEventListener('mouseup', endDrag);

        floatingBtn.addEventListener('touchstart', startDrag, { passive: false });
        doc.addEventListener('touchmove', doDrag, { passive: false });
        doc.addEventListener('touchend', endDrag, { passive: false });

        floatingBtn.addEventListener('contextmenu', e => e.preventDefault());

        // 关闭侧边栏
        [overlay, closeBtn].forEach(el => {
            el.addEventListener('click', () => {
                sidebar.classList.remove('visible');
            });
        });

        // 全部下载
        downloadAllBtn.addEventListener('click', async (e) => {
            if (cachedFiles.length === 0) return;

            e.target.innerHTML = '<span class="loading-spinner"></span>正在下载...';
            e.target.disabled = true;

            await downloadFiles(doc, cachedFiles);

            setTimeout(() => {
                e.target.textContent = '下载全部文档';
                e.target.disabled = false;
            }, 3000);
        });

        // 单个文件下载
        fileList.addEventListener('click', async (e) => {
            if (e.target.classList.contains('pdf-download-single')) {
                const index = parseInt(e.target.dataset.index);
                const file = cachedFiles[index];
                if (file) {
                    e.target.innerHTML = '<span class="loading-spinner"></span>';
                    e.target.disabled = true;

                    await downloadFiles(doc, [file]);

                    setTimeout(() => {
                        e.target.textContent = '下载';
                        e.target.disabled = false;
                    }, 2000);
                }
            }
        });
    }

    async function performIntelligentScan(frame, doc) {
        if (scanningInProgress) return;
        scanningInProgress = true;

        updateScanStatus(doc, '正在扫描...', true);

        try {
            const files = await optimizedUnifiedScraper(frame, doc);
            cachedFiles = files;

            updateUI(doc, files);
            updateScanStatus(doc, `找到 ${files.length} 个文档`, false);

        } catch (error) {
            console.error('扫描失败:', error);
            updateScanStatus(doc, '扫描失败', false);
        } finally {
            scanningInProgress = false;
            doc.getElementById('pdf-floating-btn').classList.remove('scanning');
        }
    }

    function updateScanStatus(doc, message, showProgress) {
        const statusEl = doc.getElementById('pdf-status');
        const progressEl = doc.getElementById('pdf-progress');
        const progressText = doc.getElementById('progress-text');

        if (statusEl) statusEl.textContent = message;
        if (progressText) progressText.textContent = message;
        if (progressEl) progressEl.style.display = showProgress ? 'block' : 'none';
    }

    function updateUI(doc, files) {
        const countBadge = doc.getElementById('pdf-count-badge');
        const fileList = doc.getElementById('pdf-file-list');
        const downloadAllBtn = doc.getElementById('pdf-download-all');

        // 更新计数标记
        countBadge.textContent = files.length > 99 ? '99+' : files.length;
        countBadge.className = files.length > 0 ? 'show' : '';

        // 更新文件列表
        if (files.length === 0) {
            fileList.innerHTML = `
                <div class="pdf-empty-state">
                    <div class="pdf-empty-icon">📄</div>
                    <div class="pdf-empty-text">暂无PDF文档</div>
                    <div class="pdf-empty-hint">当前页面没有找到可下载的PDF文档<br>请尝试切换到其他页面或等待内容加载</div>
                </div>
            `;
        } else {
            fileList.innerHTML = files.map((file, index) => `
                <li class="pdf-file-item">
                    <div class="pdf-file-icon">PDF</div>
                    <div class="pdf-file-info">
                        <div class="pdf-file-name" title="${file.fileName}">${file.fileName}</div>
                        <div class="pdf-file-size">PDF文档 • 点击下载</div>
                    </div>
                    <button class="pdf-download-single" data-index="${index}">下载</button>
                </li>
            `).join('');
        }

        // 更新下载按钮状态
        downloadAllBtn.disabled = files.length === 0;
        if (files.length > 0) {
            downloadAllBtn.innerHTML = `📥 下载全部文档 <span style="opacity: 0.8;">(${files.length})</span>`;
        } else {
            downloadAllBtn.textContent = '📥 暂无可下载文档';
        }
    }

    // 优化的扫描器
    async function optimizedUnifiedScraper(frame, doc) {
        const collectedPdfs = new Map();
        let usingPagination = false;
        let pageNum = 1;
        const maxPages = 10; // 限制最大页数防止无限循环

        updateScanStatus(doc, '扫描当前页面...', true);

        // 扫描分页
        while (pageNum <= maxPages) {
            const currentDoc = frame.contentDocument;
            findPdfsOnCurrentPage(currentDoc, collectedPdfs);

            const nextPageLink = findNextPageLink(currentDoc);
            if (nextPageLink && pageNum < maxPages) {
                usingPagination = true;
                updateScanStatus(doc, `扫描第 ${pageNum + 1} 页...`, true);
                await navigateAndAwaitLoad(frame, nextPageLink);
                pageNum++;

                // 小延迟确保页面加载完成
                await new Promise(r => setTimeout(r, 800));
            } else {
                break;
            }
        }

        // 如果没有分页,尝试滚动加载
        if (!usingPagination) {
            updateScanStatus(doc, '扫描动态内容...', true);
            await performScrollScan(frame, collectedPdfs, doc);
        }

        return Array.from(collectedPdfs.values());
    }

    async function performScrollScan(frame, collectedPdfs, doc) {
        let lastSize = -1;
        let scrollAttempts = 0;
        const maxScrollAttempts = 5;

        while (collectedPdfs.size > lastSize && scrollAttempts < maxScrollAttempts) {
            lastSize = collectedPdfs.size;

            const scrollElement = frame.contentDocument.scrollingElement || frame.contentDocument.documentElement;
            scrollElement.scrollTop = scrollElement.scrollHeight;

            updateScanStatus(doc, `滚动扫描... 已找到 ${collectedPdfs.size} 个文档`, true);

            await new Promise(r => setTimeout(r, 1200));
            findPdfsOnCurrentPage(frame.contentDocument, collectedPdfs);
            scrollAttempts++;
        }
    }

    function findPdfsOnCurrentPage(doc, collection) {
        const links = doc.querySelectorAll('a[href*="download_preview.jsp"]');

        links.forEach(link => {
            let realDownloadHref = '';
            try {
                const url = new URL(link.href);
                const resId = url.searchParams.get('resid');
                const lid = url.searchParams.get('lid');

                if (resId && lid) {
                    realDownloadHref = `${url.protocol}//${url.host}/meol/analytics/resPdfShow.do?resId=${resId}&lid=${lid}`;
                } else {
                    return;
                }
            } catch (e) {
                console.error("解析链接失败:", link.href, e);
                return;
            }

            const fromText = link.textContent.trim();
            const fromTitle = link.title.trim();
            let fileName = fromText || fromTitle;
            if (fileName && !fileName.toLowerCase().endsWith('.pdf')) {
                fileName += '.pdf';
            }
            if (!fileName) {
                 fileName = (new URL(link.href).searchParams.get('resid') || 'unnamed') + '.pdf';
            }

            if (realDownloadHref && !collection.has(realDownloadHref)) {
                collection.set(realDownloadHref, { href: realDownloadHref, fileName: fileName });
            }
        });
    }

    // -- 其他辅助函数 --
    function findNextPageLink(doc) {
        const t = ['下一页', 'Next', '›', '>'];
        const a = doc.querySelectorAll('a');
        for (const l of a) {
            if (t.includes(l.innerText.trim())) return l;
        }
        const i = doc.querySelector('img[alt="下一页"], img[title="下一页"]');
        return i ? i.closest('a') : null;
    }

    function navigateAndAwaitLoad(frame, target) {
        return new Promise(r => {
            const h = () => {
                frame.removeEventListener('load', h);
                r();
            };
            frame.addEventListener('load', h);
            target.click();
        });
    }

    async function downloadFiles(doc, files) {
        for (const file of files) {
            try {
                const a = doc.createElement('a');
                a.href = file.href;
                a.download = file.fileName;
                doc.body.appendChild(a);
                a.click();
                doc.body.removeChild(a);
            } catch (e) {
                console.error(`下载 ${file.fileName} 时失败:`, e);
            }
            await new Promise(resolve => setTimeout(resolve, 600));
        }
    }
})();