// ==UserScript== // @name 学习通文档下载助手 // @namespace https://github.com/user/chaoxing-doc-downloader // @version 7.0 // @description 通过 /ananas/status API 获取真实下载链接,自动捕获文档信息 // @author You // @match *://mooc1.chaoxing.com/* // @match *://mooc2.chaoxing.com/* // @match *://mooc1-2.chaoxing.com/* // @match *://mooc2-1.chaoxing.com/* // @match *://*.*.chaoxing.com/* // @match *://*.*.mooc.chaoxing.com/* // @match *://*.*.zxx.chaoxing.com/* // @match *://*.*.superstar.com/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @connect chaoxing.com // @connect mooc.chaoxing.com // @connect cldisk.com // @connect s3.cldisk.com // @connect d0.cldisk.com // @connect cs.cldisk.com // @connect * // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; // ==================== 全局数据存储 ==================== let capturedDocs = []; // 已捕获的文档信息列表 let capturedFileIds = new Set(); /** * 拦截所有 XHR 请求,捕获 /ananas/status/{fileId} 响应 */ function interceptXHR() { if (window._chaoxing_xhr_intercepted) return; window._chaoxing_xhr_intercepted = true; const origXHROpen = XMLHttpRequest.prototype.open; const origXHRSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url, ...rest) { this._chaoxing_url = url ? String(url) : ''; this._chaoxing_method = method || 'GET'; return origXHROpen.apply(this, [method, url, ...rest]); }; XMLHttpRequest.prototype.send = function (...args) { const url = this._chaoxing_url || ''; // 监控 /ananas/status/ 请求 if (url.includes('/ananas/status/')) { this.addEventListener('load', () => { try { const resp = JSON.parse(this.responseText); if (resp && resp.status === 'success' && resp.objectid) { const fileId = resp.objectid; if (!capturedFileIds.has(fileId)) { capturedFileIds.add(fileId); capturedDocs.push({ fileId: fileId, filename: resp.filename || 'unknown.pdf', downloadUrl: resp.download || '', pdfUrl: resp.pdf || '', httpUrl: resp.http || '', length: resp.length || 0, pagenum: resp.pagenum || 0, }); console.log('[学习通下载v7] 捕获文档:', resp.filename); } } } catch (e) { /* ignore */ } }); } return origXHRSend.apply(this, args); }; } /** * 扫描 body 中的 file_ 模式来发现更多文件 */ function scanForFileIds() { const bodyHtml = document.body ? document.body.innerHTML || '' : ''; const fileMatches = bodyHtml.match(/file_([a-f0-9]{32})/gi); if (fileMatches) { fileMatches.forEach((m) => { const id = m.replace('file_', ''); if (!capturedFileIds.has(id)) { capturedFileIds.add(id); // 延迟调用 API 获取详细信息 getFileInfo(id); } }); } // 也扫描所有 script 标签 document.querySelectorAll('script').forEach((script) => { const text = script.textContent || ''; const matches = text.match(/file_([a-f0-9]{32})/gi); if (matches) { matches.forEach((m) => { const id = m.replace('file_', ''); if (!capturedFileIds.has(id)) { capturedFileIds.add(id); getFileInfo(id); } }); } }); } /** * 通过 API 获取文件信息 */ function getFileInfo(fileId) { if (!fileId || capturedFileIds.has(fileId)) { return Promise.resolve(null); } return new Promise((resolve) => { capturedFileIds.add(fileId); GM_xmlhttpRequest({ method: 'GET', url: `https://mooc1.chaoxing.com/ananas/status/${fileId}?flag=normal`, headers: { 'Referer': window.location.href, 'Accept': 'application/json', }, onload: function (response) { try { const data = JSON.parse(response.responseText); if (data && data.status === 'success') { capturedDocs.push({ fileId: data.objectid || fileId, filename: data.filename || `document_${fileId}.pdf`, downloadUrl: data.download || '', pdfUrl: data.pdf || '', httpUrl: data.http || '', length: data.length || 0, pagenum: data.pagenum || 0, }); console.log('[学习通下载v7] API 获取到:', data.filename); } resolve(data); } catch (e) { resolve(null); } }, onerror: function () { resolve(null); }, }); }); } /** * 下载文件 */ function downloadFile(url, filename) { if (!url) { alert('没有可用的下载链接!'); return; } console.log('[学习通下载v7] 开始下载:', filename); GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', headers: { 'Referer': 'https://mooc1.chaoxing.com/', }, onload: function (response) { if (response.status === 200 && response.response) { const blob = response.response; const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); console.log('[学习通下载v7] 下载成功!'); alert(`✅ "${filename}" 已开始下载`); } else if (response.status === 302 || response.status === 301) { const location = response.getResponseHeader('Location'); if (location) { window.open(location, '_blank'); alert('文件已在新窗口打开,请手动保存 (Ctrl+S)'); } else { window.open(url, '_blank'); alert('已在新的标签页中打开,请手动保存文件。'); } } else { console.log('[学习通下载v7] 下载失败 (status:', response.status, ')'); window.open(url, '_blank'); alert('下载失败,已在新的标签页中打开。请手动保存。'); } }, onerror: function () { window.open(url, '_blank'); alert('下载请求失败,已在新的标签页中打开。请手动保存。'); }, }); } // ==================== UI ==================== function createBtn() { const btn = document.createElement('button'); btn.id = 'chaoxing-dl-btn'; btn.textContent = '📥 下载文档'; btn.style.cssText = ` position: fixed; top: 20px; left: 20px; z-index: 999999; padding: 10px 16px; background: #4a90d9; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: bold; box-shadow: 0 2px 8px rgba(0,0,0,0.3); font-family: Microsoft YaHei, sans-serif; transition: all 0.3s ease; `; btn.addEventListener('mouseenter', () => { btn.style.background = '#357abd'; btn.style.transform = 'scale(1.05)'; }); btn.addEventListener('mouseleave', () => { btn.style.background = '#4a90d9'; btn.style.transform = 'scale(1)'; }); document.body.appendChild(btn); return btn; } function createPanel() { // 移除旧面板 const old = document.getElementById('chaoxing-dl-panel'); if (old) old.remove(); const panel = document.createElement('div'); panel.id = 'chaoxing-dl-panel'; panel.style.cssText = ` position: fixed; top: 65px; left: 20px; width: 480px; max-height: 65vh; z-index: 999998; background: #fff; border-radius: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); display: flex; flex-direction: column; overflow: hidden; font-family: Microsoft YaHei, sans-serif; `; // 头部 const header = document.createElement('div'); header.style.cssText = `padding: 12px 16px; background: #4a90d9; color: white; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0;`; header.innerHTML = ` 📄 找到 ${capturedDocs.length} 个文档 `; // 工具栏 const toolbar = document.createElement('div'); toolbar.style.cssText = `padding: 8px 12px; background: #f5f5f5; border-bottom: 1px solid #ddd; display: flex; gap: 8px; align-items: center; flex-shrink: 0;`; const selectAllCb = document.createElement('input'); selectAllCb.type = 'checkbox'; selectAllCb.id = 'chaoxing-select-all'; const selectAllLabel = document.createElement('label'); selectAllLabel.setAttribute('for', 'chaoxing-select-all'); selectAllLabel.style.cssText = 'font-size: 13px; color: #666; cursor: pointer;'; selectAllLabel.textContent = '全选'; const downloadAllBtn = document.createElement('button'); downloadAllBtn.textContent = '⬇ 下载全部'; downloadAllBtn.style.cssText = `margin-left: auto; padding: 4px 12px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: bold;`; toolbar.appendChild(selectAllCb); toolbar.appendChild(selectAllLabel); toolbar.appendChild(downloadAllBtn); // 列表 const listContainer = document.createElement('div'); listContainer.style.cssText = `flex: 1; overflow-y: auto; padding: 8px 0;`; panel.appendChild(header); panel.appendChild(toolbar); panel.appendChild(listContainer); document.body.appendChild(panel); // 事件 document.getElementById('chaoxing-panel-close').onclick = () => panel.remove(); selectAllCb.addEventListener('change', () => { listContainer.querySelectorAll('input[type="checkbox"]').forEach((cb) => { cb.checked = selectAllCb.checked; }); }); downloadAllBtn.onclick = () => { const checked = listContainer.querySelectorAll('input[type="checkbox"]:checked'); if (checked.length === 0) { alert('请至少勾选一个文档'); return; } checked.forEach((cb, i) => { const idx = parseInt(cb.dataset.index); const doc = capturedDocs[idx]; if (doc) { const url = doc.downloadUrl || doc.pdfUrl; setTimeout(() => { if (url) downloadFile(url, doc.filename); }, i * 800); } }); }; // 渲染 if (capturedDocs.length === 0) { listContainer.innerHTML = `