中小学智慧教育平台助手

提取 iframe 中 PDF,支持下载与 ConvertAPI 转换为 PPTX(Bearer 授权、多部分上传),

// ==UserScript==
// @name         中小学智慧教育平台助手
// @namespace    http://tampermonkey.net/
// @version      2.3.1
// @description  提取 iframe 中 PDF,支持下载与 ConvertAPI 转换为 PPTX(Bearer 授权、多部分上传),
//               带进度与日志;禁止页面全屏;美化浮动按钮;支持 API Key 配置与日志区打开。仅在顶层页面运行。
// @match        https://*.smartedu.cn/*
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @connect      r3-ndr-private.ykt.cbern.com.cn
// @connect      v2.convertapi.com
// @noframes
// ==/UserScript==

(function () {
    'use strict';

    // ========= 配置(默认空,建议通过「设置 API」输入并保存) =========
    const DEFAULT_BEARER_TOKEN = ''; // 保留,但不要把真实 token 写死在这里
    const CONVERT_API_URL = 'https://v2.convertapi.com/convert/pdf/to/pptx';
    // ================================================================

    // ======= 简易样式 =======
    GM_addStyle(`
    .edu-helper-wrapper { position: fixed; bottom: 20px; right: 20px; z-index: 2147483647; font-family: Arial, sans-serif; }
    .edu-helper-mainbtn { background: rgba(70,130,180,0.85); color:#fff; border:none; border-radius:50%; width:50px; height:50px; cursor:pointer; box-shadow:0 2px 8px rgba(0,0,0,0.25); }
    .edu-helper-panel { display:none; margin-top:10px; background: #fff; border:1px solid #ddd; border-radius:8px; padding:10px; min-width:260px; max-height:360px; overflow:auto; box-shadow:0 2px 10px rgba(0,0,0,0.12); }
    .edu-helper-row { margin-bottom:8px; }
    .edu-helper-btn { padding:6px 8px; margin-right:6px; border:none; border-radius:4px; cursor:pointer; color:#fff; }
    .edu-helper-download { background: rgba(70,130,180,0.95); }
    .edu-helper-convert { background: rgba(34,139,34,0.95); }
    .edu-helper-ctrl { display:flex; gap:6px; margin-top:6px; }
    .edu-helper-logbox { position: fixed; left: 20px; bottom: 20px; width: 420px; max-height: 320px; overflow-y: auto; background: rgba(255,255,255,0.85); color: #fff; padding:10px; border-radius:8px; z-index:2147483647; display:none; font-size:12px; }
    .edu-helper-small { font-size:12px; color:#666; margin-left:6px; }
    .edu-helper-input { width: 160px; padding:4px; border:1px solid #ccc; border-radius:4px; }
  `);

    // ======= 日志功能 =======
    function ensureLogBox() {
        let lb = document.getElementById('edu-helper-logbox');
        if (!lb) {
            lb = document.createElement('div');
            lb.id = 'edu-helper-logbox';
            lb.className = 'edu-helper-logbox';
            lb.innerHTML = '<div style="font-weight:600;margin-bottom:6px;">📋 助手日志 <span id="edu-log-close" style="float:right;cursor:pointer">关闭</span></div><div id="edu-helper-log-content"></div>';
            document.body.appendChild(lb);
            document.getElementById('edu-log-close').onclick = () => { lb.style.display = 'none'; };
        }
        return lb;
    }

    function appendLog(msg) {
        const lb = ensureLogBox();
        const box = document.getElementById('edu-helper-log-content');
        const time = new Date().toLocaleTimeString();
        const line = document.createElement('div');
        line.textContent = `[${time}] ${msg}`;
        box.appendChild(line);
        box.scrollTop = box.scrollHeight;
        console.log('[助手]', msg);
    }

    // ======= 禁止全屏(保留原有实现) =======
    function disableFullscreen() {
        if (Element.prototype.requestFullscreen) Element.prototype.requestFullscreen = () => console.log('阻止 requestFullscreen');
        if (Element.prototype.webkitRequestFullscreen) Element.prototype.webkitRequestFullscreen = () => console.log('阻止 webkitRequestFullscreen');
        if (Element.prototype.mozRequestFullScreen) Element.prototype.mozRequestFullScreen = () => console.log('阻止 mozRequestFullScreen');
        if (Element.prototype.msRequestFullscreen) Element.prototype.msRequestFullscreen = () => console.log('阻止 msRequestFullscreen');

        window.addEventListener('keydown', e => {
            if (e.key === 'F11') {
                e.preventDefault();
                appendLog('阻止 F11 全屏');
            }
        });

        function exitFs() {
            if (document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement) {
                if (document.exitFullscreen) document.exitFullscreen();
                else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
                else if (document.mozCancelFullScreen) document.mozCancelFullScreen();
                else if (document.msExitFullscreen) document.msExitFullscreen();
                appendLog('强制退出全屏');
            }
        }
        setInterval(exitFs, 1000);
    }
    disableFullscreen();

    // ======= 提取 iframe 中的 pdf 和 header(不改动) =======
    function extractPDFinfoFromIframe(iframe) {
        const src = iframe.getAttribute('src');
        if (!src) return null;
        const hashIndex = src.indexOf('#'); if (hashIndex === -1) return null;
        const hash = src.slice(hashIndex + 1);
        const pdfMatch = hash.match(/pdf=([^&]+)/);
        const headerMatch = hash.match(/header=([^&]+)/);
        if (!pdfMatch) return null;
        const pdfUrl = decodeURIComponent(pdfMatch[1]);
        let headers = {};
        if (headerMatch) {
            try { headers = JSON.parse(decodeURIComponent(headerMatch[1])); } catch (e) { console.warn('解析 iframe header 失败', e); }
        }
        return { pdfUrl, headers };
    }

    // ======= 下载 PDF(保留) =======
    function downloadPDF(url, headers = {}, filename = 'file.pdf') {
        appendLog(`开始下载 PDF: ${url}`);
        GM_xmlhttpRequest({
            method: 'GET',
            url,
            headers,
            responseType: 'blob',
            onload(res) {
                try {
                    const blob = res.response;
                    const a = document.createElement('a');
                    a.href = URL.createObjectURL(blob);
                    a.download = filename;
                    a.click();
                    URL.revokeObjectURL(a.href);
                    appendLog(`PDF 已下载:${filename}`);
                } catch (e) {
                    console.error(e);
                    appendLog(`PDF 下载失败:${e.message}`);
                    alert('PDF下载失败,请查看控制台或日志');
                }
            },
            onerror(err) {
                console.error(err);
                appendLog('PDF下载请求出错');
                alert('PDF下载请求出错,请查看控制台或日志');
            }
        });
    }

    // ======= 辅助:获取存储的 Bearer token(优先使用用户配置) =======
    function getStoredToken() {
        const t = GM_getValue('convertapi_token', '');
        if (t && t.length > 0) return t;
        return DEFAULT_BEARER_TOKEN || '';
    }

    // ======= 工具:fetch PDF Blob(保留) =======
    function fetchPdfBlob(pdfUrl, headers = {}) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: pdfUrl,
                headers,
                responseType: 'blob',
                onload: res => resolve(res.response),
                onerror: err => reject(err)
            });
        });
    }

    // ======= 工具:构建 multipart body(保留) =======
    function buildMultipartBody(fileBlob, fileFieldName = 'File', fileName = 'file.pdf', extraFields = { StoreFile: 'true' }) {
        const boundary = '----TamperBoundary' + Date.now().toString(36);
        const CRLF = '\r\n';
        const parts = [];

        for (const k in extraFields) {
            parts.push(`--${boundary}${CRLF}`);
            parts.push(`Content-Disposition: form-data; name="${k}"${CRLF}${CRLF}`);
            parts.push(`${extraFields[k]}${CRLF}`);
        }

        parts.push(`--${boundary}${CRLF}`);
        parts.push(`Content-Disposition: form-data; name="${fileFieldName}"; filename="${fileName}"${CRLF}`);
        parts.push(`Content-Type: application/pdf${CRLF}${CRLF}`);
        parts.push(fileBlob);
        parts.push(CRLF);
        parts.push(`--${boundary}--${CRLF}`);

        return { body: new Blob(parts), boundary };
    }

    function downloadBlob(blob, filename) {
        const a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        a.download = filename;
        a.click();
        URL.revokeObjectURL(a.href);
    }

    // ======= 调用 ConvertAPI(使用存储或默认 token) =======
    async function pdfToPptViaConvertAPI(pdfUrl, headers = {}, outFilename = 'converted.pptx', statusBtn = null) {
        const token = getStoredToken();
        if (!token) {
            alert('未设置 ConvertAPI Bearer token,请点击「设置 API」输入并保存。');
            return;
        }

        try {
            if (statusBtn) statusBtn.textContent = '⏳ 获取 PDF...';
            appendLog(`开始 ConvertAPI 转换:${pdfUrl}`);

            const pdfBlob = await fetchPdfBlob(pdfUrl, headers);
            appendLog(`已获取 PDF Blob,大小 ${pdfBlob.size} bytes`);

            if (statusBtn) statusBtn.textContent = '⏳ 上传到 ConvertAPI...';
            const { body, boundary } = buildMultipartBody(pdfBlob, 'File', outFilename.replace('.pptx', '.pdf'));

            GM_xmlhttpRequest({
                method: 'POST',
                url: CONVERT_API_URL,
                data: body,
                headers: {
                    'Authorization': 'Bearer ' + token,
                    'Content-Type': 'multipart/form-data; boundary=' + boundary
                },
                onprogress(e) {
                    if (statusBtn && e.lengthComputable) {
                        const pct = Math.round((e.loaded / e.total) * 100);
                        statusBtn.textContent = `⬆️ 上传 ${pct}%`;
                    }
                },
                onload(res) {
                    appendLog(`ConvertAPI 返回状态 ${res.status}`);
                    if (res.status >= 200 && res.status < 300) {
                        let data = null;
                        try { data = JSON.parse(res.responseText); } catch (e) { /* ignore */ }

                        if (data && data.Files && data.Files[0] && data.Files[0].Url) {
                            const downloadUrl = data.Files[0].Url;
                            appendLog(`转换成功,文件 URL:${downloadUrl}`);
                            if (statusBtn) statusBtn.textContent = '⬇️ 服务器生成,开始下载...';

                            GM_xmlhttpRequest({
                                method: 'GET',
                                url: downloadUrl,
                                responseType: 'blob',
                                onprogress(evt) {
                                    if (statusBtn && evt.lengthComputable) {
                                        const pct = Math.round((evt.loaded / evt.total) * 100);
                                        statusBtn.textContent = `⬇️ 下载 ${pct}%`;
                                    }
                                },
                                onload(r2) {
                                    downloadBlob(r2.response, outFilename);
                                    appendLog(`PPTX 已下载:${outFilename}`);
                                    if (statusBtn) { statusBtn.textContent = '✅ 完成'; setTimeout(() => statusBtn.textContent = '转换 PPT', 1500); }
                                },
                                onerror(err2) {
                                    console.error('下载 ConvertAPI 结果失败:', err2);
                                    appendLog('下载 ConvertAPI 结果失败');
                                    if (statusBtn) { statusBtn.textContent = '❌ 下载失败'; setTimeout(() => statusBtn.textContent = '转换 PPT', 1500); }
                                }
                            });
                        } else {
                            console.warn('ConvertAPI 返回没有 Files URL', res.responseText);
                            appendLog('ConvertAPI 返回无文件 URL(控制台查看原始响应)');
                            if (statusBtn) { statusBtn.textContent = '❌ 返回异常'; setTimeout(() => statusBtn.textContent = '转换 PPT', 1500); }
                        }
                    } else {
                        console.error('ConvertAPI 返回错误', res);
                        appendLog(`ConvertAPI 返回错误:HTTP ${res.status}`);
                        if (statusBtn) { statusBtn.textContent = '❌ 请求失败'; setTimeout(() => statusBtn.textContent = '转换 PPT', 1500); }
                    }
                },
                onerror(err) {
                    console.error('POST 到 ConvertAPI 出错:', err);
                    appendLog('POST 到 ConvertAPI 出错,详见控制台');
                    if (statusBtn) { statusBtn.textContent = '❌ 请求出错'; setTimeout(() => statusBtn.textContent = '转换 PPT', 1500); }
                }
            });

        } catch (err) {
            console.error('pdfToPptViaConvertAPI 异常:', err);
            appendLog(`转换异常:${err.message || err}`);
            if (statusBtn) { statusBtn.textContent = '❌ 异常'; setTimeout(() => statusBtn.textContent = '转换 PPT', 1500); }
            alert('转换失败(详情查看日志或控制台)');
        }
    }

    // ======= UI:扫描 iframe,生成浮动工具(保留原行为,但加入设置与日志按钮) =======
    function scanForPdfIframes() {
        const iframes = document.querySelectorAll('iframe');
        const infos = [];
        iframes.forEach(f => {
            const info = extractPDFinfoFromIframe(f);
            if (info) infos.push(info);
        });
        return infos;
    }

    function createFloatingUI(pdfInfos) {
        if (document.getElementById('edu-helper-wrapper')) return; // 避免重复创建

        const wrapper = document.createElement('div');
        wrapper.id = 'edu-helper-wrapper';
        wrapper.className = 'edu-helper-wrapper';

        const mainBtn = document.createElement('button');
        mainBtn.className = 'edu-helper-mainbtn';
        mainBtn.title = 'PDF 工具';
        mainBtn.textContent = '📄';

        const panel = document.createElement('div');
        panel.className = 'edu-helper-panel';

        // top control: API 设置 & 日志开关
        const ctrl = document.createElement('div');
        ctrl.className = 'edu-helper-row';
        ctrl.innerHTML = `
      <button id="edu-set-api" class="edu-helper-btn" style="background:#f39c12;color:#fff">设置 API</button>
      <button id="edu-toggle-log" class="edu-helper-btn" style="background:#34495e;color:#fff">打开日志</button>
      <span class="edu-helper-small" id="edu-token-mask"></span>
    `;
        panel.appendChild(ctrl);

        // list area
        const listArea = document.createElement('div');
        listArea.id = 'edu-list-area';
        panel.appendChild(listArea);

        // fill list
        if (pdfInfos.length === 0) {
            listArea.innerText = '未检测到 PDF iframe';
        } else {
            pdfInfos.forEach((info, idx) => {
                const row = document.createElement('div');
                row.className = 'edu-helper-row';

                const title = document.createElement('div');
                title.textContent = `PDF ${idx + 1}`;
                title.style.fontSize = '13px';
                row.appendChild(title);

                const btnDownload = document.createElement('button');
                btnDownload.textContent = '下载 PDF';
                btnDownload.className = 'edu-helper-btn edu-helper-download';
                btnDownload.onclick = () => downloadPDF(info.pdfUrl, info.headers, `pdf_${idx + 1}.pdf`);
                row.appendChild(btnDownload);

                const btnPpt = document.createElement('button');
                btnPpt.textContent = '转换 PPT';
                btnPpt.className = 'edu-helper-btn edu-helper-convert';
                btnPpt.onclick = () => pdfToPptViaConvertAPI(info.pdfUrl, info.headers, `pdf_${idx + 1}.pptx`, btnPpt);
                row.appendChild(btnPpt);

                panel.appendChild(row);
            });
        }

        // attach
        wrapper.appendChild(mainBtn);
        wrapper.appendChild(panel);
        document.body.appendChild(wrapper);

        // show/hide panel
        mainBtn.onclick = () => {
            panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
        };

        // 设置 API 按钮行为
        document.getElementById('edu-set-api').onclick = () => {
            const current = GM_getValue('convertapi_token', '');
            const input = prompt('请输入 ConvertAPI Bearer token(不会明文保存到共享区域,仅保存在本地 Tampermonkey):', current);
            if (input !== null) {
                GM_setValue('convertapi_token', input);
                appendLog('已保存 ConvertAPI token(已隐藏显示)');
                updateTokenMask();
            }
        };

        // 日志开关
        document.getElementById('edu-toggle-log').onclick = () => {
            const lb = ensureLogBox();
            lb.style.display = lb.style.display === 'none' ? 'block' : 'none';
        };

        // 显示已保存 token(遮掩)
        function updateTokenMask() {
            const maskEl = document.getElementById('edu-token-mask');
            const token = GM_getValue('convertapi_token', '');
            if (!token) maskEl.textContent = '(未设置 API)';
            else maskEl.textContent = '(已设置 API,长度 ' + token.length + ')';
        }
        updateTokenMask();
    }

    // ======= 动态监听并启动 UI(只在顶层页面) =======
    if (window.top === window.self) {
        const mo = new MutationObserver(() => {
            const infos = scanForPdfIframes();
            if (infos.length > 0) {
                mo.disconnect();
                createFloatingUI(infos);
                appendLog('检测到 PDF iframe 并创建工具 UI');
            }
        });
        mo.observe(document.body, { childList: true, subtree: true });
    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址