// ==UserScript== // @name CNKI AI 文献PDF一键下载 // @namespace http://tampermonkey.net/ // @version 3.0 // @description CNKI AI 深度研究页。每个表格顶部"下载表格"旁注入下载按钮,仅下载该表格所有分页的PDF。 // @author You // @match https://ai.cnki.net/chat/result/* // @noframes // ==/UserScript== (function () { 'use strict'; // ============================================================== // 辅助 // ============================================================== function sanitize(name) { return (name || 'document') .replace(/[\/\\:*?"<>|]/g, '_') .replace(/\s+/g, ' ').trim().slice(0, 120); } function toast(msg, dur = 3000) { const id = 'cnki-pdf-toast'; const old = document.getElementById(id); if (old) old.remove(); const d = document.createElement('div'); d.id = id; d.textContent = msg; Object.assign(d.style, { position: 'fixed', bottom: '24px', left: '50%', transform: 'translateX(-50%)', background: 'rgba(0,0,0,0.82)', color: '#fff', padding: '10px 22px', borderRadius: '8px', fontSize: '13px', zIndex: '9999999', pointerEvents: 'none', maxWidth: '88vw', whiteSpace: 'pre-wrap', textAlign: 'center', fontFamily: 'system-ui,sans-serif', boxShadow: '0 2px 12px rgba(0,0,0,0.3)' }); document.body.appendChild(d); setTimeout(() => { d.style.opacity = '0'; setTimeout(() => d.remove(), 250); }, dur); } // ============================================================== // 翻页工具(全部限定在 root 内操作) // ============================================================== function waitForPage(root, page, timeout = 8000) { return new Promise((resolve, reject) => { const start = Date.now(); const tick = () => { const active = root.querySelector('.pagination-container .pagination-item.active'); if (active && parseInt(active.textContent.trim(), 10) === page) { resolve(); return; } if (Date.now() - start > timeout) { reject(new Error('翻页超时')); return; } setTimeout(tick, 300); }; tick(); }); } function goToPage(root, page) { const items = root.querySelectorAll('.pagination-container .pagination-item'); for (const item of items) { if (parseInt(item.textContent.trim(), 10) === page) { item.click(); return true; } } return false; } function getTotalPages(root) { const items = root.querySelectorAll('.pagination-container .pagination-item'); let max = 0; for (const item of items) { const n = parseInt(item.textContent.trim(), 10); if (n > max) max = n; } return max; } function getCurrentPage(root) { const active = root.querySelector('.pagination-container .pagination-item.active'); return active ? parseInt(active.textContent.trim(), 10) : 1; } // ============================================================== // 在 root 内抓取当前页 PDF 条目 // ============================================================== function collectItemsIn(root) { const items = []; const seen = {}; // 只在表格 tbody 行中查找,避免误捞表格顶部的"下载表格"等链接 const table = root.querySelector('.el-table'); if (!table) return items; table.querySelectorAll('a').forEach(a => { if (a.textContent.trim() !== 'PDF下载') return; if (!a.href || a.href.indexOf('bar.cnki.net') === -1) return; if (seen[a.href]) return; seen[a.href] = true; const tr = a.closest('tr'); let title = ''; if (tr) { // 尝试从行中第一个包含标题文本的 td 提取 const tds = tr.querySelectorAll('td'); for (const td of tds) { const txt = td.textContent.trim(); // 标题 td 通常包含序号开头 + 较长文本 if (txt.length > 10 && !txt.includes('PDF下载')) { title = txt.replace(/^\d+\s+/, '').split('\n')[0].trim(); break; } } } // 保存原始 a 元素引用,后续直接点击它(保留 Referer / cookie 等同源信息) items.push({ el: a, href: a.href, title: title || ('文献_' + (items.length + 1)) }); }); return items; } // ============================================================== // 全量抓取(遍历分页),限定在 root 范围内 // ============================================================== async function collectAllIn(root, progressFn) { const totalPages = getTotalPages(root); const startPage = getCurrentPage(root); const allItems = []; for (let p = startPage; p <= totalPages; p++) { if (p !== startPage) { goToPage(root, p); try { await waitForPage(root, p); } catch (_) {} } await new Promise(r => setTimeout(r, 600)); const pageItems = collectItemsIn(root); allItems.push(...pageItems); if (progressFn) progressFn(p, totalPages, allItems.length); } // 恢复原页 if (startPage > 1 && startPage !== totalPages) { goToPage(root, startPage); try { await waitForPage(root, startPage); } catch (_) {} } return allItems; } // ============================================================== // 下载 // ============================================================== async function downloadItems(items, btn) { let done = 0, errors = 0; for (let i = 0; i < items.length; i++) { const it = items[i]; if (btn) btn.textContent = `⏳ ${i + 1}/${items.length}`; try { // 用 window.open 打开下载链接,保留同源 cookie 和 Referer // 直接 click 在异步循环中会丢失用户激活状态被弹窗拦截 const w = window.open(it.href, '_blank'); if (!w) { errors++; toast('⚠️ 弹窗被拦截,请允许弹窗后重试', 3000); break; } done++; } catch (_) { errors++; } await new Promise(r => setTimeout(r, 1800)); } if (btn) { btn.textContent = errors > 0 ? `✅ 成功${done} 失败${errors}` : `✅ 完成(${done}篇)`; } toast(`✅ 下载完成!成功 ${done} 篇${errors > 0 ? `,失败 ${errors} 篇` : ''}`, 4000); setTimeout(() => { if (btn) btn.textContent = '📥 下载本表PDF'; }, 5000); } // ============================================================== // 在 .title-container 内注入下载按钮 // ============================================================== function buildButton(titleContainer) { if (titleContainer.dataset.cnkiPdfInjected) return; titleContainer.dataset.cnkiPdfInjected = '1'; // 找到 titleContainer 所在的 .container(表格根节点) const container = titleContainer.closest('.container'); if (!container) return; const btn = document.createElement('button'); btn.textContent = '📥 下载本表PDF'; Object.assign(btn.style, { marginLeft: '8px', background: 'linear-gradient(135deg,#e74c3c,#c0392b)', color: 'white', border: 'none', borderRadius: '4px', padding: '0 10px', fontSize: '12px', fontWeight: 'bold', cursor: 'pointer', lineHeight: '28px', height: '28px', verticalAlign: 'middle', whiteSpace: 'nowrap', boxShadow: '0 2px 6px rgba(231,76,60,0.35)', transition: 'opacity 0.2s', fontFamily: 'system-ui,sans-serif' }); btn.addEventListener('mouseenter', () => { btn.style.opacity = '0.85'; }); btn.addEventListener('mouseleave', () => { btn.style.opacity = '1'; }); btn.addEventListener('click', async (e) => { e.stopPropagation(); e.preventDefault(); btn.disabled = true; btn.textContent = '🔍 扫描中…'; try { const items = await collectAllIn(container, (page, total, count) => { btn.textContent = `🔍 第${page}/${total}页(${count}篇)`; }); if (items.length === 0) { toast('⚠️ 未找到 PDF 下载链接', 3000); btn.disabled = false; btn.textContent = '📥 下载本表PDF'; return; } btn.textContent = `🚀 下载${items.length}篇…`; await downloadItems(items, btn); } catch (err) { console.error('[CNKI PDF]', err); toast('❌ ' + err.message, 4000); btn.disabled = false; btn.textContent = '📥 下载本表PDF'; } }); // 插入到 title-container 末尾 titleContainer.appendChild(btn); } // ============================================================== // 注入逻辑 // ============================================================== function injectAll() { // 只匹配 .container > .title-container(表格顶部操作栏) // 不匹配 tr 内的 .btn-container(那些是每行摘要展开按钮) document.querySelectorAll('.container > .title-container').forEach(buildButton); } // ============================================================== // 启动 // ============================================================== injectAll(); const obs = new MutationObserver(() => { injectAll(); }); obs.observe(document.body, { childList: true, subtree: true }); setTimeout(injectAll, 2500); })();