// ==UserScript== // @name CNKI Batch Downloader (Bilingual) // @name:zh-CN 知网CNKI论文PDF批量下载-双语版 // @namespace https://greasyfork.org/zh-CN/users/236397-hust-hzb // @version 1.0 // @icon https://www.cnki.net/favicon.ico // @description Batch download CNKI papers/theses PDF (Bilingual, Smart monitoring, Auto verification) // @description:zh-CN 知网文献、硕博论文PDF批量下载 (中英双语,智能驻守,自动核对) // @author HUST HuangZhenbin // @license MIT // @match *://*.cnki.net/* // @run-at document-idle // @grant unsafeWindow // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant GM_download // @grant GM_registerMenuCommand // @downloadURL https://update.greasyfork.icu/scripts/560374/CNKI%20Batch%20Downloader%20%28Bilingual%29.user.js // @updateURL https://update.greasyfork.icu/scripts/560374/CNKI%20Batch%20Downloader%20%28Bilingual%29.meta.js // ==/UserScript== (function() { 'use strict'; // --- 配置与状态 --- let useWebVPN = GM_getValue('useWebVPN', false); // 默认语言检测:如果浏览器语言包含 'zh' 则为 'zh',否则为 'en' const defaultLang = navigator.language.includes('zh') ? 'zh' : 'en'; let currentLang = GM_getValue('cnki_lang', defaultLang); const DEFAULT_MIN_DELAY = 5000; const DEFAULT_MAX_DELAY = 10000; const DEFAULT_FOLDER = "CNKI_Downloads"; let isRunning = false; let lastCheckedIndex = null; // --- 国际化文本字典 --- const i18n = { zh: { title: "📚 CNKI 批量下载助手", version: "v1.0", close: "关闭", guide_title: "使用前请务必检查以下配置,否则无法自动下载:", guide_browser: "浏览器设置:请关闭“下载前询问每个文件的保存位置”(设置 -> 下载 -> 询问保存位置 -> 关)。", guide_tamper: "油猴权限:请允许 Tampermonkey 扩展访问“管理下载”权限(扩展管理 -> 详情 -> 允许访问文件URL/下载)。", guide_overwrite: "文件去重:下载时若本地存在同名文件,脚本将直接替换覆盖。", mask_title: "任务已暂停:需要人工验证", mask_desc: "检测到知网验证码拦截。
已为您自动打开验证窗口,请在新窗口中手动点击下载并完成滑块验证
验证成功且开始下载后,关闭那个窗口,回来点击下方按钮。", btn_resume: "✅ 我已解除,继续下载", btn_stop_task: "⏹ 停止任务", report_title: "📊 下载结果核对报告", report_retry: "🔄 重试失败项目", report_close: "关闭报告", label_folder: "📂 归档文件夹:", label_vpn: "开启 WebVPN 模式", btn_scan: "🔍 1. 扫描当前页", btn_start: "▶ 2. 开始下载选中", btn_verify: "🛠️ 仅打开验证页", btn_clear: "🗑 清空列表", btn_reset_history: "🧹 清除历史记录", tip_shift: "💡 提示: 按住 Shift 键点击复选框可进行批量多选。文件名纯净,同名文件将自动覆盖。", th_check: "选", th_no: "No.", th_title: "标题", th_author: "作者/来源", th_status: "状态", status_wait: "待下载", status_done: "✔ 完成", status_error: "✘ 失败", status_pay: "💰 需付费", status_nopdf: "⚪ 无PDF", status_exists: "🔁 已存在", status_running: "⟳ 解析中...", status_downloading: "⬇ 下载中...", status_skip: "⚠ 跳过", status_ready: "等待操作...", status_stopped: "🚫 任务已手动停止", status_scanned: "扫描完成,新增 {new} 条,共 {total} 条。", status_total: "列表共 {total} 条文献", status_finished: "✅ 批量任务完成", alert_no_item: "未找到文献,请确保在搜索结果页", alert_no_check: "请先勾选需要下载的文献", alert_history_clear: "确定要清除所有下载历史记录吗?\n这将导致脚本不再跳过同名文件。", alert_history_done: "历史记录已清除。", report_success: "成功", report_exists: "已存在", report_pay: "需付费", report_nopdf: "无PDF", report_fail: "失败", report_msg_check: "请检查以下失败项目:", report_msg_none: "🎉 没有下载失败的项目", main_btn: "CNKI批量导出", err_captcha: "触发验证码", err_no_auth: "无权限/收费", err_download_fail: "下载失败", cool_down: "冷却" }, en: { title: "📚 CNKI Batch Downloader", version: "v1.0", close: "Close", guide_title: "Please check the following configurations before use:", guide_browser: "Browser: Disable 'Ask where to save each file before downloading' (Settings -> Downloads).", guide_tamper: "Tampermonkey: Allow 'Manage Downloads' permission (Extension Management -> Details).", guide_overwrite: "Overwrite: If a file with the same name exists locally, it will be overwritten.", mask_title: "Task Paused: Manual Verification Required", mask_desc: "CNKI CAPTCHA detected.
A verification window has been opened. Please manually click download and solve the slider in the new window.
After success, close that window and click the button below.", btn_resume: "✅ I've Solved it, Continue", btn_stop_task: "⏹ Stop Task", report_title: "📊 Download Result Report", report_retry: "🔄 Retry Failed Items", report_close: "Close Report", label_folder: "📂 Folder:", label_vpn: "Enable WebVPN Mode", btn_scan: "🔍 1. Scan Page", btn_start: "▶ 2. Start Download", btn_verify: "🛠️ Open Verify Page", btn_clear: "🗑 Clear List", btn_reset_history: "🧹 Reset History", tip_shift: "💡 Tip: Hold Shift to select multiple items. Filenames are clean and will overwrite duplicates.", th_check: "chk", th_no: "No.", th_title: "Title", th_author: "Author/Source", th_status: "Status", status_wait: "Waiting", status_done: "✔ Done", status_error: "✘ Failed", status_pay: "💰 Pay Req", status_nopdf: "⚪ No PDF", status_exists: "🔁 Exists", status_running: "⟳ Parsing...", status_downloading: "⬇ Downloading...", status_skip: "⚠ Skipped", status_ready: "Ready...", status_stopped: "🚫 Task Stopped", status_scanned: "Scanned, added {new}, total {total}.", status_total: "Total {total} items", status_finished: "✅ Batch Task Completed", alert_no_item: "No papers found. Please use on search result page.", alert_no_check: "Please select items first.", alert_history_clear: "Are you sure to clear download history?\nThis will cause re-downloading of existing files.", alert_history_done: "History cleared.", report_success: "Success", report_exists: "Exists", report_pay: "Pay Req", report_nopdf: "No PDF", report_fail: "Failed", report_msg_check: "Please check failed items:", report_msg_none: "🎉 No failed items", main_btn: "CNKI Export", err_captcha: "Captcha Triggered", err_no_auth: "No Auth/Paid", err_download_fail: "Download Failed", cool_down: "Cooldown" } }; function t(key) { return i18n[currentLang][key] || key; } function toggleLang() { currentLang = currentLang === 'zh' ? 'en' : 'zh'; GM_setValue('cnki_lang', currentLang); // 移除旧界面重新渲染 const overlay = document.getElementById('cnki-overlay'); if (overlay) overlay.remove(); openDashboard(); } // --- CSS --- function injectStyle() { if (document.getElementById('cnki-style')) return; const style = document.createElement('style'); style.id = 'cnki-style'; style.textContent = ` .cnki-ui-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); backdrop-filter: blur(3px); z-index: 99999; display: flex; justify-content: center; align-items: center; } .cnki-ui-modal { background: #fff; width: 950px; height: 90vh; border-radius: 12px; box-shadow: 0 15px 40px rgba(0,0,0,0.25); display: flex; flex-direction: column; overflow: hidden; font-family: "Microsoft YaHei", sans-serif; animation: fadeIn 0.3s ease; position: relative;} @keyframes fadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } .cnki-ui-header { padding: 15px 25px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; background: #f8f9fa; } .cnki-ui-title { font-size: 18px; font-weight: bold; color: #333; display: flex; align-items: center; gap: 8px; } .cnki-ui-close { cursor: pointer; border: none; background: none; font-size: 24px; color: #999; transition: color 0.2s; } .cnki-ui-close:hover { color: #333; } .cnki-lang-btn { font-size: 12px; background: #e0f2fe; color: #0284c7; border: 1px solid #bae6fd; padding: 2px 8px; border-radius: 4px; cursor: pointer; margin-right: 10px; } .cnki-config-guide { background: #fff1f2; border-bottom: 1px solid #fecdd3; padding: 12px 25px; font-size: 13px; color: #881337; line-height: 1.6; display: flex; gap: 10px; align-items: flex-start; } .cnki-guide-icon { font-size: 18px; } .cnki-guide-list { margin: 0; padding-left: 20px; } .cnki-ui-toolbar { padding: 15px 25px; border-bottom: 1px solid #eee; background: #fff; display: flex; flex-direction: column; gap: 12px; } .cnki-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; } .cnki-ui-btn { padding: 8px 16px; border-radius: 6px; border: 1px solid #ddd; background: #fff; cursor: pointer; font-size: 13px; font-weight: 500; transition: all 0.2s; display: flex; align-items: center; gap: 5px; } .cnki-ui-btn:hover { background: #f3f4f6; transform: translateY(-1px); } .cnki-ui-btn:active { transform: translateY(0); } .cnki-btn-primary { background: #3b82f6; color: #fff; border-color: #3b82f6; box-shadow: 0 2px 5px rgba(59,130,246,0.3); } .cnki-btn-primary:hover { background: #2563eb; } .cnki-btn-warn { background: #f59e0b; color: #fff; border-color: #f59e0b; } .cnki-btn-warn:hover { background: #d97706; } .cnki-btn-danger { background: #ef4444; color: #fff; border-color: #ef4444; } .cnki-btn-danger:hover { background: #dc2626; } .cnki-btn-info { background: #0ea5e9; color: #fff; border-color: #0ea5e9; } .cnki-btn-info:hover { background: #0284c7; } .cnki-input-group { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #555; background: #f9fafb; padding: 5px 10px; border-radius: 6px; border: 1px solid #e5e7eb; } .cnki-input { padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; width: 160px; font-size: 13px; } .cnki-input:focus { border-color: #3b82f6; outline: none; } .cnki-table-wrap { flex: 1; overflow-y: auto; padding: 0; background: #fdfdfd; } .cnki-table { width: 100%; border-collapse: collapse; font-size: 13px; } .cnki-table th { position: sticky; top: 0; background: #f1f5f9; padding: 12px 15px; text-align: left; color: #475569; font-weight: 600; border-bottom: 1px solid #e2e8f0; z-index: 10; } .cnki-table td { padding: 10px 15px; border-bottom: 1px solid #f1f5f9; color: #334155; vertical-align: middle; } .cnki-table tr:hover { background: #f8fafc; } .cnki-row-selected { background: #eff6ff !important; } .cnki-footer { padding: 10px 25px; border-top: 1px solid #eee; background: #f8f9fa; font-size: 12px; color: #666; display: flex; justify-content: space-between; align-items: center; } .cnki-orcid-link { color: #a3d014; text-decoration: none; display: flex; align-items: center; gap: 5px; font-weight: bold; } .cnki-orcid-link:hover { text-decoration: underline; } .cnki-pause-mask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.95); z-index: 20; display: none; flex-direction: column; justify-content: center; align-items: center; gap: 20px; } .cnki-pause-box { background: white; padding: 30px; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.1); border: 1px solid #eee; text-align: center; max-width: 450px; } /* 结果报告遮罩 */ .cnki-report-mask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 30; display: none; justify-content: center; align-items: center; } .cnki-report-box { background: white; width: 650px; max-height: 85%; border-radius: 12px; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 20px 50px rgba(0,0,0,0.3); animation: fadeIn 0.2s ease; } .cnki-report-header { padding: 20px; background: #f0fdf4; border-bottom: 1px solid #dcfce7; } .cnki-report-header.has-error { background: #fef2f2; border-bottom: 1px solid #fee2e2; } .cnki-report-list { flex: 1; overflow-y: auto; padding: 20px; } .cnki-report-item { padding: 10px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; font-size: 13px; align-items: center; } .cnki-report-item:last-child { border-bottom: none; } .cnki-report-status-fail { color: #dc2626; font-weight: bold; background: #fef2f2; padding: 2px 6px; border-radius: 4px; font-size: 12px;} .cnki-report-status-pay { color: #b45309; font-weight: bold; background: #fffbeb; padding: 2px 6px; border-radius: 4px; font-size: 12px;} .cnki-report-status-nopdf { color: #6b7280; font-weight: bold; background: #f3f4f6; padding: 2px 6px; border-radius: 4px; font-size: 12px;} .cnki-report-btn { padding: 15px; text-align: right; border-top: 1px solid #eee; background: #fff; display: flex; justify-content: flex-end; gap: 10px;} /* 主按钮 */ .cnki-main-btn { position: fixed; bottom: 60px; right: 40px; padding: 12px 20px; border-radius: 50px; background: #3b82f6; color: white; border: none; box-shadow: 0 4px 15px rgba(59,130,246,0.4); cursor: pointer; z-index: 2147483647 !important; display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: bold; transition: all 0.2s; } .cnki-main-btn:hover { transform: translateY(-2px); background: #2563eb; } .cnki-status-wait { color: #94a3b8; } .cnki-status-run { color: #3b82f6; font-weight: bold; } .cnki-status-ok { color: #16a34a; font-weight: bold; } .cnki-status-err { color: #ef4444; font-weight: bold; } .cnki-status-pay { color: #f59e0b; font-weight: bold; } .cnki-status-nopdf { color: #6b7280; font-weight: bold; } .cnki-status-exists { color: #9ca3af; font-weight: bold; } `; document.head.appendChild(style); } // --- 核心入口:智能驻守 (轮询检测) --- function tryCreateButton() { if (document.getElementById('cnki-main-btn')) return; // 只要是知网搜索页就显示 const currentURL = window.location.href; if (currentURL.includes('defaultresult') || currentURL.includes('advsearch') || currentURL.includes('search') || currentURL.includes('kns8s')) { const btn = document.createElement('button'); btn.id = 'cnki-main-btn'; btn.className = 'cnki-main-btn'; btn.innerHTML = ` ${t('main_btn')}`; btn.title = t('title'); btn.onclick = openDashboard; document.body.appendChild(btn); } } // 使用 MutationObserver 监听 DOM 变化 const observer = new MutationObserver(() => { tryCreateButton(); }); function startObserver() { const targetNode = document.body; if(targetNode) { observer.observe(targetNode, { childList: true, subtree: true }); } else { setTimeout(startObserver, 500); } } const url = window.location.href.toLowerCase(); function openDashboard() { if (document.getElementById('cnki-overlay')) return; const overlay = document.createElement('div'); overlay.id = 'cnki-overlay'; overlay.className = 'cnki-ui-overlay'; overlay.innerHTML = `
${t('title')} ${t('version')}
⚠️
${t('guide_title')}
  • ${t('guide_browser')}
  • ${t('guide_tamper')}
  • ${t('guide_overwrite')}
