// ==UserScript== // @name 资料宝库·文档下载器 // @namespace http://tampermonkey.net/ // @version 3.3 // @description 替换网页预览区为完整全文渲染(支持文字文档和PPT图片文档),解除试读限制,支持直接选择/复制/下载 // @author you // @match *://web.cpsad.cn/* // @icon https://web.cpsad.cn/favicon.ico // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_log // @connect gyy.kuduoke.net // @connect web.cpsad.cn // @run-at document-start // @license MIT // ==/UserScript== (function () { 'use strict'; const TAG = '[cpsad_dl]'; const log = (...a) => { const msg = a.map(x => (typeof x === 'string' ? x : JSON.stringify(x))).join(' '); try { GM_log(TAG + ' ' + msg); } catch (e) {} try { console.log(TAG, ...a); } catch (e) {} }; log('脚本启动 v3.3, URL =', location.href); // ===== 样式 ===== GM_addStyle(` /* 全文渲染容器 — 替换原 iframe 预览区 */ .cpsad-fullview { width: 100% !important; min-height: 60vh !important; padding: 32px 48px !important; box-sizing: border-box !important; background: #fff !important; position: relative !important; font-family: "仿宋_GB2312","仿宋",FangSong,STFangsong,serif !important; font-size: 16pt !important; line-height: 1.9 !important; color: #333 !important; overflow: visible !important; -webkit-user-select: text !important; user-select: text !important; -webkit-touch-callout: default !important; } .cpsad-fullview * { box-sizing: border-box !important; -webkit-user-select: text !important; user-select: text !important; } .cpsad-fullview p { margin: 0 0 4pt 0 !important; text-indent: 2em !important; text-align: justify !important; } .cpsad-fullview h1 { font-family: SimHei,"黑体" !important; font-size: 16pt !important; font-weight: normal !important; text-indent: 2em !important; margin: 12pt 0 4pt 0 !important; } .cpsad-fullview h2 { font-family: KaiTi,"楷体" !important; font-size: 16pt !important; font-weight: normal !important; text-indent: 2em !important; margin: 12pt 0 4pt 0 !important; } .cpsad-fullview h3 { font-size: 16pt !important; font-weight: bold !important; text-indent: 2em !important; margin: 12pt 0 4pt 0 !important; } .cpsad-fullview h4 { font-size: 16pt !important; font-weight: normal !important; text-indent: 2em !important; margin: 8pt 0 4pt 0 !important; } .cpsad-fullview table { border-collapse: collapse !important; width: 100% !important; margin: 8pt 0 !important; } .cpsad-fullview table td, .cpsad-fullview table th { border: 1px solid #999 !important; padding: 6px 10px !important; font-size: 14pt !important; } .cpsad-fullview img { max-width: 100% !important; height: auto !important; } .cpsad-fullview .gw-title { font-family: "方正小标宋简体","小标宋体","华文中宋",SimSun,serif !important; font-size: 22pt !important; text-align: center !important; text-indent: 0 !important; font-weight: normal !important; margin: 0 0 28pt 0 !important; line-height: 1.5 !important; } /* PPT 图片容器 */ .cpsad-pptview { width: 100% !important; display: flex !important; flex-direction: column !important; align-items: center !important; gap: 16px !important; padding: 24px 0 !important; } .cpsad-pptview .ppt-slide { width: 100% !important; max-width: 888px !important; position: relative !important; border-radius: 8px !important; overflow: hidden !important; box-shadow: 0 2px 12px rgba(0,0,0,0.12) !important; background: #f8f9fa !important; } .cpsad-pptview .ppt-slide img { display: block !important; width: 100% !important; height: auto !important; object-fit: contain !important; } .cpsad-pptview .ppt-page-num { position: absolute !important; top: 8px !important; right: 12px !important; background: rgba(0,0,0,0.5) !important; color: #fff !important; padding: 2px 10px !important; border-radius: 12px !important; font-size: 12px !important; font-family: -apple-system, BlinkMacSystemFont, sans-serif !important; z-index: 1 !important; } @media (min-width: 768px) { .cpsad-pptview.ppt-multiple { flex-direction: row !important; flex-wrap: wrap !important; justify-content: center !important; } .cpsad-pptview.ppt-multiple .ppt-slide { width: calc(50% - 8px) !important; max-width: 444px !important; } } /* 顶部工具条 */ .cpsad-actionbar { display: flex !important; align-items: center !important; gap: 10px !important; padding: 12px 24px !important; background: #f8f8f8 !important; border-bottom: 1px solid #e8e8e8 !important; border-radius: 8px 8px 0 0 !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important; position: sticky !important; top: 0 !important; z-index: 100 !important; } .cpsad-actionbar .cpsad-title-display { flex: 1 !important; font-size: 15px !important; font-weight: 600 !important; color: #333 !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } .cpsad-actionbar button { border: none !important; border-radius: 6px !important; padding: 8px 16px !important; font-size: 13px !important; cursor: pointer !important; font-weight: 500 !important; transition: all .15s !important; white-space: nowrap !important; } .cpsad-actionbar .btn-copy { background: #8e44ad !important; color: #fff !important; } .cpsad-actionbar .btn-copy:hover { background: #7d3c9d !important; } .cpsad-actionbar .btn-copy:disabled { background: #bbb !important; cursor: not-allowed !important; } .cpsad-actionbar .btn-download { background: #07c160 !important; color: #fff !important; } .cpsad-actionbar .btn-download:hover { background: #06ad56 !important; } .cpsad-actionbar .btn-download:disabled { background: #bbb !important; cursor: not-allowed !important; } .cpsad-actionbar .btn-restore { background: #f0f0f0 !important; color: #666 !important; } .cpsad-actionbar .btn-restore:hover { background: #e0e0e0 !important; } /* 占位 */ .cpsad-placeholder { padding: 60px 24px !important; text-align: center !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important; font-size: 15px !important; color: #999 !important; } .cpsad-placeholder.error { color: #e64340 !important; } /* Toast */ .cpsad-toast { position: fixed !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; z-index: 2147483647 !important; background: rgba(0,0,0,.8) !important; color: #fff !important; padding: 12px 28px !important; border-radius: 8px !important; font-size: 15px !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important; animation: cpsad-toast-in .2s ease !important; } @keyframes cpsad-toast-in { from { opacity: 0; transform: translate(-50%, -45%); } to { opacity: 1; transform: translate(-50%, -50%); } } `); // ===== 全局拦截:阻止原网页 JS 禁用复制/选择/右键 ===== const rawAddEventListener = EventTarget.prototype.addEventListener; const blockedEvents = ['contextmenu', 'selectstart', 'dragstart', 'copy']; EventTarget.prototype.addEventListener = function (type, listener, options) { if (blockedEvents.includes(type)) { return; } return rawAddEventListener.call(this, type, listener, options); }; ['oncontextmenu', 'onselectstart', 'oncopy', 'ondragstart'].forEach(prop => { try { Object.defineProperty(document, prop, { get: () => null, set: () => {}, configurable: true }); } catch (e) {} }); ['contextmenu', 'selectstart', 'copy', 'dragstart'].forEach(evtName => { document.addEventListener(evtName, e => { e.stopImmediatePropagation(); if (e.cancelable) e.returnValue = true; }, true); }); const styleUnlock = document.createElement('style'); styleUnlock.textContent = ` body, * { -webkit-user-select: text !important; user-select: text !important; -webkit-touch-callout: default !important; } `; (document.head || document.documentElement).appendChild(styleUnlock); // ===== 查找预览 iframe 及 URL ===== function findPreviewIframe() { const ifr = document.querySelector('iframe[src*="gyy.kuduoke.net"], iframe[src*="kuduoke"]'); if (ifr) return { iframe: ifr, url: ifr.src }; const html = document.documentElement.outerHTML; const m = html.match(/https?:\/\/gyy\.kuduoke\.net[^\s"'<>\\]+/i) || html.match(/https?:\/\/[\w.-]*kuduoke\.net[^\s"'<>\\]+/i); if (m) { const anyIfr = document.querySelector('iframe'); return { iframe: anyIfr, url: m[0] }; } return null; } // ===== 缓存 ===== let cachedDoc = null; // ===== 抓取预览页完整正文 ===== function fetchPreviewContent(previewUrl, callback) { GM_xmlhttpRequest({ method: 'GET', url: previewUrl, headers: { 'Referer': location.href }, timeout: 15000, onload: function (resp) { const html = resp.responseText || ''; log('预览返回 状态:', resp.status, '长度:', html.length); const bodyMatch = html.match(/]*>([\s\S]*?)<\/body>/i); if (!bodyMatch) { callback({ error: 'parse', msg: '未匹配到 ' }); return; } const rawBody = bodyMatch[1]; const previewOrigin = previewUrl.match(/^(https?:\/\/[^/]+)/i)[1]; const docIdMatch = previewUrl.match(/\/html\/([\w]+)\.html/i); const docId = docIdMatch ? docIdMatch[1] : ''; // ===== 检测 PPT/图片类型 ===== const hasGallery = rawBody.includes('galleryContainer') || rawBody.includes('img-container'); if (hasGallery) { log('检测到 PPT/图片类型文档'); const images = []; const imgRegex = /data-src=["']([^"']+)["']/gi; let m; while ((m = imgRegex.exec(rawBody)) !== null) { let src = m[1]; if (src.startsWith('/')) src = previewOrigin + src; else if (!src.startsWith('http')) src = previewOrigin + '/html/' + docId + '/' + src; images.push(src); } if (images.length === 0) { const srcRegex = /]*\ssrc=["'](?!data:)([^"']+)["']/gi; while ((m = srcRegex.exec(rawBody)) !== null) { let src = m[1]; if (src.startsWith('/')) src = previewOrigin + src; else if (!src.startsWith('http')) src = previewOrigin + '/html/' + docId + '/' + src; images.push(src); } } if (images.length === 0) { callback({ error: 'parse', msg: 'PPT文档但未找到图片' }); return; } log('PPT图片总数:', images.length); let title = ''; const h1 = document.querySelector('h1'); if (h1) title = h1.textContent.trim(); if (!title) title = document.title; title = title.replace(/试读.*$/, '').replace(/[\\/:*?"<>|]/g, '_').trim(); if (!title) title = 'cpsad_doc'; cachedDoc = { title, body: '', type: 'ppt', images }; callback({ ok: true, title, body: '', type: 'ppt', images }); return; } // ===== 文字类型文档 ===== const textLen = html.replace(/<[^>]+>/g, '').trim().length; if (textLen < 200) { callback({ error: 'skip', msg: '仅PPT/图片,无可提取文本' }); return; } let body = rawBody; body = body.replace(/]*>[\s\S]*?<\/div>/gi, ''); body = body.replace(/]*>[\s\S]*?<\/div>/gi, ''); body = body.replace(/]*>[\s\S]*?<\/script>/gi, ''); body = body.replace(/]*>[\s\S]*?<\/style>/gi, ''); body = body.replace(/<\/?span[^>]*>/g, ''); body = body.replace(/<\/?font[^>]*>/g, ''); body = body.replace(/\s(?:style|class|bgcolor|color|face|align)\s*=\s*("[^"]*"|'[^']*')/gi, ''); body = body.replace(/]*>(?:\s| |)*<\/p>/gi, ''); let title = ''; const h1 = document.querySelector('h1'); if (h1) title = h1.textContent.trim(); if (!title) { const t = body.match(/]*>(.*?)<\/h1>/i); if (t) title = t[1].replace(/<[^>]+>/g, '').trim(); } if (!title) title = document.title; title = title.replace(/试读.*$/, '').replace(/[\\/:*?"<>|]/g, '_').trim(); if (!title) title = 'cpsad_doc'; body = body.replace(/]*>[\s\S]*?<\/h1>/i, ''); cachedDoc = { title, body, type: 'text' }; callback({ ok: true, title, body, type: 'text' }); }, onerror: function () { callback({ error: 'network', msg: '网络错误' }); }, ontimeout: function () { callback({ error: 'timeout', msg: '请求超时' }); } }); } // ===== 生成 .doc(文字文档)===== function downloadDoc(title, body) { const docHtml = ` ${title}

