Greasy Fork

来自缓存

Greasy Fork is available in English.

国家中小学智慧教育平台电子课本下载

在国家中小学智慧教育平台网站中添加电子课本下载按钮,下载电子课本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         国家中小学智慧教育平台电子课本下载
// @description  在国家中小学智慧教育平台网站中添加电子课本下载按钮,下载电子课本
// @namespace    https://github.com/BaeKey/smartedu
// @version      0.1.1
// @match        https://basic.smartedu.cn/tchMaterial/detail?contentType=assets_document*
// @icon         https://basic.smartedu.cn/favicon.ico
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 创建按钮
    function createDownloadButton() {
        // 创建按钮元素
        const button = document.createElement('button');
        button.textContent = '下载文档';

        // 设置按钮样式
        button.style.position = 'fixed';
        button.style.position = 'fixed';
        button.style.top = '13vh';
        button.style.right = '0.2vw';
        button.style.padding = '1em 1em';
        button.style.fontSize = '1rem';
        button.style.backgroundColor = '#ff7d24';
        button.style.color = 'white';
        button.style.border = 'none';
        button.style.borderRadius = '5px';
        button.style.cursor = 'pointer';
        button.style.zIndex = '1000';

        // 按钮点击事件
        button.onclick = main;

        // 将按钮添加到页面中
        document.body.appendChild(button);
    }

    window.onload = function() {
        console.log("Window 加载完成,创建下载按钮...");
        createDownloadButton();
    };

    var main = async function() {
        var params = new URLSearchParams(document.location.search);
        var id = params.get("contentId");

        if (id) {
            // 获取下载地址
            var jsonUrl = `https://s-file-2.ykt.cbern.com.cn/zxx/ndrv2/resources/tch_material/details/${id}.json`;
            try {
                let response = await fetch(jsonUrl);
                if (response.ok) {
                    let data = await response.json();
                    let tiItems = data.ti_items || [];
                    let downUrl = tiItems[1].ti_storages[0];
                    // 获取文件名
                    var fileName = document.querySelector("span.fish-breadcrumb-link").innerText;

                    if (fileName && downUrl) {
                        // 获取认证请求头
                         let authHeader = await authEncrypt(downUrl, 'GET');
                         // 发起下载请求
                         downloadWithHeaders(downUrl, fileName, authHeader);
                    }
                }
            }
             catch (error) {
                console.error('JSON 解析出错:', error);
            }

        }
    }

    // 下载函数,携带认证头
    function downloadWithHeaders(url, fileName, authHeader) {
        GM_xmlhttpRequest({
            method: "GET",
            url: url,
            headers: {
                "x-nd-auth": authHeader,
            },
            responseType: "blob",
            onload: function(response) {
                if (response.status === 200) {
                    // 创建一个 Blob 对象,并用来生成下载链接
                    const blob = response.response;
                    const link = document.createElement("a");
                    link.href = URL.createObjectURL(blob);
                    link.download = fileName + ".pdf";
                    link.click();
                } else {
                    console.error('下载失败,状态码:', response.status);
                }
            },
            onerror: function(error) {
                console.error('请求错误:', error);
            }
        });
    }

    // 生成认证的请求头
    function authEncrypt(url, methodType) {
        // 获取当前时间戳(毫秒)
        const currentTimeMs = Date.now();
        // 生成一个 700 到 900 之间的随机数
        // 将参数 diff 转换为整数
        const diff = Math.floor(Math.random() * (900 - 700 + 1)) + 700;
        const diffInt = parseInt(diff, 10);

        // 生成随机字符串
        const characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        let randomStr = '';
        for (let i = 0; i < 8; i++) {
            randomStr += characters.charAt(Math.floor(Math.random() * characters.length));
        }

        // 拼接时间戳、整数部分和随机字符串
        const nonce = `${currentTimeMs + diffInt}:${randomStr}`;

        // 解析 URL 并构造相对路径
        const urlObj = new URL(url);
        const relativePath = urlObj.pathname + (urlObj.search || "") + (urlObj.hash || "");
        const authority = urlObj.host;

        // 构造签名字符串
        const signatureString = `${nonce}\n${methodType}\n${relativePath}\n${authority}\n`;

        // 获取 accessToken 和 macKey
        let { accessToken, macKey } = getAccessTokenAndMacKeyFromLocalStorage();
        // 计算 HMAC-SHA256
        const macBytes = new TextEncoder().encode(macKey);
        const signatureBytes = new TextEncoder().encode(signatureString);


        return crypto.subtle.importKey(
            "raw",
            macBytes,
            { name: "HMAC", hash: "SHA-256" },
            false,
            ["sign"]
        ).then(key => crypto.subtle.sign("HMAC", key, signatureBytes))
         .then(hmacBuffer => {
            // 转换为 Base64 编码的字符串
            const base64Encoded = btoa(String.fromCharCode(...new Uint8Array(hmacBuffer)));

            // 返回认证签名字符串
            return `MAC id="${accessToken}",nonce="${nonce}",mac="${base64Encoded}"`;
        });
    }

    // 获取access_token
    function getAccessTokenAndMacKeyFromLocalStorage() {
        // 遍历 localStorage 寻找符合条件的键
        for (let i = 0; i < localStorage.length; i++) {
            const key = localStorage.key(i);

            // 检查键是否以 "ND_UC_AUTH" 开头并以 "token" 结尾
            if (key.startsWith("ND_UC_AUTH") && key.endsWith("token")) {
                try {
                    // 获取键对应的值并解析为 JSON 对象
                    const valueJson = JSON.parse(localStorage.getItem(key));

                    // 检查是否存在 "value" 键且对应值也是 JSON
                    if (valueJson && valueJson.value) {
                        const innerValue = JSON.parse(valueJson.value);

                        // 获取 "access_token" 值
                        const accessToken = innerValue.access_token;
                        const macKey = innerValue.mac_key;
                        if (accessToken && macKey) {
                            return { accessToken, macKey };
                        } else {
                            console.warn("access_token 未找到");
                            return null;
                        }
                    } else {
                        console.warn("未找到 value 字段或 JSON 格式错误");
                        return null;
                    }
                } catch (error) {
                    console.error("JSON 解析出错:", error);
                    return null;
                }
            }
        }
        console.warn("未找到符合条件的键");
        return null;
    }
})();