🛡️

${t('mask_title')}

${t('mask_desc')}

${t('report_title')}

${t('label_folder')}
${t('tip_shift')}
${t('th_no')} ${t('th_title')} ${t('th_author')} ${t('th_status')}
`; document.body.appendChild(overlay); document.getElementById('cnki-close').onclick = () => overlay.remove(); document.getElementById('cnki-lang-toggle').onclick = toggleLang; document.getElementById('cnki-scan').onclick = scanPage; document.getElementById('cnki-start').onclick = () => startBatchDownload(false); document.getElementById('cnki-stop').onclick = stopDownload; document.getElementById('cnki-clear').onclick = clearTable; document.getElementById('cnki-report-close').onclick = () => document.getElementById('cnki-report-mask').style.display = 'none'; document.getElementById('cnki-report-retry').onclick = () => { document.getElementById('cnki-report-mask').style.display = 'none'; startBatchDownload(true); }; document.getElementById('cnki-verify').onclick = () => openVerificationWindow(null); document.getElementById('cnki-reset-history').onclick = () => { if(confirm(t('alert_history_clear'))) { GM_setValue('cnki_dl_history', []); alert(t('alert_history_done')); } }; document.getElementById('cnki-check-all').onclick = (e) => { document.querySelectorAll('.cnki-item-check').forEach(cb => { cb.checked = e.target.checked; toggleRowHighlight(cb); }); }; document.getElementById('cnki-webvpn').onchange = (e) => { useWebVPN = e.target.checked; GM_setValue('useWebVPN', useWebVPN); }; document.getElementById('cnki-folder').onchange = (e) => { GM_setValue('savedFolder', e.target.value.trim()); }; renderTable(); } function toggleRowHighlight(checkbox) { const tr = checkbox.closest('tr'); if(checkbox.checked) tr.classList.add('cnki-row-selected'); else tr.classList.remove('cnki-row-selected'); } // --- 核心功能 --- function openVerificationWindow(targetUrl) { let url = targetUrl; if (!url) { const data = JSON.parse(sessionStorage.getItem('cnki_data') || '[]'); if (data.length > 0) url = data[0].detailUrl; } if (!url) { alert(t('alert_no_item')); return; } window.open(url, '_blank', 'width=1024,height=768'); } function waitForUserVerification(url) { return new Promise((resolve) => { openVerificationWindow(url); const mask = document.getElementById('cnki-pause-mask'); const resumeBtn = document.getElementById('cnki-resume'); const stopBtn = document.getElementById('cnki-stop-pause'); mask.style.display = 'flex'; const onResume = () => { mask.style.display = 'none'; cleanup(); resolve(true); }; const onStop = () => { mask.style.display = 'none'; cleanup(); resolve(false); }; const cleanup = () => { resumeBtn.removeEventListener('click', onResume); stopBtn.removeEventListener('click', onStop); }; resumeBtn.addEventListener('click', onResume); stopBtn.addEventListener('click', onStop); }); } // 扫描页面 function scanPage() { const rows = Array.from(document.querySelectorAll('tbody tr, .list-item')).filter(row => { return row.style.display !== 'none' && row.innerText.trim() !== ''; }); if(rows.length === 0) { alert(t('alert_no_item')); return; } const currentData = JSON.parse(sessionStorage.getItem('cnki_data') || '[]'); let newCount = 0; rows.forEach((row, index) => { const link = row.querySelector('.fz14, .name a, .wx-tit h1'); if (!link) return; let detailUrl = link.href; if (!detailUrl || detailUrl.includes('javascript')) return; if(useWebVPN) { const origin = window.location.origin; detailUrl = origin + detailUrl.replace(/^(https?:\/\/)?(www\.)?[^\/]+/, ''); } const title = link.textContent.trim(); const author = row.querySelector('.author')?.textContent.trim() || '-'; const source = row.querySelector('.source')?.textContent.trim() || '-'; if(!currentData.find(d => d.detailUrl === detailUrl)) { currentData.push({ id: Date.now() + index, title, author, source, detailUrl, status: 'wait', errorMsg: '' }); newCount++; } }); sessionStorage.setItem('cnki_data', JSON.stringify(currentData)); renderTable(); updateStatusText(t('status_scanned').replace('{new}', newCount).replace('{total}', currentData.length)); } function renderTable() { const tbody = document.getElementById('cnki-tbody'); if(!tbody) return; tbody.innerHTML = ''; const data = JSON.parse(sessionStorage.getItem('cnki_data') || '[]'); data.forEach((item, idx) => { const tr = document.createElement('tr'); let statusHtml = `${t('status_wait')}`; if(item.status === 'done') statusHtml = `${t('status_done')}`; if(item.status === 'error') statusHtml = `✘ ${item.errorMsg || t('status_error')}`; if(item.status === 'pay') statusHtml = `${t('status_pay')}`; if(item.status === 'no_pdf') statusHtml = `${t('status_nopdf')}`; if(item.status === 'exists') statusHtml = `${t('status_exists')}`; if(item.status === 'running') statusHtml = `${t('status_running')}`; if(item.status === 'downloading') statusHtml = `${t('status_downloading')}`; tr.innerHTML = ` ${idx + 1} ${item.title}
${item.author}
${item.source}
${statusHtml} `; tbody.appendChild(tr); const checkbox = tr.querySelector('.cnki-item-check'); checkbox.addEventListener('click', (e) => { toggleRowHighlight(checkbox); if (e.shiftKey && lastCheckedIndex !== null) { const checks = Array.from(document.querySelectorAll('.cnki-item-check')); const start = Math.min(idx, lastCheckedIndex); const end = Math.max(idx, lastCheckedIndex); for (let i = start; i <= end; i++) { checks[i].checked = checkbox.checked; toggleRowHighlight(checks[i]); } } lastCheckedIndex = idx; }); if(checkbox.checked) toggleRowHighlight(checkbox); }); updateStatusText(t('status_total').replace('{total}', data.length)); } function clearTable() { sessionStorage.removeItem('cnki_data'); renderTable(); } function updateStatusText(text) { const el = document.getElementById('cnki-status-text'); if(el) el.textContent = text; } function stopDownload() { isRunning = false; document.getElementById('cnki-start').style.display = 'inline-block'; document.getElementById('cnki-stop').style.display = 'none'; updateStatusText(t('status_stopped')); showFinalReport(); } function showFinalReport() { const data = JSON.parse(sessionStorage.getItem('cnki_data') || '[]'); let totalSelected = 0, success = 0, failed = 0, pay = 0, nopdf = 0, exists = 0; const failedList = []; data.forEach(item => { if (item.status !== 'wait') { totalSelected++; if (item.status === 'done') success++; else if (item.status === 'exists') exists++; else if (item.status === 'pay') pay++; else if (item.status === 'no_pdf') nopdf++; else if (item.status === 'error') { failed++; failedList.push(item); } } }); if (totalSelected === 0) return; const mask = document.getElementById('cnki-report-mask'); const header = document.getElementById('cnki-report-header'); const summary = document.getElementById('cnki-report-summary'); const list = document.getElementById('cnki-report-list'); const retryBtn = document.getElementById('cnki-report-retry'); mask.style.display = 'flex'; list.innerHTML = ''; let reportHtml = `
${t('report_success')}: ${success} ${t('report_exists')}: ${exists} ${t('report_pay')}: ${pay} ${t('report_nopdf')}: ${nopdf} ${t('report_fail')}: ${failed}
`; if (failed > 0) { header.classList.add('has-error'); reportHtml += `
${t('report_msg_check')}
`; summary.innerHTML = reportHtml; failedList.forEach((item, idx) => { const div = document.createElement('div'); div.className = 'cnki-report-item'; div.innerHTML = `
${idx + 1}. ${item.title}
${item.errorMsg || t('status_error')}
`; list.appendChild(div); }); retryBtn.style.display = 'inline-block'; } else { header.classList.remove('has-error'); summary.innerHTML = reportHtml; list.innerHTML = `
${t('report_msg_none')}
`; retryBtn.style.display = 'none'; } } async function startBatchDownload(isRetry = false) { if(isRunning) return; let checkboxes = Array.from(document.querySelectorAll('.cnki-item-check')); if (isRetry) { const data = JSON.parse(sessionStorage.getItem('cnki_data') || '[]'); checkboxes.forEach(cb => { const id = parseInt(cb.value); const item = data.find(d => d.id === id); if (item && item.status === 'error') cb.checked = true; else cb.checked = false; }); } const checkedBoxes = document.querySelectorAll('.cnki-item-check:checked'); if(checkedBoxes.length === 0) { alert(t('alert_no_check')); return; } isRunning = true; document.getElementById('cnki-start').style.display = 'none'; document.getElementById('cnki-stop').style.display = 'inline-block'; const data = JSON.parse(sessionStorage.getItem('cnki_data') || '[]'); const folder = document.getElementById('cnki-folder').value.trim() || DEFAULT_FOLDER; for(let i=0; i d.id === id); if(!item) continue; const result = await processSingleItem(item, folder); if (result === 'captcha') { updateStatus(id, 'error', `⛔ ${t('err_captcha')}`); item.errorMsg = t('err_captcha'); const userChoice = await waitForUserVerification(item.detailUrl); if (userChoice) { i--; continue; } else { stopDownload(); return; } } else if (result === 'skip') { updateStatus(id, 'skip', `⚠ ${t('err_no_auth')}`); item.status = 'skip'; item.errorMsg = t('err_no_auth'); sessionStorage.setItem('cnki_data', JSON.stringify(data)); checkedBoxes[i].checked = false; } else if (result === 'no_pdf') { updateStatus(id, 'no_pdf', `⚪ ${t('status_nopdf')}`); item.status = 'no_pdf'; sessionStorage.setItem('cnki_data', JSON.stringify(data)); checkedBoxes[i].checked = false; } else if (result === 'exists') { updateStatus(id, 'exists', `🔁 ${t('status_exists')}`); item.status = 'exists'; sessionStorage.setItem('cnki_data', JSON.stringify(data)); checkedBoxes[i].checked = false; continue; } else if (result === true) { updateStatus(id, 'done', t('status_done')); item.status = 'done'; item.errorMsg = ''; sessionStorage.setItem('cnki_data', JSON.stringify(data)); checkedBoxes[i].checked = false; } else { updateStatus(id, 'error', `✘ ${t('status_error')}`); item.status = 'error'; item.errorMsg = t('err_download_fail'); sessionStorage.setItem('cnki_data', JSON.stringify(data)); } if(i < checkedBoxes.length - 1 && isRunning) { if (result === true) { const delay = Math.floor(Math.random() * (DEFAULT_MAX_DELAY - DEFAULT_MIN_DELAY + 1)) + DEFAULT_MIN_DELAY; let remaining = delay / 1000; let finalText = t('status_done'); const timer = setInterval(() => { if(!isRunning) clearInterval(timer); updateStatus(id, item.status, `${finalText} (${t('cool_down')} ${remaining.toFixed(0)}s)`); remaining--; }, 1000); await new Promise(r => setTimeout(r, delay)); clearInterval(timer); updateStatus(id, item.status, finalText); } else { let msg = ''; if(item.status === 'exists') msg = `🔁 ${t('status_exists')}`; else if(item.status === 'no_pdf') msg = `⚪ ${t('status_nopdf')}`; else if(item.status === 'pay') msg = `💰 ${t('status_pay')}`; else msg = `✘ ${t('status_error')}`; updateStatus(id, item.status, msg + ` (⏩ ${t('status_skip')})`); await new Promise(r => setTimeout(r, 500)); updateStatus(id, item.status, msg); } } } stopDownload(); if(isRunning) { updateStatusText(t('status_finished')); showFinalReport(); } } function updateStatus(id, status, text) { const cell = document.getElementById(`status-${id}`); if(cell) { let color = '#94a3b8'; if(status.includes('run') || status.includes('download')) color = '#3b82f6'; if(status === 'done') color = '#16a34a'; if(status === 'error') color = '#ef4444'; if(status === 'pay') color = '#f59e0b'; if(status === 'no_pdf') color = '#6b7280'; if(status === 'exists') color = '#6b7280'; cell.innerHTML = `${text || status}`; } } async function processSingleItem(item, folder) { return new Promise(async (resolve) => { try { const safeTitle = item.title.replace(/[\\/:*?"<>|]/g, '_').trim(); const finalName = folder ? `${folder}/${safeTitle}.pdf` : `${safeTitle}.pdf`; const history = GM_getValue('cnki_dl_history', []); if (history.includes(finalName)) { resolve('exists'); return; } updateStatus(item.id, 'running', t('status_running')); const res = await new Promise((rs, rj) => { GM_xmlhttpRequest({ method: 'GET', url: item.detailUrl, headers: { 'Referer': window.location.href }, onload: rs, onerror: rj }); }); const doc = new DOMParser().parseFromString(res.responseText, 'text/html'); if (res.responseText.includes('captcha-element') || res.responseText.includes('TencentCaptcha') || res.responseText.includes('拼图校验')) { resolve('captcha'); return; } let pdfLink = null; const btnArea = doc.querySelector('.operate-btn') || doc.querySelector('#DownLoadParts'); if(btnArea) { const links = btnArea.querySelectorAll('a'); for(let a of links) { if(a.textContent.includes('PDF') || a.textContent.includes('整本') || a.textContent.includes('Whole')) { pdfLink = a.href; break; } } } if(!pdfLink) { resolve('no_pdf'); return; } if(!pdfLink.startsWith('http')) { const origin = new URL(item.detailUrl).origin; pdfLink = origin + (pdfLink.startsWith('/') ? '' : '/') + pdfLink; } updateStatus(item.id, 'downloading', t('status_downloading')); GM_xmlhttpRequest({ method: 'GET', url: pdfLink, responseType: 'blob', headers: { 'Referer': item.detailUrl, 'Cookie': document.cookie, 'User-Agent': navigator.userAgent }, onload: function(response) { const blob = response.response; const contentType = response.responseHeaders.match(/content-type:\s*(.*)/i)?.[1] || ''; if(contentType.includes('text/html')) { const reader = new FileReader(); reader.onload = function() { const text = reader.result; if (text.includes('captcha-element') || text.includes('TencentCaptcha')) { resolve('captcha'); } else if (text.includes('充值') || text.includes('登录') || text.includes('权限') || text.includes('fee') || text.includes('cz-alert') || text.includes('购买')) { resolve('pay'); } else { resolve(false); } }; reader.readAsText(blob); return; } if(blob.size < 2000) { resolve(false); return; } const blobUrl = URL.createObjectURL(blob); GM_download({ url: blobUrl, name: finalName, saveAs: false, conflictAction: 'overwrite', onload: () => { const currentHistory = GM_getValue('cnki_dl_history', []); if (!currentHistory.includes(finalName)) { currentHistory.push(finalName); GM_setValue('cnki_dl_history', currentHistory); } setTimeout(() => URL.revokeObjectURL(blobUrl), 5000); resolve(true); }, onerror: (err) => { console.error(err); resolve(false); } }); }, onerror: function(err) { console.error(err); resolve(false); } }); } catch(e) { console.error(e); resolve(false); } }); } // 启动 injectStyle(); setInterval(tryCreateButton, 1000); tryCreateButton(); startObserver(); })();