${title}

${body}

—  PAGE  —

`; const blob = new Blob([docHtml], { type: 'application/msword' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = title + '.doc'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); log('下载完成:', title + '.doc'); } // ===== 下载 PPT 图片(打包为 HTML)===== function downloadPptHtml(title, images) { const imgsHtml = images.map((src, i) => `

第 ${i + 1} 页 / 共 ${images.length} 页

` ).join('\n'); const html = `${title}

${title}

${imgsHtml}`; const blob = new Blob([html], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = title + '.html'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); log('PPT下载完成:', title + '.html'); } // ===== Toast ===== function showToast(msg, duration = 2000) { const old = document.querySelector('.cpsad-toast'); if (old) old.remove(); const toast = document.createElement('div'); toast.className = 'cpsad-toast'; toast.textContent = msg; document.body.appendChild(toast); setTimeout(() => toast.remove(), duration); } // ===== 复制(文字文档)===== function copyToClipboard(title, body, btn) { let plainText = title + '\n\n'; const temp = document.createElement('div'); temp.innerHTML = body; temp.querySelectorAll('p, h1, h2, h3, h4, div').forEach(el => { el.insertAdjacentText('beforebegin', '\n'); el.insertAdjacentText('afterend', '\n'); }); temp.querySelectorAll('br').forEach(el => { el.insertAdjacentText('beforebegin', '\n'); }); temp.querySelectorAll('tr').forEach(el => { el.insertAdjacentText('afterend', '\n'); }); temp.querySelectorAll('td, th').forEach(el => { el.insertAdjacentText('afterend', '\t'); }); plainText += temp.textContent.replace(/\n{3,}/g, '\n\n').trim(); const done = () => { btn.textContent = '✅ 已复制'; showToast('✅ 已复制全文到剪贴板'); setTimeout(() => { btn.textContent = '📋 复制全文'; btn.disabled = false; }, 2500); }; btn.disabled = true; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(plainText).then(done).catch(() => fallbackCopy(plainText, btn, done)); } else { fallbackCopy(plainText, btn, done); } } function fallbackCopy(text, btn, done) { const ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0;'; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); done(); } catch (e) { showToast('❌ 复制失败,请手动选择文本'); btn.disabled = false; } document.body.removeChild(ta); } // ===== 替换预览区 ===== function replacePreviewWithFullView(targetEl, previewUrl) { const originalEl = targetEl; const parentNode = originalEl.parentNode; const wrapper = document.createElement('div'); wrapper.className = 'cpsad-fullview-wrapper'; wrapper.setAttribute('data-cpsad-original', '1'); const actionbar = document.createElement('div'); actionbar.className = 'cpsad-actionbar'; const titleDisplay = document.createElement('div'); titleDisplay.className = 'cpsad-title-display'; titleDisplay.textContent = '📄 加载中...'; const btnCopy = document.createElement('button'); btnCopy.className = 'btn-copy'; btnCopy.textContent = '📋 复制全文'; btnCopy.disabled = true; const btnDownload = document.createElement('button'); btnDownload.className = 'btn-download'; btnDownload.textContent = '📄 下载文档'; btnDownload.disabled = true; const btnRestore = document.createElement('button'); btnRestore.className = 'btn-restore'; btnRestore.textContent = '↩ 恢复原预览'; btnRestore.onclick = function () { wrapper.replaceWith(originalEl); }; actionbar.appendChild(titleDisplay); actionbar.appendChild(btnCopy); actionbar.appendChild(btnDownload); actionbar.appendChild(btnRestore); const content = document.createElement('div'); content.innerHTML = '
⏳ 正在获取完整正文...
'; wrapper.appendChild(actionbar); wrapper.appendChild(content); parentNode.replaceChild(wrapper, originalEl); log('已替换预览区,开始抓取完整正文'); function load(callback) { if (cachedDoc) { callback({ ok: true, ...cachedDoc }); return; } fetchPreviewContent(previewUrl, callback); } load(function (res) { if (res.ok) { if (res.type === 'ppt') { // ===== 渲染 PPT 图片 ===== content.className = 'cpsad-pptview' + (res.images.length > 1 ? ' ppt-multiple' : ''); content.innerHTML = ''; res.images.forEach((src, i) => { const slide = document.createElement('div'); slide.className = 'ppt-slide'; const num = document.createElement('div'); num.className = 'ppt-page-num'; num.textContent = (i + 1) + ' / ' + res.images.length; const img = document.createElement('img'); img.src = src; img.alt = '第' + (i + 1) + '页'; img.loading = 'lazy'; slide.appendChild(num); slide.appendChild(img); content.appendChild(slide); }); titleDisplay.textContent = res.title + '(PPT · ' + res.images.length + '页)'; titleDisplay.title = res.title; btnCopy.disabled = true; btnCopy.textContent = '📋 PPT不支持复制'; btnCopy.title = '图片类型文档无法复制文字'; btnDownload.disabled = false; btnDownload.textContent = '📄 下载PPT'; btnDownload.onclick = function () { downloadPptHtml(res.title, res.images); const orig = btnDownload.textContent; btnDownload.textContent = '✅ 已下载'; setTimeout(() => { btnDownload.textContent = orig; }, 2500); }; log('PPT渲染完成:', res.title, res.images.length + '张图片'); } else { // ===== 渲染文字文档 ===== content.className = 'cpsad-fullview'; content.innerHTML = ''; const titleP = document.createElement('p'); titleP.className = 'gw-title'; titleP.textContent = res.title; content.appendChild(titleP); content.innerHTML += res.body; titleDisplay.textContent = res.title; titleDisplay.title = res.title; btnCopy.disabled = false; btnCopy.onclick = function () { copyToClipboard(res.title, res.body, btnCopy); }; btnDownload.disabled = false; btnDownload.onclick = function () { downloadDoc(res.title, res.body); const orig = btnDownload.textContent; btnDownload.textContent = '✅ 已下载'; setTimeout(() => { btnDownload.textContent = orig; }, 2500); }; log('全文渲染完成:', res.title); } } else if (res.error === 'skip') { content.innerHTML = `
⚠️ ${res.msg}
`; titleDisplay.textContent = '⚠️ ' + res.msg; } else { content.innerHTML = `
❌ ${res.msg}
`; titleDisplay.textContent = '❌ 加载失败'; } }); } // ===== 轮询等待预览 iframe ===== let pollTimer = null; function waitForPreview() { if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } let tries = 0; const maxTries = 30; pollTimer = setInterval(() => { tries++; const result = findPreviewIframe(); if (result && result.iframe) { clearInterval(pollTimer); pollTimer = null; log('找到预览 iframe(第' + tries + '次):', result.url); if (document.querySelector('.cpsad-fullview-wrapper[data-cpsad-original]')) { log('已存在替换容器,跳过'); return; } replacePreviewWithFullView(result.iframe, result.url); } else if (tries >= maxTries) { clearInterval(pollTimer); pollTimer = null; log('超时仍未找到预览 iframe'); } }, 600); } function onReady(callback) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', callback); } else { callback(); } } onReady(() => { log('DOM ready, 开始检测预览 iframe'); waitForPreview(); }); // ===== SPA 路由监听 ===== ['pushState', 'replaceState'].forEach(fn => { const raw = history[fn]; history[fn] = function () { const r = raw.apply(this, arguments); window.dispatchEvent(new Event('cpsad:locationchange')); return r; }; }); window.addEventListener('popstate', () => window.dispatchEvent(new Event('cpsad:locationchange'))); let lastHref = location.href; window.addEventListener('cpsad:locationchange', () => { if (location.href === lastHref) return; lastHref = location.href; log('检测到路由变化:', location.href); setTimeout(() => { cachedDoc = null; const oldWrapper = document.querySelector('.cpsad-fullview-wrapper[data-cpsad-original]'); if (oldWrapper) oldWrapper.remove(); waitForPreview(); }, 300); }); })();