Greasy Fork is available in English.

Wenku Doc Downloader

对文档截图,合并为纯图片PDF。有限地支持(1)豆丁网(2)道客巴巴(3)360个人图书馆(4)得力文库(5)MBA智库(6)爱问文库(7)原创力文档(8)读根网(9)国标网(10)安全文库网(11)人人文库(12)云展网(13)360文库(14)技工教育网(15)文库吧(16)中国社会科学文库(17)金锄头(18)自然资源标准。预览多少页,导出多少页。额外支持(1)食典通(2)JJG 计量技术规范,详见下方说明。

// ==UserScript==
// @name         Wenku Doc Downloader
// @namespace    http://tampermonkey.net/
// @version      1.10.1
// @description  对文档截图,合并为纯图片PDF。有限地支持(1)豆丁网(2)道客巴巴(3)360个人图书馆(4)得力文库(5)MBA智库(6)爱问文库(7)原创力文档(8)读根网(9)国标网(10)安全文库网(11)人人文库(12)云展网(13)360文库(14)技工教育网(15)文库吧(16)中国社会科学文库(17)金锄头(18)自然资源标准。预览多少页,导出多少页。额外支持(1)食典通(2)JJG 计量技术规范,详见下方说明。
// @author       2690874578@qq.com
// @match        *://*.docin.com/p-*
// @match        *://docimg1.docin.com/?wk=true
// @match        *://ishare.iask.sina.com.cn/f/*
// @match        *://ishare.iask.com/f/*
// @match        *://swf.ishare.down.sina.com.cn/?path=*
// @match        *://swf.ishare.down.sina.com.cn/?wk=true
// @match        *://www.deliwenku.com/p-*
// @match        *://file.deliwenku.com/?num=*
// @match        *://file3.deliwenku.com/?num=*
// @match        *://www.doc88.com/p-*
// @match        *://www.360doc.com/content/*
// @match        *://doc.mbalib.com/view/*
// @match        *://www.dugen.com/p-*
// @match        *://max.book118.com/html/*
// @match        *://openapi.book118.com/?*
// @match        *://view-cache.book118.com/pptView.html?*
// @match        *://*.book118.com/?readpage=*
// @match        *://c.gb688.cn/bzgk/gb/showGb?*
// @match        *://www.safewk.com/p-*
// @match        *://www.renrendoc.com/paper/*
// @match        *://www.renrendoc.com/p-*
// @match        *://www.yunzhan365.com/basic/*
// @match        *://book.yunzhan365.com/*index.html*
// @match        *://wenku.so.com/d/*
// @match        *://jg.class.com.cn/cms/resourcedetail.htm?contentUid=*
// @match        *://preview.imm.aliyuncs.com/index.html?url=*/jgjyw/*
// @match        *://www.wenkub.com/p-*.html*
// @match        *://*/manuscripts/?*
// @match        *://gwfw.sdlib.com:8000/*
// @match        *://www.jinchutou.com/shtml/view-*
// @match        *://www.jinchutou.com/p-*
// @match        *://www.nrsis.org.cn/*/read/*
// @match        https://xianxiao.ssap.com.cn/readerpdf/?id=*
// @match        https://xianxiao.ssap.com.cn/index/rpdf/read/id/*/catalog_id/0.html?file=*
// @require      https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/jspdf/2.5.1/jspdf.umd.min.js
// @require      https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/html2canvas/1.4.1/html2canvas.min.js
// @icon         https://s2.loli.net/2022/01/12/wc9je8RX7HELbYQ.png
// @icon64       https://s2.loli.net/2022/01/12/tmFeSKDf8UkNMjC.png
// @grant        none
// @run-at       document-idle
// @license      GPL-3.0-only
// @create       2021-11-22
// @note         1. 应对 sklib 的 AES 加密
// ==/UserScript==


(function () {
    'use strict';

    /**
     * 基于 window.postMessage 通信的套接字对象
     */
    class Socket {
        /**
         * 创建套接字对象
         * @param {Window} target 目标窗口
         */
        constructor(target) {
            if (!(target.window && (target === target.window))) {
                console.log(target);
                throw new Error(`target is not a [Window Object]`); 
            }
            this.target = target;
            this.connected = false;
            this.listeners = new Set();
        }

        get [Symbol.toStringTag]() { return "Socket"; }

        /**
         * 向目标窗口发消息
         * @param {*} message 
         */
        talk(message) {
            if (!this.target) {
                throw new TypeError(
                    `socket.target is not a window: ${this.target}`
                );
            }
            this.target.postMessage(message, "*");
        }

        /**
         * 添加捕获型监听器,返回实际添加的监听器
         * @param {Function} listener (e: MessageEvent) => {...}
         * @param {boolean} once 是否在执行后自动销毁,默认 false;如为 true 则使用自动包装过的监听器
         * @returns {Function} listener
         */
        listen(listener, once=false) {
            if (this.listeners.has(listener)) {
                return;
            }

            let real_listener = listener;
            // 包装监听器
            if (once) {
                const self = this;
                function wrapped(e) {
                    listener(e);
                    self.notListen(wrapped);
                }
                real_listener = wrapped;
            }
            
            // 添加监听器
            this.listeners.add(real_listener);
            window.addEventListener(
                "message", real_listener, true
            );
            return real_listener;
        }

        /**
         * 移除socket上的捕获型监听器
         * @param {Function} listener (e: MessageEvent) => {...}
         */
        notListen(listener) {
            console.log(listener);
            console.log(
                "listener delete operation:",
                this.listeners.delete(listener)
            );
            window.removeEventListener("message", listener, true);
        }

        /**
         * 检查对方来信是否为pong消息
         * @param {MessageEvent} e 
         * @param {Function} resolve 
         */
        _on_pong(e, resolve) {
            // 收到pong消息
            if (e.data.pong) {
                this.connected = true;
                this.listeners.forEach(
                    listener => listener.ping ? this.notListen(listener) : 0
                );
                console.log("Client: Connected!\n" + new Date());
                resolve(this);
            }
        }

        /**
         * 向对方发送ping消息
         * @returns {Promise<Socket>}
         */
        _ping() {
            return new Promise((resolve, reject) => {
                // 绑定pong检查监听器
                const listener = this.listen(
                    e => this._on_pong(e, resolve)
                );
                listener.ping = true;

                // 5分钟后超时
                setTimeout(
                    () => reject(new Error(`Timeout Error during receiving pong (>5min)`)),
                    5 * 60 * 1000
                );
                // 发送ping消息
                this.talk({ ping: true });
            });
        }

        /**
         * 检查对方来信是否为ping消息
         * @param {MessageEvent} e 
         * @param {Function} resolve 
         */
        _on_ping(e, resolve) {
            // 收到ping消息
            if (e.data.ping) {
                this.target = e.source;
                this.connected = true;
                this.listeners.forEach(
                    listener => listener.pong ? this.notListen(listener) : 0
                );
                console.log("Server: Connected!\n" + new Date());
                
                // resolve 后期约状态无法回退
                // 但后续代码仍可执行
                resolve(this);
                // 回应pong消息
                this.talk({ pong: true });
            }
        }

        /**
         * 当对方来信是为ping消息时回应pong消息
         * @returns {Promise<Socket>}
         */
        _pong() {
            return new Promise(resolve => {
                // 绑定ping检查监听器
                const listener = this.listen(
                    e => this._on_ping(e, resolve)
                );
                listener.pong = true;
            });
        }

        /**
         * 连接至目标窗口
         * @param {boolean} talk_first 是否先发送ping消息
         * @param {Window} target 目标窗口
         * @returns {Promise<Socket>}
         */
        connect(talk_first) {
            // 先发起握手
            if (talk_first) {
                return this._ping();
            }
            // 后发起握手
            return this._pong();
        }
    }


    const base = {
        Socket,

        init_gbk_encoder() {

            let table;

            function initGbkTable() {
                // https://en.wikipedia.org/wiki/GBK_(character_encoding)#Encoding
                const ranges = [
                    [0xA1, 0xA9,  0xA1, 0xFE],
                    [0xB0, 0xF7,  0xA1, 0xFE],
                    [0x81, 0xA0,  0x40, 0xFE],
                    [0xAA, 0xFE,  0x40, 0xA0],
                    [0xA8, 0xA9,  0x40, 0xA0],
                    [0xAA, 0xAF,  0xA1, 0xFE],
                    [0xF8, 0xFE,  0xA1, 0xFE],
                    [0xA1, 0xA7,  0x40, 0xA0],
                ];
                const codes = new Uint16Array(23940);
                let i = 0;

                for (const [b1Begin, b1End, b2Begin, b2End] of ranges) {
                    for (let b2 = b2Begin; b2 <= b2End; b2++) {
                        if (b2 !== 0x7F) {
                            for (let b1 = b1Begin; b1 <= b1End; b1++) {
                                codes[i++] = b2 << 8 | b1;
                            }
                        }
                    }
                }
                table = new Uint16Array(65536);
                table.fill(0xFFFF);

                const str = new TextDecoder('gbk').decode(codes);
                for (let i = 0; i < str.length; i++) {
                    table[str.charCodeAt(i)] = codes[i];
                }
            }

            const defaultOnAlloc = (len) => new Uint8Array(len);
            const defaultOnError = () => 63;   // '?'

            /**
             * 字符串编码为gbk字节串
             * @param {string} str
             * @param {Function} onError 处理编码失败时返回字符替代值的函数,默认是返回 63('?') 的函数
             * @returns {Uint8Array}
             */
            return function(str, onError=null) {
                if (!table) {
                    initGbkTable();
                }
                const onAlloc = defaultOnAlloc;
                onError = onError === null ? defaultOnError : onError;

                const buf = onAlloc(str.length * 2);
                let n = 0;

                for (let i = 0; i < str.length; i++) {
                    const code = str.charCodeAt(i);
                    if (code < 0x80) {
                        buf[n++] = code;
                        continue;
                    }

                    const gbk = table[code];

                    if (gbk !== 0xFFFF) {
                        buf[n++] = gbk;
                        buf[n++] = gbk >> 8;
                    }
                    
                    else if (code === 8364) {
                        // 8364 == '€'.charCodeAt(0)
                        // Code Page 936 has a single-byte euro sign at 0x80
                        buf[n++] = 0x80;
                    }
                    
                    else {
                        const ret = onError(i, str);
                        if (ret === -1) {
                            break;
                        }
                        if (ret > 0xFF) {
                            buf[n++] = ret;
                            buf[n++] = ret >> 8;
                        } else {
                            buf[n++] = ret;
                        }
                    }
                }
                return buf.subarray(0, n)
            }
        },

        /**
         * Construct a table with table[i] as the length of the longest prefix of the substring 0..i
         * @param {Array<number>} arr 
         * @returns {Array<number>}
         */
        longest_prefix: function(arr) {

            // create a table of size equal to the length of `str`
            // table[i] will store the prefix of the longest prefix of the substring str[0..i]
            let table = new Array(arr.length);
            let maxPrefix = 0;
            // the longest prefix of the substring str[0] has length
            table[0] = 0;

            // for the substrings the following substrings, we have two cases
            for (let i = 1; i < arr.length; i++) {
                // case 1. the current character doesn't match the last character of the longest prefix
                while (maxPrefix > 0 && arr[i] !== arr[maxPrefix]) {
                    // if that is the case, we have to backtrack, and try find a character  that will be equal to the current character
                    // if we reach 0, then we couldn't find a chracter
                    maxPrefix = table[maxPrefix - 1];
                }
                // case 2. The last character of the longest prefix matches the current character in `str`
                if (arr[maxPrefix] === arr[i]) {
                    // if that is the case, we know that the longest prefix at position i has one more character.
                    // for example consider `-` be any character not contained in the set [a-c]
                    // str = abc----abc
                    // consider `i` to be the last character `c` in `str`
                    // maxPrefix = will be 2 (the first `c` in `str`)
                    // maxPrefix now will be 3
                    maxPrefix++;
                    // so the max prefix for table[9] is 3
                }
                table[i] = maxPrefix;
            }
            return table;
        },

        // 用于取得一次列表中所有迭代器的值
        getAllValus: function(iterators) {
            if (iterators.length === 0) {
                return [true, []];
            }
        
            let values = [];
            for (let iterator of iterators) {
                let {value, done} = iterator.next();
                if (done) {
                    return [true, []];
                }
                values.push(value);
            }
            return [false, values];
        },

        /**
         * 使用过时的execCommand复制文字
         * @param {string} text
         */
        oldCopy: function(text) {
            document.oncopy = function(event) {
                event.clipboardData.setData('text/plain', text);
                event.preventDefault();
            };
            document.execCommand('Copy', false, null);
        },

        b64ToUint6: function(nChr) {
            return nChr > 64 && nChr < 91 ?
                nChr - 65
                : nChr > 96 && nChr < 123 ?
                nChr - 71
                : nChr > 47 && nChr < 58 ?
                nChr + 4
                : nChr === 43 ?
                62
                : nChr === 47 ?
                63
                :
                0;
        },

        /**
         * 元素选择器
         * @param {string} selector 选择器
         * @returns {Array<HTMLElement>} 元素列表
         */
        $: function(selector) {
            const self = this?.querySelectorAll ? this : document;
            return [...self.querySelectorAll(selector)];
        },

        /**
         * 安全元素选择器,直到元素存在时才返回元素列表,最多等待5秒
         * @param {string} selector 选择器
         * @returns {Promise<Array<HTMLElement>>} 元素列表
         */
        $$: async function(selector) {
            const self = this?.querySelectorAll ? this : document;

            for (let i = 0; i < 10; i++) {
                let elems = [...self.querySelectorAll(selector)];
                if (elems.length > 0) {
                    return elems;
                }
                await new Promise(r => setTimeout(r, 500));
            }
            throw Error(`"${selector}" not found in 5s`);
        },

        /**
         * 将2个及以上的空白字符(除了换行符)替换成一个空格
         * @param {string} text 
         * @returns {string}
         */
        stripBlanks: function(text) {
            return text
                .replace(/([^\r\n])(\s{2,})(?=[^\r\n])/g, "$1 ")
                .replace(/\n{2,}/, "\n");
        },

        /**
         * 复制属性(含访问器)到 target
         * @param {Object} target 
         * @param  {...Object} sources 
         * @returns 
         */
        superAssign: function(target, ...sources) {
            sources.forEach(source => 
                Object.defineProperties(
                    target, Object.getOwnPropertyDescriptors(source)
                )
            );
            return target;
        },

        makeCRC32: function() {
            function makeCRCTable() {
                let c;
                let crcTable = [];
                for(var n =0; n < 256; n++){
                    c = n;
                    for(var k =0; k < 8; k++){
                        c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
                    }
                    crcTable[n] = c;
                }
                return crcTable;
            }

            const crcTable = makeCRCTable();

            /**
             * @param {string} str
             * @returns {number}
             */
            return function(str) {
                let crc = 0 ^ (-1);
            
                for (var i = 0; i < str.length; i++ ) {
                    crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xFF];
                }
            
                return (crc ^ (-1)) >>> 0;
            };
        }
    };

    const box = `
<div class="wk-box">
    <section class="btns-sec">
        <p class="logo_tit">Wenku Doc Downloader</p>
        <button class="btn-1">展开文档 😈</button>
        <button class="btn-2">空按钮 2</button>
        <button class="btn-3">空按钮 3</button>
        <button class="btn-4">空按钮 4</button>
        <button class="btn-5">空按钮 5</button>
    </section>
    <p class="wk-fold-btn unfold"></p>
</div>
`;

    const style = `
<style class="wk-style">
    .wk-fold-btn {
        position: fixed;
        left: 151px;
        top: 36%;
        user-select: none;
        font-size: large;
        z-index: 1001;
    }

    .wk-fold-btn::after {
        content: "🐵";
    }
    
    .wk-fold-btn.folded {
        left: 20px;
    }
    
    .wk-fold-btn.folded::after {
        content: "🙈";
    }

    .wk-box {
        position: fixed;
        width: 154px;
        left: 10px;
        top: 32%;
        z-index: 1000;
    }

    .btns-sec {
        background: #E7F1FF;
        border: 2px solid #1676FF;
        padding: 0px 0px 10px 0px;
        font-weight: 600;
        border-radius: 2px;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
            'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji',
            'Segoe UI Emoji', 'Segoe UI Symbol';
    }

    .btns-sec.folded {
        display: none;
    }

    .logo_tit {
        width: 100%;
        background: #1676FF;
        text-align: center;
        font-size: 12px;
        color: #E7F1FF;
        line-height: 40px;
        height: 40px;
        margin: 0 0 16px 0;
    }

    .btn-1 {
        display: block;
        width: 128px;
        height: 28px;
        background: linear-gradient(180deg, #00E7F7 0%, #FEB800 0.01%, #FF8700 100%);
        border-radius: 4px;
        color: #fff;
        font-size: 12px;
        border: none;
        outline: none;
        margin: 8px auto;
        font-weight: bold;
        cursor: pointer;
        opacity: .9;
    }

    .btn-2 {
        display: none;
        width: 128px;
        height: 28px;
        background: #07C160;
        border-radius: 4px;
        color: #fff;
        font-size: 12px;
        border: none;
        outline: none;
        margin: 8px auto;
        font-weight: bold;
        cursor: pointer;
        opacity: .9;
    }

    .btn-3 {
        display: none;
        width: 128px;
        height: 28px;
        background: #FA5151;
        border-radius: 4px;
        color: #fff;
        font-size: 12px;
        border: none;
        outline: none;
        margin: 8px auto;
        font-weight: bold;
        cursor: pointer;
        opacity: .9;
    }

    .btn-4 {
        display: none;
        width: 128px;
        height: 28px;
        background: #1676FF;
        border-radius: 4px;
        color: #fff;
        font-size: 12px;
        border: none;
        outline: none;
        margin: 8px auto;
        font-weight: bold;
        cursor: pointer;
        opacity: .9;
    }

    .btn-5 {
        display: none;
        width: 128px;
        height: 28px;
        background: #ff6600;
        border-radius: 4px;
        color: #fff;
        font-size: 12px;
        border: none;
        outline: none;
        margin: 8px auto;
        font-weight: bold;
        cursor: pointer;
        opacity: .9;
    }


    .btns-sec button:hover {
        opacity: 0.8;
    }

    .btns-sec button:active{
        opacity: 1;
    }

    .btns-sec button[disabled] {
        cursor: not-allowed;
        opacity: 1;
        filter: grayscale(1);
    }

    .wk-popup-container {
        height: 100vh;
        width: 100vw;
        display: flex;
        flex-direction: column;
        justify-content: space-around;
        z-index: 999;
        background: 0 0;
    }

    .wk-popup-head {
        font-size: 1.5em;
        margin-bottom: 12px
    }

    .wk-card {
        background: #fff;
        background-image: linear-gradient(48deg, #fff 0, #e5efe9 100%);
        border-top-right-radius: 16px;
        border-bottom-left-radius: 16px;
        box-shadow: -20px 20px 35px 1px rgba(10, 49, 86, .18);
        display: flex;
        flex-direction: column;
        padding: 32px;
        margin: 0;
        max-width: 400px;
        width: 100%
    }

    .content-wrapper {
        font-size: 1.1em;
        margin-bottom: 44px
    }

    .content-wrapper:last-child {
        margin-bottom: 0
    }

    .wk-button {
        align-items: center;
        background: #e5efe9;
        border: 1px solid #5a72b5;
        border-radius: 4px;
        color: #121943;
        cursor: pointer;
        display: flex;
        font-size: 1em;
        font-weight: 700;
        height: 40px;
        justify-content: center;
        width: 150px
    }

    .wk-button:focus {
        border: 2px solid transparent;
        box-shadow: 0 0 0 2px #121943;
        outline: solid 4px transparent
    }

    .link {
        color: #121943
    }

    .link:focus {
        box-shadow: 0 0 0 2px #121943
    }

    .input-wrapper {
        display: flex;
        flex-direction: column
    }

    .input-wrapper .label {
        align-items: baseline;
        display: flex;
        font-weight: 700;
        justify-content: space-between;
        margin-bottom: 8px
    }

    .input-wrapper .optional {
        color: #5a72b5;
        font-size: .9em
    }

    .input-wrapper .input {
        border: 1px solid #5a72b5;
        border-radius: 4px;
        height: 40px;
        padding: 8px
    }

    .modal-header {
        align-items: baseline;
        display: flex;
        justify-content: space-between
    }

    .close {
        background: 0 0;
        border: none;
        cursor: pointer;
        display: flex;
        height: 16px;
        text-decoration: none;
        width: 16px
    }

    .close svg {
        width: 16px
    }

    .modal-wrapper {
        background: rgba(0, 0, 0, .7);
    }

    #wk-popup {
        opacity: 0;
        transition: opacity .25s ease-in-out;
        display: none;
        flex-direction: row;
        justify-content: space-around;
    }

    #wk-popup:target {
        opacity: 1;
        display: flex;
    }

    #wk-popup:target .modal-body {
        opacity: 1;
        transform: translateY(1);
    }

    #wk-popup .modal-body {
        max-width: 500px;
        opacity: 0;
        transform: translateY(-3vh);
        transition: opacity .25s ease-in-out;
        width: 100%;
        z-index: 1
    }

    .outside-trigger {
        bottom: 0;
        cursor: default;
        left: 0;
        position: fixed;
        right: 0;
        top: 0;
    }
</style>
`;

    const popup = `
<div class="wk-popup-container">
    <div class='modal-wrapper' id='wk-popup'>
        <div class='modal-body wk-card'>
            <div class='modal-header'>
                <h2 class='wk-popup-head'>下载进度条</h2>
                <a href='#!' role='wk-button' class='close' aria-label='close this modal'>
                    <svg viewBox='0 0 24 24'>
                        <path
                            d='M24 20.188l-8.315-8.209 8.2-8.282-3.697-3.697-8.212 8.318-8.31-8.203-3.666 3.666 8.321 8.24-8.206 8.313 3.666 3.666 8.237-8.318 8.285 8.203z'>
                        </path>
                    </svg>
                </a>
            </div>
            <p class='wk-popup-body'>正在初始化内容...</p>
        </div>
        <a href='#!' class='outside-trigger'></a>
    </div>
</div>
`;

    globalThis.wk$ = base.$;
    globalThis.wk$$ = base.$$;


    const utils = {
        Socket: base.Socket,

        PDF_LIB_URL: "https://cdn.staticfile.org/pdf-lib/1.17.1/pdf-lib.min.js",

        encode_to_gbk: base.init_gbk_encoder(),

        print: function(...args) {
            const time = new Date().toTimeString().slice(0, 8);
            console.info(`[wk ${time}]`, ...args);
        },

        /**
         * 字节串转b64字符串
         * @param {Uint8Array} bytes 
         * @returns {Promise<string>}
         */
        bytes_to_b64: function(bytes) {
            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onerror = () => reject(new Error("转换失败", { cause: bytes }));
                reader.onloadend = () => resolve(reader.result.split(",")[1]);
                reader.readAsDataURL(new Blob([bytes]));
            });
        },

        /**
         * 以指定原因弹窗提示并抛出错误
         * @param {string} reason 
         */
        raise: function(reason) {
            alert(reason);
            throw new Error(reason);
        },

        /**
         * 将错误定位转为可读的字符串
         * @param {Error} err 
         * @returns {string}
         */
        get_stack: function(err) {
            let stack = `${err.stack}`;
            const matches = stack.matchAll(/at .+?( [(].+[)])/g);

            for (const group of matches) {
                stack = stack.replace(group[1], "");
            }
            return stack.trim();
        },

        /**
         * 合并多个PDF
         * @param {Array<ArrayBuffer | Uint8Array>} pdfs 
         * @param {Function} loop_fn
         * @param {Window} win
         * @returns {Promise<Uint8Array>}
         */
        join_pdfs: async function(pdfs, loop_fn=null, win=null) {
            const _win = win || window;
            if (!_win.PDFLib) {
                await this.load_web_script(this.PDF_LIB_URL);
            }

            const combined = await PDFLib.PDFDocument.create();

            for (const [i, buffer] of this.enumerate(pdfs)) {
                const pdf = await PDFLib.PDFDocument.load(buffer);
                const pages = await combined.copyPages(
                    pdf, pdf.getPageIndices()
                );

                for (const page of pages) {
                    combined.addPage(page);
                }

                if (loop_fn) {
                    // 如有,则使用自定义钩子函数
                    loop_fn();
                } else {
                    // 否则使用旧版 popup
                    this.update_popup(`已经合并 ${i + 1} 组`);
                }
            }

            return await combined.save();
        },

        /**
         * raise an error for status which is not in [200, 299] 
         * @param {Response} response 
         */
        raise_for_status(response) {
            if (!response.ok) {
                throw new Error(
                    `Fetch Error with status code: ${response.status}`
                );
            }
        },

        /**
         * 计算 str 的 CRC32 摘要(number)
         * @param {string} str
         * @returns {number}
         */
        crc32: base.makeCRC32(),

        /**
         * 返回函数参数定义
         * @param {Function} fn 
         * @param {boolean} print 是否打印到控制台,默认 true
         * @returns {string | undefined}
         */
        help: function(fn, print=true) {
            if (!(fn instanceof Function))
                throw new Error(`fn must be a function`);

            const
                _fn = fn.__func__ || fn,
                ARROW_ARG = /^([^(]+?)=>/,
                FN_ARGS = /^[^(]*\(\s*([^)]*)\)/m,
                STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,
                fn_text = Function.prototype.toString.call(_fn).replace(STRIP_COMMENTS, ''),
                args = fn_text.match(ARROW_ARG) || fn_text.match(FN_ARGS),
                // 如果自带 doc,优先使用,否则使用源码
                doc = fn.__doc__ ? fn.__doc__ : args[0];
            
            if (!print) return base.stripBlanks(doc);

            const color = (window.matchMedia &&
                    window.matchMedia('(prefers-color-scheme: dark)').matches
                ) ;
            console.log("%c" + doc, `color: ${color}; font: small italic`);
        },

        /**
         * 字节数组转十六进制字符串
         * @param {Uint8Array} arr 
         * @returns {string}
         */
        hex_bytes: function(arr) {
            return Array.from(arr)
                .map(byte => byte.toString(16).padStart(2, "0"))
                .join("");
        },

        /**
         * 取得对象类型
         * @param {*} obj 
         * @returns {string} class
         */
        classof: function(obj) {
            return Object
                .prototype
                .toString
                .call(obj)
                .slice(8, -1);
        },

        /**
         * 随机改变字体颜色、大小、粗细
         * @param {HTMLElement} elem 
         */
        emphasize_text: function(elem) {
            const rand = Math.random;
            elem.style.cssText = `
            font-weight: ${200 + parseInt(700 * rand())};
            font-size: ${(1 + rand()).toFixed(1)}em;
            color: hsl(${parseInt(360 * rand())}, ${parseInt(40 + 60 * rand())}%, ${parseInt(60 * rand())}%);
            background-color: yellow;`;
        },

        /**
         * 等待直到 DOM 节点停止变化
         * @param {HTMLElement} elem 监听节点 
         * @param {number} timeout 超时毫秒数
         * @returns {Promise<MutationObserver>} observer
         */
        until_stop: async function(elem, timeout=2000) {
            // 创建用于共享的监听器
            let observer;
            // 创建超时 Promise
            const timeout_promise = new Promise((_, reject) => {
                setTimeout(() => {
                    // 停止监听、释放资源
                    observer.disconnect();
                    const error = new Error(
                        `Timeout Error occured on listening DOM mutation (max ${timeout}ms)`,
                        { cause: elem }
                    );
                    reject(error);
                }, timeout);
            });
            
            // 开始元素节点变动监听
            return Promise.race([
                new Promise(resolve => {
                    // 创建监听器
                    observer = new MutationObserver(
                        (_, observer) => {
                            // DOM 变动结束后终止监听、释放资源
                            observer.disconnect();
                            // 返回监听器
                            resolve(observer);
                        }
                    );
                    // 开始监听目标节点
                    observer.observe(elem, {
                        subtree: true,
                        childList: true,
                        attributes: true
                    });
                }),
                timeout_promise,
            ])
            .catch(error => {
                if (`${error}`.includes("Timeout Error")) {
                    return observer;
                }
                console.error(error);
                throw error;
            });
        },

        /**
         * Find all the patterns that matches in a given string `str`
         * this algorithm is based on the Knuth–Morris–Pratt algorithm. Its beauty consists in that it performs the matching in O(n)
         * @param {Array<number>} arr 
         * @param {Array<number>} sub_arr 
         * @returns {Array<number>}
         */
        kmp_matching: function(arr, sub_arr) {
            // find the prefix table in O(n)
            let prefixes = base.longest_prefix(sub_arr);
            let matches = [];

            // `j` is the index in `P`
            let j = 0;
            // `i` is the index in `S`
            let i = 0;
            while (i < arr.length) {
                // Case 1.  S[i] == P[j] so we move to the next index in `S` and `P`
                if (arr[i] === sub_arr[j]) {
                    i++;
                    j++;
                }
                // Case 2.  `j` is equal to the length of `P`
                // that means that we reached the end of `P` and thus we found a match
                if (j === sub_arr.length) {
                    matches.push(i - j);
                    // Next we have to update `j` because we want to save some time
                    // instead of updating to j = 0 , we can jump to the last character of the longest prefix well known so far.
                    // j-1 means the last character of `P` because j is actually `P.length`
                    // e.g.
                    // S =  a b a b d e
                    // P = `a b`a b
                    // we will jump to `a b` and we will compare d and a in the next iteration
                    // a b a b `d` e
                    //     a b `a` b
                    j = prefixes[j - 1];
                }
                // Case 3.
                // S[i] != P[j] There's a mismatch!
                else if (arr[i] !== sub_arr[j]) {
                    // if we have found at least a character in common, do the same thing as in case 2
                    if (j !== 0) {
                        j = prefixes[j - 1];
                    } else {
                        // otherwise, j = 0, and we can move to the next character S[i+1]
                        i++;
                    }
                }
            }

            return matches;
        },

        /**
             * 用文件头切断文件集合体
             * @param {Uint8Array} bytes 
             * @param {Uint8Array} head 默认 null,即使用 data 前 8 字节
             * @returns {Array<Uint8Array>}
             */
        split_files_by_head: function(bytes, head=null) {
            const sub = bytes.subarray || bytes.slice;
            head = head || sub.call(bytes, 0, 8);
            
            const indexes = this.kmp_matching(bytes, head);
            const size = indexes.length;
            indexes.push(bytes.length);

            const parts = new Array(size);
            for (let i = 0; i < size; i++) {
                parts[i] = sub.call(bytes, indexes[i], indexes[i+1]);
            }
            // 返回结果数组
            return parts;
        },

        /**
         * 函数装饰器:仅执行一次 func
         */
        once: function(fn) {
            let used = false;
            return function() {
                if (!used) {
                    used = true;
                    return fn();
                }
            }
        },

        /**
         * 返回一个包含计数器的迭代器, 其每次迭代值为 [index, value]
         * @param {Iterable} iterable 
         * @returns 
         */
        enumerate: function* (iterable) {
            let i = 0;
            for (let value of iterable) {
                yield [i, value];
                i++;
            }
        },

        /**
         * 同步的迭代若干可迭代对象
         * @param  {...Iterable} iterables 
         * @returns 
         */
        zip: function* (...iterables) {
            // 强制转为迭代器
            let iterators = iterables.map(
                iterable => iterable[Symbol.iterator]()
            );

            // 逐次迭代
            while (true) {
                const [done, values] = base.getAllValus(iterators);
                if (done) {
                    return;
                }
                if (values.length === 1) {
                    yield values[0];
                } else {
                    yield values;
                }
            }
        },

        /**
         * 返回指定范围整数生成器
         * @param {number} end 如果只提供 end, 则返回 [0, end)
         * @param {number} end2 如果同时提供 end2, 则返回 [end, end2)
         * @param {number} step 步长, 可以为负数,不能为 0
         * @returns 
         */
        range: function*(end, end2=null, step=1) {
            // 参数合法性校验
            if (step === 0) {
                throw new RangeError("step can't be zero");
            }
            const len = end2 - end;
            if (end2 && len && step && (len * step < 0)) {
                throw new RangeError(`[${end}, ${end2}) with step ${step} is invalid`);
            }

            // 生成范围
            end2 = end2 === null ? 0 : end2;
            let [small, big] = [end, end2].sort((a, b) => a - b);
            // 开始迭代
            if (step > 0) {
                for (let i = small; i < big; i += step) {
                    yield i;
                }
            } else {
                for (let i = big; i > small; i += step) {
                    yield i;
                }
            }    },

        /**
         * 获取整个文档的全部css样式
         * @returns {string} css text
         */
        get_all_styles: function() {
            let styles = [];
            for (let sheet of document.styleSheets) {
                let rules;
                try {
                    rules = sheet.cssRules;
                } catch(e) {
                    if (!(e instanceof DOMException)) {
                        console.error(e);
                    }
                    continue;
                }

                for (let rule of rules) {
                    styles.push(rule.cssText);   
                }
            }
            return styles.join("\n\n");
        },

        /**
         * 复制text到剪贴板
         * @param {string} text 
         * @returns 
         */
        copy_text: function(text) {
            // 输出到控制台和剪贴板
            console.log(
                text.length > 20 ?
                    text.slice(0, 21) + "..." : text
            );
            
            if (!navigator.clipboard) {
                base.oldCopy(text);
                return;
            }
            navigator.clipboard
                .writeText(text)
                .catch(_ => base.oldCopy(text));
        },

        /**
         * 复制媒体到剪贴板
         * @param {Blob} blob
         */
        copy: async function(blob) {
            const data = [new ClipboardItem({ [blob.type]: blob })];
            try {
                await navigator.clipboard.write(data);
                console.log(`${blob.type} 成功复制到剪贴板`);
            } catch (err) {
                console.error(err.name, err.message);
            }
        },

        /**
         * 创建并下载文件
         * @param {string} file_name 文件名
         * @param {ArrayBuffer | ArrayBufferView | Blob | string} content 内容
         * @param {string} type 媒体类型,需要符合 MIME 标准 
         */
        save: function(file_name, content, type="") {
            if (!type && (content instanceof Blob)) {
                type = content.type;
            }

            let blob = null;
            if (content instanceof Array) {
                blob = new Blob(content, { type });
            } else {
                blob = new Blob([content], { type });
            }
            
            const size = parseInt((blob.size / 1024).toFixed(0)).toLocaleString();
            console.log(`blob saved, size: ${size} KB, type: ${blob.type}`, blob);

            const url = URL.createObjectURL(blob);
            const a = document.createElement("a");
            a.download = file_name || "未命名文件";
            a.href = url;
            a.click();
            URL.revokeObjectURL(url);
        },

        /**
         * 显示/隐藏按钮区
         */
        toggle_box: function() {
            let sec = wk$(".wk-box")[0];
            if (sec.style.display === "none") {
                sec.style.display = "block";
                return;
            }
            sec.style.display = "none";
        },

        /**
         * 异步地睡眠 delay 毫秒, 可选 max_delay 控制波动范围
         * @param {number} delay 等待毫秒
         * @param {number} max_delay 最大等待毫秒, 默认为null
         * @returns
         */
        sleep: async function(delay, max_delay=null) {
            max_delay = max_delay === null ? delay : max_delay;
            delay = delay + (max_delay - delay) * Math.random();
            return new Promise(resolve => setTimeout(resolve, delay));
        },

        /**
         * 允许打印页面
         */
        allow_print: function() {
            const style = document.createElement("style");
            style.innerHTML = `
            @media print {
                body { display: block; }
            }`;
            document.head.append(style);
        },

        /**
         * 取得get参数key对应的value
         * @param {string} key
         * @returns {string} value
         */
        get_param: function(key) {
            return new URL(location.href).searchParams.get(key);
        },

        /**
         * 求main_set去除cut_set后的set
         * @param {Iterable} main_set 
         * @param {Iterable} cut_set 
         * @returns 差集
         */
        diff: function(main_set, cut_set) {
            const _diff = new Set(main_set);
            for (let elem of cut_set) {
                _diff.delete(elem);
            }
            return _diff;
        },

        /**
         * 增强按钮(默认为蓝色按钮:展开文档)的点击效果
         * @param {string} i 按钮序号
         */
        enhance_click: async function(i) {
            let btn = this.btn(i);
            const style = btn.getAttribute("style") || "";
            
            // 变黑缩小
            btn.setAttribute(
                "style",
                style + "color: black; font-weight: normal;"
            );
            
            await utils.sleep(500);
            btn = this.btn(i);
            // 复原加粗
            btn.setAttribute("style", style);
        },

        /**
         * 绑定事件处理函数到指定按钮,返回实际添加的事件处理函数
         * @param {(event: PointerEvent) => Promise<void>} listener click监听器
         * @param {number} i 按钮序号
         * @param {string} new_text 按钮的新文本,为null则不替换
         * @returns {Function} 事件处理函数
         */
        onclick: function(listener, i, new_text=null) {
            const btn = this.btn(i);

            // 如果需要,替换按钮内文本
            if (new_text) {
                btn.textContent = new_text;
            }

            // 绑定事件,添加到页面上
            /**
             * @param {PointerEvent} event 
             */
            async function wrapped_listener(event) {
                const btn = event.target;
                const text = btn.textContent;
                btn.disabled = true;
                try {
                    await listener.call(btn, event);
                } catch(err) {
                    console.error(err);
                }
                btn.disabled = false;
                btn.textContent = text;
            }

            btn.onclick = wrapped_listener;
            return wrapped_listener;
        },

        /**
         * 返回第 index 个按钮引用
         * @param {number} i 
         * @returns {HTMLButtonElement}
         */
        btn: function(i) {
            return wk$(`.wk-box [class="btn-${i}"]`)[0];
        },

        /**
         * 强制隐藏元素
         * @param {string | Array<HTMLElement>} selector_or_elems 
         */
        force_hide: function(selector_or_elems) {
            const cls = "force-hide";
            const elems = selector_or_elems instanceof Array ?
                selector_or_elems : wk$(selector_or_elems);

            elems.forEach(elem => {
                elem.classList.add(cls);
            });

            // 判断css样式是否已经存在
            let style = wk$(`style.${cls}`)[0];
            // 如果已经存在,则无须重复创建
            if (style) {
                return;
            }
            // 否则创建
            style = document.createElement("style");
            style.innerHTML = `style.${cls} {
            visibility: hidden !important;
            display: none !important;
        }`;
            document.head.append(style);
        },

        /**
         * 等待直到元素可见。最多等待5秒。
         * @param {HTMLElement} elem 一个元素
         * @returns {Promise<HTMLElement>} elem
         */
        until_visible: async function(elem) {
            let [max, i] = [25, 0];
            let style = getComputedStyle(elem);
            // 如果不可见就等待0.2秒/轮
            while (i <= max &&
                (style.display === "none" ||
                style.visibility !== "hidden")
                ) {
                i++;
                style = getComputedStyle(elem);
                await this.sleep(200);
            }
            return elem;
        },

        /**
         * 等待直到函数返回true
         * @param {Function} isReady 判断条件达成与否的函数
         * @param {number} timeout 最大等待秒数, 默认5000毫秒
         */
        wait_until: async function(isReady, timeout=5000) {
            const gap = 200;
            let chances = parseInt(timeout / gap);
            chances = chances < 1 ? 1 : chances;
            
            while (! await isReady()) {
                await this.sleep(200);
                chances -= 1;
                if (!chances) {
                    break;
                }
            }
        },

        /**
         * 隐藏按钮,打印页面,显示按钮
         */
        print_page: function() {
            // 隐藏按钮,然后打印页面
            this.toggle_box();
            setTimeout(window.print, 500);
            setTimeout(this.toggle_box, 1000);
        },

        /**
         * 切换按钮显示/隐藏状态
         * @param {number} i 按钮序号
         * @returns 按钮元素的引用
         */
        toggle_btn: function(i) {
            const btn = this.btn(i);
            const display = getComputedStyle(btn).display;
            
            if (display === "none") {
                btn.style.display = "block";
            } else {
                btn.style.display = "none";
            }
            return btn;
        },

        /**
         * 用input框跳转到对应页码
         * @param {HTMLInputElement} input 当前页码
         * @param {string | number} page_num 目标页码
         * @param {string} type 键盘事件类型:"keyup" | "keypress" | "keydown"
         */
        to_page: function(input, page_num, type) {
            // 设置跳转页码为目标页码
            input.value = `${page_num}`;
            // 模拟回车事件来跳转
            const enter = new KeyboardEvent(type, {
                bubbles: true,
                cancelable: true,
                keyCode: 13
            });
            input.dispatchEvent(enter);
        },

        /**
         * 判断给定的url是否与当前页面同源
         * @param {string} url 
         * @returns {boolean}
         */
        is_same_origin: function(url) {
            url = new URL(url);
            if (url.protocol === "data:") {
                return true;
            }
            if (location.protocol === url.protocol
                && location.host === url.host
                && location.port === url.port
                ) {
                return true;
            }
            return false;
        },

        /**
         * 在新标签页打开链接,如果提供文件名则下载
         * @param {string} url 
         * @param {string} fname 下载文件的名称,默认为空,代表不下载
         */
        open_in_new_tab: function(url, fname="") {
            const a = document.createElement("a");
            a.href = url;
            a.target = "_blank";
            if (fname && this.is_same_origin(url)) {
                a.download = fname;
            }
            a.click();
        },

        /**
         * 用try移除元素
         * @param {HTMLElement | string} elem_or_selector
         */
        remove: function(elem_or_selector) {
            try {
                const cls = this.classof(elem_or_selector);
                if (cls === "String") {
                    wk$(elem_or_selector).forEach(
                        elem => elem.remove()
                    );
                }
                else if (cls.endsWith("Element")) {
                    elem_or_selector.remove();
                }
            } catch (e) {
                console.error(e);
            }
        },

        /**
         * 用try移除若干元素
         * @param {Iterable<HTMLElement>} elements 要移除的元素列表
         */
        remove_multi: function(elements) {
            for (const elem of elements) {
                this.remove(elem);
            }
        },

        /**
         * 等待全部任务落定后返回值的列表
         * @param {Array<Promise>} tasks 
         * @returns {Promise<Array>}
         */
        gather: async function(tasks) {
            const results = await Promise.allSettled(tasks);
            const values = [];

            for (const result of results) {
                // 期约成功解决且返回值不为空的才有效
                if (result.status === "fulfilled"
                    && !([NaN, null, undefined].includes(result.value))) {
                    values.push(result.value);
                }
            }
            return values;
        },

        /**
         * html元素列表转为canvas列表
         * @param {Array<HTMLElement>} elements 
         * @returns {Promise<Array<HTMLCanvasElement>>}
         */
        elems_to_canvases: async function(elements) {
            if (!globalThis.html2canvas) {
                await this.load_web_script(
                    "https://cdn.staticfile.org/html2canvas/1.4.1/html2canvas.min.js"
                );
            }

            // 如果是空列表, 则抛出异常
            if (elements.length === 0) {
                throw new Error("htmlToCanvases 未得到任何html元素");
            }

            return this.gather(
                elements.map(html2canvas)
            );
        },

        /**
         * 将html元素转为canvas再合并到pdf中,最后下载pdf
         * @param {Array<HTMLElement>} elements 元素列表
         * @param {string} title 文档标题
         */
        elems_to_pdf: async function(elements, title="文档") {
            // 如果是空元素列表,终止函数
            const canvases = await this.elems_to_canvases(elements);
            // 控制台检查结果
            console.log("生成的canvas元素如下:");
            console.log(canvases);
            // 合并为PDF
            this.imgs_to_pdf(canvases, title);
        },

        /**
         * 使用xhr异步GET请求目标url,返回响应体blob
         * @param {string} url 
         * @returns {Promise<Blob>} blob
         */
        xhr_get_blob: async function(url) {
            const xhr = new XMLHttpRequest();
            xhr.open("GET", url);
            xhr.responseType = "blob";
            
            return new Promise((resolve, reject) => {
                xhr.onload = () => {
                    const code = xhr.status;
                    if (code >= 200 && code <= 299) {
                        resolve(xhr.response);
                    }
                    else {
                        reject(new Error(`Network Error: ${code}`));
                    }
                };
                xhr.send();
            });
        },

        /**
         * 加载CDN脚本
         * @param {string} url 
         */
        load_web_script: async function(url) {
            try {
                const resp = await fetch(url);
                const code = await resp.text();
                Function(code)();

            } catch(e) {
                console.error(e);
                // 嵌入<script>方式
                return new Promise(resolve => {
                    const script = document.createElement("script");
                    script.src = url;
                    script.onload = resolve;
                    document.body.append(script);
                });
            }
        },

        /**
         * b64编码字符串转Uint8Array
         * @param {string} sBase64 b64编码的字符串
         * @param {number} nBlockSize 字节数
         * @returns {Uint8Array} arr
         */
        b64_to_bytes: function(sBase64, nBlockSize=1) {
            const
                sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length,
                nOutLen = nBlockSize ? Math.ceil((nInLen * 3 + 1 >>> 2) / nBlockSize) * nBlockSize : nInLen * 3 + 1 >>> 2, aBytes = new Uint8Array(nOutLen);

            for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
                nMod4 = nInIdx & 3;
                nUint24 |= base.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
                
                if (nMod4 === 3 || nInLen - nInIdx === 1) {
                    for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
                        aBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
                    }
                    nUint24 = 0;
                }
            }
            return aBytes;
        },

        /**
         * canvas转blob
         * @param {HTMLCanvasElement} canvas 
         * @param {string} type
         * @returns {Promise<Blob>}
         */
        canvas_to_blob: function(canvas, type="image/png") {
            return new Promise(
                resolve => canvas.toBlob(resolve, type, 1)
            );
        },

        /**
         * 合并blobs到压缩包,然后下载
         * @param {Iterable<Blob>} blobs 
         * @param {string} base_name 文件名通用部分,如 page-1.jpg 中的 page
         * @param {string} ext 扩展名,如 jpg
         * @param {string} zip_name 压缩包名称
         * @param {boolean} download 是否下载,可选,默认true,如果不下载则返回压缩包对象
         * @returns {"Promise<JSZip | null>"}
         */
        blobs_to_zip: async function(blobs, base_name, ext, zip_name, download=true) {
            const zip = new window.JSZip();
            // 归档
            for (const [i, blob] of this.enumerate(blobs)) {
                zip.file(`${base_name}-${i+1}.${ext}`, blob, { binary: true });
            }

            // 导出
            if (!download) {
                return zip;
            }

            const zip_blob = await zip.generateAsync({ type: "blob" });
            console.log(zip_blob);
            this.save(`${zip_name}.zip`, zip_blob);
            return null;
        },

        /**
         * 存储所有canvas图形为png到一个压缩包
         * @param {Iterable<HTMLCanvasElement>} canvases canvas元素列表
         * @param {string} title 文档标题
         */
        canvases_to_zip: async function(canvases, title) {
            // canvas元素转为png图像
            // 所有png合并为一个zip压缩包
            const tasks = [];
            for (let canvas of canvases) {
                tasks.push(this.canvas_to_blob(canvas));
            }
            const blobs = await this.gather(tasks);
            this.blobs_to_zip(blobs, "page", "png", title);
        },



        /**
         * 合并图像并导出PDF
         * @param {Iterable<HTMLCanvasElement | Uint8Array | HTMLImageElement>} imgs 图像元素列表
         * @param {string} title 文档标题
         * @param {number} width (可选)页面宽度 默认 0
         * @param {number} height (可选)页面高度 默认 0
         * @param {boolean} blob (可选)是否返回 blob 默认 false
         */
        imgs_to_pdf: async function(imgs, title, width = 0, height = 0, blob=false) {
            imgs = Array.from(imgs);
            if (imgs.length === 0) {
                this.raise("没有任何图像用于合并为PDF");
            }

            // 先获取第一个canvas用于判断竖向还是横向,以及得到页面长宽
            const first = imgs[0];

            // 如果没有手动指定canvas的长宽,则自动检测
            if (!width && !height) {
                // 如果是字节数组
                if (first instanceof Uint8Array) {
                    const cover = await createImageBitmap(
                        new Blob([first])
                    );
                    [width, height] = [cover.width, cover.height];

                    // 如果是画布或图像元素
                } else if (
                    first instanceof HTMLCanvasElement ||
                    first instanceof HTMLImageElement
                    ) {
                    if (first.width && parseInt(first.width) && parseInt(first.height)) {
                        [width, height] = [first.width, first.height];
                    } else {
                        const
                            width_str = first.style.width.replace(/(px)|(rem)|(em)/, ""),
                            height_str = first.style.height.replace(/(px)|(rem)|(em)/, "");
                        width = parseInt(width_str);
                        height = parseInt(height_str);
                    }
                } else {
                    // 其他未知类型
                    throw TypeError("不能处理的画布元素类型:" + this.classof(first));
                }
            }
            console.log(`canvas数据:宽: ${width}px,高: ${height}px`);
            
            // 如果文档第一页的宽比长更大,则landscape,否则portrait
            const orientation = width > height ? 'l' : 'p';
            // jsPDF的第三个参数为format,当自定义时,参数为数字数组。
            const pdf = new jspdf.jsPDF(orientation, 'px', [height, width]);

            const last = imgs.pop();
            const self = this;
            // 保存每一页文档到每一页pdf
            imgs.forEach((canvas, i) => {
                pdf.addImage(canvas, 'png', 0, 0, width, height);
                pdf.addPage();
                self?.update_popup(`PDF 已经绘制 ${i + 1} 页`);
            });
            // 添加尾页
            pdf.addImage(last, 'png', 0, 0, width, height);
            
            // 导出文件
            if (blob) {
                return pdf.output("blob");
            }
            pdf.save(`${title}.pdf`);
        },

        /**
         * imageBitMap转canvas
         * @param {ImageBitmap} bmp 
         * @returns {HTMLCanvasElement} canvas
         */
        bmp_to_canvas: function(bmp) {
            const canvas = document.createElement("canvas");
            canvas.height = bmp.height;
            canvas.width = bmp.width;
            
            const ctx = canvas.getContext("bitmaprenderer");
            ctx.transferFromImageBitmap(bmp);
            return canvas;
        },

        /**
         * 导出图片链接
         * @param {Iterable<string>} urls
         */
        save_urls: function(urls) {
            const _urls = Array
                .from(urls)
                .map((url) => {
                    const _url = url.trim();
                    if (url.startsWith("//"))
                        return "https:" + _url;
                    return _url;
                })
                .filter(url => url);

            this.save("urls.csv", _urls.join("\n"), "text/csv");
        },

        /**
         * 图片blobs合并并导出为单个PDF
         * @param {Array<Blob>} blobs 
         * @param {string} title (可选)文档名称, 不含后缀, 默认为"文档"
         * @param {boolean} filter (可选)是否过滤 type 不以 "image/" 开头的 blob; 默认为 true
         * @param {boolean} blob (可选)是否返回 blob,默认 false
         */
        img_blobs_to_pdf: async function(blobs, title="文档", filter=true, blob=false) {
            // 格式转换:img blob -> bmp
            let tasks = blobs;
            if (filter) {
                tasks = blobs.filter(
                    blob => blob.type.startsWith("image/")
                );
            }
            tasks = await this.gather(
                tasks.map(blob => blob.arrayBuffer())      
            );
            tasks = tasks.map(buffer => new Uint8Array(buffer));
            // 导出PDF
            return this.imgs_to_pdf(tasks, title, 0, 0, blob);
        },

        /**
         * 下载可以简单直接请求的图片,合并到 PDF 并导出
         * @param {Iterable<string>} urls 图片链接列表
         * @param {string} title 文档名称
         * @param {number} min_num 如果成功获取的图片数量 < min_num, 则等待 2 秒后重试; 默认 0 不重试
         * @param {boolean} clear 是否在请求完成后清理控制台输出,默认false
         * @param {boolean} blobs 是否返回二进制图片列表,默认 false(即直接导出PDF)
         */
        img_urls_to_pdf: async function(urls, title, min_num=0, clear=false, blobs=false) {
            // 强制转换为迭代器类型
            urls = urls[Symbol.iterator]();
            const first = urls.next().value;
            
            // 如果不符合同源策略,在打开新标签页
            if (!this.is_same_origin(first)) {
                console.info("URL 不符合同源策略;转为新标签页打开目标网站");
                this.open_in_new_tab((new URL(first)).origin);
                return;
            }

            let tasks, img_blobs, i = 3;
            // 根据请求成功数量判断是否循环
            do {
                i -= 1;
                // 发起请求
                tasks = [this.xhr_get_blob(first)];  // 初始化时加入第一个
                // 然后加入剩余的
                for (const [j, url] of this.enumerate(urls)) {
                    tasks.push(this.xhr_get_blob(url));
                    this.update_popup(`已经请求 ${j} 张图片`);
                }
                
                // 接收响应
                img_blobs = (await this.gather(tasks)).filter(
                    blob => blob.type.startsWith("image/")
                );

                if (clear) {
                    console.clear();
                }

                if (
                    min_num 
                    && img_blobs.length < min_num 
                    && i
                    ) {
                    // 下轮行动前冷却
                    console.log(`打盹 2 秒`);
                    await utils.sleep(2000);
                } else {
                    // 结束循环
                    break;
                }
            } while (true)

            if (blobs) return img_blobs;
            await this.img_blobs_to_pdf(img_blobs, title, false);
        },

        /**
         * 返回子串个数
         * @param {string} str 
         * @param {string} sub 
         */
        count_sub_str: function(str, sub) {
            return [...str.matchAll(sub)].length;
        },

        /**
         * 返回按钮区引用
         * @returns 
         */
        sec: function() {
            const sec = wk$(".wk-box .btns-sec")[0];
            if (!sec) throw new Error("wk 按钮区找不到");
            return sec;
        },

        _monkey: function() {
            const mky = wk$(".wk-box .wk-fold-btn")[0];
            if (!mky) throw new Error("wk 小猴子找不到");
            return mky;
        },

        /**
         * 折叠按钮区,返回是否转换了状态
         */
        fold_box: function() {
            const sec = this.sec();
            const mky = this._monkey();
            const display = getComputedStyle(sec).display;
            if (display !== "block") return false; 
            
            // 显示 -> 隐藏
            [sec, mky].forEach(
                elem => elem.classList.add("folded")
            );
            return true;
        },

        /**
         * 展开按钮区,返回是否转换了状态
         */
        unfold_box: function() {
            const sec = this.sec();
            const mky = this._monkey();
            const display = getComputedStyle(sec).display;
            if (display === "block") return false; 
            
            // 隐藏 -> 显示
            // 显示 -> 隐藏
            [sec, mky].forEach(
                elem => elem.classList.remove("folded")
            );
            return true;
        },

        /**
         * 运行基于按钮的、显示进度条的函数
         * @param {number} i 按钮序号
         * @param {Function} task 需要等待的耗时函数
         */
        run_with_prog: async function(i, task) {
            const btn = utils.btn(i);
            let new_btn;

            if (!wk$("#wk-popup")[0]) {
                this.add_popup();
            }

            this.fold_box();
            this.toID("wk-popup");

            new_btn = btn.cloneNode(true);
            btn.replaceWith(new_btn);
            this.onclick(
                () => utils.toID("wk-popup"), i, "显示进度"
            );

            try {
                await task();
            } catch(e) {
                console.error(e);
            }

            this.toID("");
            this.unfold_box();
            this.remove_popup();
            new_btn.replaceWith(btn);
        },

        /**
         * 创建5个按钮:展开文档、导出图片、导出PDF、未设定4、未设定5;除第1个外默认均为隐藏
         */
        create_btns: function() {
            // 添加样式
            document.head.insertAdjacentHTML("beforeend", style);
            // 添加按钮区
            document.body.insertAdjacentHTML("beforeend", box);

            // 绑定小猴子按钮回调
            const monkey = wk$(".wk-fold-btn")[0];
            // 隐藏【🙈】,展开【🐵】
            monkey.onclick = () => this.fold_box() || this.unfold_box();
        },

        /**
         * 添加弹窗到 body, 通过 utils.toID("wk-popup") 激发
         */
        add_popup: function() {
            document.body.insertAdjacentHTML("beforeend", popup);
        },

        /**
         * 设置弹窗正文
         * @param {string} text 
         */
        update_popup: function(text) {
            const body = wk$(".wk-popup-body")[0];
            if (!body) return;
            body.textContent = text;
        },

        /**
         * 移除弹窗
         */
        remove_popup: function() {
            this.remove(wk$(".wk-popup-container")[0]);
        },

        /**
         * 滚动页面到id位置的元素处
         * @param {string} id 
         */
        toID: function(id) {
            location.hash = `#${id}`;
        }
    };


    /**
     * ---------------------------------------------------------------------
     * 绑定使用 this 的函数到 utils,使其均成为绑定方法
     * ---------------------------------------------------------------------
     */

    /**
     * 确保特定外部脚本加载的装饰器
     * @param {string} global_obj_name 
     * @param {string} cdn_url 
     * @param {Function} func
     * @returns
     */
    function ensure_script_existed(global_obj_name, cdn_url, func) {
        async function inner(...args) {
            if (!window[global_obj_name]) {
                // 根据需要加载依赖
                await utils.load_web_script(cdn_url);
            }
            return func(...args);
        }
        // 存储参数定义
        base.superAssign(inner, func);
        return inner;
    }


    /**
     * 确保引用外部依赖的函数都在调用前加载了依赖
     */
    for (const prop of Object.keys(utils)) {
        // 跳过非函数
        if (
            !(typeof utils[prop] === "function")
            && !`${utils[prop]}`.startsWith("class")
            ) {  
            continue;
        }

        // 绑定this到utils
        if (/ this[.[][a-z_]/.test(`${utils[prop]}`)) {
            // 存储参数定义
            const doc = utils.help(utils[prop], false);
            // 绑死this,同时提供 __func__ 来取回原先的函数
            const fn = utils[prop];
            utils[prop] = utils[prop].bind(utils);
            utils[prop].__func__ = fn;
            // 重设参数定义
            utils[prop].__doc__ = doc;
        }

        // 设定 __doc__ 为访问器属性
        const doc_box = [
            utils.help(utils[prop], false)
        ];
        Object.defineProperty(utils[prop], "__doc__", {
            configurable: true,
            enumerable: true,
            get() { return doc_box.join("\n"); },
            set(new_doc) { doc_box.push(new_doc); },
        });

        // 为有外部依赖的函数做包装
        let obj, url;
        const name = prop.toLowerCase();

        if (name.includes("_to_zip")) {
            obj = "JSZip";
            url = "https://cdn.staticfile.org/jszip/3.7.1/jszip.min.js";

        } else if (name.includes("_to_pdf")) {
            obj = "jspdf";
            url = "https://cdn.staticfile.org/jspdf/2.5.1/jspdf.umd.min.js";

        } else {
            continue;
        }
        utils[prop] = ensure_script_existed(obj, url, utils[prop]);
    }


    /**
     * ---------------------------------------------------------------------
     * 为 utils 部分函数绑定更详细的说明
     * ---------------------------------------------------------------------
     */

    utils.b64_to_bytes.__doc__ = `
/**
 * b64编码字符串转Uint8Array
 * @param {string} sBase64 b64编码的字符串
 * @param {number} nBlockSize 字节数
 * @returns {Uint8Array} arr
 */
`;

    utils.blobs_to_zip.__doc__ = `
/**
 * 合并blobs到压缩包,然后下载
 * @param {Iterable<Blob>} blobs 
 * @param {string} base_name 文件名通用部分,如 image-1.jpg 中的 image
 * @param {string} ext 扩展名,如 jpg
 * @param {string} zip_name 压缩包名称
 */
`;

    utils.imgs_to_pdf.__doc__ = `
/**
 * 合并图像并导出PDF
 * @param {Iterable<HTMLCanvasElement | Uint8Array | HTMLImageElement>} imgs 图像元素列表
 * @param {string} title 文档标题
 * @param {number} width (可选)页面宽度 默认 0
 * @param {number} height (可选)页面高度 默认 0
 * @param {boolean} blob (可选)是否返回 blob 默认 false
 */
`;

    utils.img_urls_to_pdf.__doc__ = `
/**
 * 下载可以简单直接请求的图片,合并到 PDF 并导出
 * @param {Iterable<string>} urls 图片链接列表
 * @param {string} title 文档名称
 * @param {number} min_num 如果成功获取的图片数量 < min_num, 则等待 2 秒后重试; 默认 0 不重试
 * @param {boolean} clear 是否在请求完成后清理控制台输出,默认false
 */
`;

    utils.img_blobs_to_pdf.__doc__ = `
/**
 * 图片blobs合并并导出为单个PDF
 * @param {Array<Blob>} blobs 
 * @param {string} title (可选)文档名称, 不含后缀, 默认为"文档"
 * @param {boolean} filter (可选)是否过滤 type 不以 "image/" 开头的 blob; 默认为 true
 * @param {boolean} blob (可选)是否返回 blob
 */
`;


    /**
     * ---------------------------------------------------------------------
     * 绑定 utils 成员到 wk$,允许外部轻松调用
     * ---------------------------------------------------------------------
     */

    base.superAssign(wk$, utils);
    console.info("wk: `wk$` 已经挂载到全局");

    /**
     * 展开道客巴巴的文档
     */
    async function readAllDoc88() {
        // 获取“继续阅读”按钮
        let continue_btn = wk$("#continueButton")[0];
        // 如果存在“继续阅读”按钮
        if (continue_btn) {
            // 跳转到文末(等同于展开全文)
            let cur_page = wk$("#pageNumInput")[0];
            // 取得最大页码
            let page_max = cur_page.parentElement.textContent.replace(" / ", "");
            // 跳转到尾页
            utils.to_page(cur_page, page_max, "keypress");
            // 返回顶部
            await utils.sleep(1000);
            utils.to_page(cur_page, "1", "keypress");
        }
        // 文档展开后,显示按钮
        else {
            for (const i of utils.range(1, 6)) {
                utils.toggle_btn(i);
            }
        }
    }


    /**
     * 隐藏选择文字的弹窗
     */
    async function hideSelectPopup() {
        const
            elem = (await wk$$("#left-menu"))[0],
            hide = elem => elem.style.zIndex = -1;
        return utils.until_visible(elem).then(hide);
    }


    /**
     * 初始化任务
     */
    async function initService() {
        // 初始化
        console.log("正在执行初始化任务");

        // 1. 查找复制文字可能的api名称
        const prop = getCopyAPIValue();
        globalThis.doc88JS._apis = Object
            .getOwnPropertyNames(prop)
            .filter(name => {
                if (!name.startsWith("_")) {
                    return false;
                }
                if (prop[name] === "") {
                    return true;
                }
            });
        
        // 2. 隐藏选中文字的提示框
        await hideSelectPopup();
        // 3. 隐藏搜索框
        // hideSearchBox();
        // 4. 移除vip复制弹窗
        // hideCopyPopup();
    }


    /**
     * 取得 doc88JS.copy_api 所指向属性的值
     * @returns 
     */
    function getCopyAPIValue() {
        let aim = globalThis;
        for (let name of globalThis.doc88JS.copy_api) {
            aim = aim[name];
        }
        return aim;
    }


    /**
     * 返回选中的文字
     * @returns {string}
     */
    function getSelectedText() {
        // 首次复制文字,需要先找出api
        if (globalThis.doc88JS.copy_api.length === 3) {
            // 拼接出路径,得到属性
            let prop = getCopyAPIValue();  // 此时是属性,尚未取得值

            // 查询值
            for (let name of globalThis.doc88JS._apis) {
                let value = prop[name];
                // 值从空字符串变为非空字符串了,确认是目标api名称
                if (typeof value === 'string'
                    && value.length > 0
                    && !value.match(/\d/)  // 开头不能是数字,因为可能是 '1-179-195' 这种值
                    ) {
                    globalThis.doc88JS.copy_api.push(name);
                    break;
                }
            }
        }
        return getCopyAPIValue();
    }


    /**
     * 输出选中的文字到剪贴板和控制台,返回是否复制了文档
     * @returns {boolean} doc_is_copied
     */
    function copySelected() {
        // 尚未选中文字
        if (getComputedStyle(wk$("#left-menu")[0]).display === "none") {
            console.log("尚未选中文字");
            return false;
        }
        // 输出到控制台和剪贴板
        utils.copy_text(getSelectedText());
        return true;
    }


    /**
     * 捕获 ctrl + c 以复制文字
     * @param {KeyboardEvent} e 
     * @returns 
     */
    function onCtrlC(e) {
        // 判断是否为 ctrl + c
        if (!(e.code === "KeyC" && e.ctrlKey === true)) {
            return;
        }

        // 判断触发间隔
        let now = Date.now();
        // 距离上次小于0.5秒
        if (now - doc88JS.last_copy_time < 500 * 1) {
            doc88JS.last_copy_time = now;
            return;
        }
        // 大于1秒
        // 刷新最近一次触发时间
        doc88JS.last_copy_time = now;
        // 复制文字
        copySelected();
        // if (!copySelected()) return;
        
        // 停止传播
        e.stopImmediatePropagation();
        e.stopPropagation();
    }


    /**
     * 浏览并加载所有页面
     */
    async function walkThrough$2() {
        // 文档容器
        let container = wk$("#pageContainer")[0];
        container.style.display = "none";
        // 页码
        let page_num = wk$("#pageNumInput")[0];
        // 文末提示
        let tail = wk$("#readEndDiv > p")[0];
        let origin = tail.textContent;
        // 按钮
        wk$('.btns_section > [class*="btn-"]').forEach(
            elem => elem.style.display = "none"
        );

        // 逐页渲染
        let total = parseInt(Config.p_pagecount);
        try {
            for (let i = 1; i <= total; i++) {
                // 前往页码
                GotoPage(i);
                await utils.wait_until(async() => {
                    let page = wk$(`#page_${i}`)[0];
                    // page无法选中说明有弹窗
                    if (!page) {
                        // 关闭弹窗,等待,然后递归
                        wk$("#ym-window .DOC88Window_close")[0].click();
                        await utils.sleep(500);
                        walkThrough$2();
                        throw new Error("walkThrough 递归完成,终止函数");
                    }
                    // canvas尚未绘制时width=300
                    return page.width !== 300;
                });
                // 凸显页码
                utils.emphasize_text(page_num);
                tail.textContent = `请勿反复点击按钮,耐心等待页面渲染:${i}/${total}`;
            }
        } catch(e) {
            // 捕获退出信号,然后退出
            console.log(e);
            return;
        }

        // 恢复原本显示
        container.style.display = "";
        page_num.style = "";
        tail.textContent = origin;
        // 按钮
        wk$('.btns_section > [class*="btn-"]').forEach(
            elem => elem.style.display = "block"
        );
        wk$(".btns_section > .btn-1")[0].style.display = "none";
    }


    /**
     * 道客巴巴文档下载策略
     */
    async function doc88() {
        // 全局对象
        globalThis.doc88JS = {
            last_copy_time: 0,  // 上一次 ctrl + c 的时间戳(毫秒)
            copy_api: ["Core", "Annotation", "api"]
        };

        // 创建脚本启动按钮1、2
        utils.create_btns();

        // 绑定主函数
        let prepare = function() {
            // 获取canvas元素列表
            let node_list = wk$(".inner_page");
            // 获取文档标题
            let title;
            if (wk$(".doctopic h1")[0]) {
                title = wk$(".doctopic h1")[0].title;
            } else {
                title = "文档";
            }
            return [node_list, title];
        };

        // btn_1: 展开文档
        utils.onclick(readAllDoc88, 1);

        // // btn_2: 加载全部页面
        utils.onclick(walkThrough$2, 2, "加载所有页面");
        
        // btn_3: 导出PDF
        function imgsToPDF() {
            if (confirm("确定每页内容都加载完成了吗?")) {
                utils.run_with_prog(
                    3, () => utils.imgs_to_pdf(...prepare())
                );
            }
        }    utils.onclick(imgsToPDF, 3, "导出图片到PDF");

        // btn_4: 导出ZIP
        utils.onclick(() => {
            if (confirm("确定每页内容都加载完成了吗?")) {
                utils.canvases_to_zip(...prepare());
            }
        }, 4, "导出图片到ZIP");

        // btn_5: 复制选中文字
        utils.onclick(btn => {
            if (!copySelected()) {
                btn.textContent = "未选中文字";
            } else {
                btn.textContent = "复制成功!";
            }
        }, 5, "复制选中文字");

        // 为 ctrl + c 添加响应
        window.addEventListener("keydown", onCtrlC, true);
        // 执行一次初始化任务
        window.addEventListener(
            "mousedown", initService, { once: true, capture: true }
        );
    }

    function get_title$1() {
        return document.title.slice(0,-6);
    }


    function save_canvases(type) {
        return () => {
            if (!wk$(".hkswf-content2 canvas").length) {
                alert("当前页面不适用此按钮");
                return;
            }
        
            if (confirm("页面加载完毕了吗?")) {
                const title = get_title$1();
                const canvases = wk$(".hkswf-content2 canvas");
                let data_to;

                switch (type) {
                    case "pdf":
                        data_to = utils.imgs_to_pdf;
                        break;

                    case "zip":
                        data_to = utils.canvases_to_zip;
                        break;
                
                    default:
                        data_to = () => utils.raise(`未知 type: ${type}`);
                        break;
                }
                data_to(canvases, title);
            }
        }
    }


    function get_base_url() {
        // https://docimg1.docin.com/docinpic.jsp?file=2179420769&width=1000&sid=bZh4STs-f4NA88IA02INyapgA9Z5X3NN1sGo4WnpquIvk4CyflMk1Oxey1BsO1BG&pageno=2&pcimg=1
        return `https://docimg1.docin.com/docinpic.jsp?` +
            `file=` + location.pathname.match(/p-(\d+)[.]html/)[1] + 
            `&width=1000&sid=` + window.readerConfig.flash_param_hzq + 
            `&pcimg=1&pageno=`;
    }


    /**
     * 返回总页码
     * @returns {number}
     */
    function get_page_num() {
        return parseInt(
            wk$(".page_num")[0].textContent.slice(1)
        );
    }

    function init_save_imgs() {
        const iframe = document.createElement("iframe");
        iframe.src = "https://docimg1.docin.com/?wk=true";
        iframe.style.display = "none";
        
        let sock;

        /**
         * @param {MessageEvent} event 
         */
        function on_client_msg(event) {
            if (event.data.author !== "wk"
                || event.data.action !== "finish"
            ) return;
        
            sock.notListen(on_client_msg);
            iframe.remove();
            utils.toggle_btn(1);
            utils.toggle_btn(3);
        }
        
        /**
         * @param {string} type "pdf" | "zip"
         */
        return (type) => {
            return async function() {
                if (!wk$("[id*=img_] img").length) {
                    alert("当前页面不适用此按钮");
                    return;
                }
                
                utils.toggle_btn(1);
                utils.toggle_btn(3);

                document.body.append(iframe);
                await utils.sleep(500);
        
                sock = new utils.Socket(iframe.contentWindow);
                await sock.connect(false);
                sock.listen(on_client_msg);
                sock.talk({
                    author: "wk",
                    type,
                    title: get_title$1(),
                    base_url: get_base_url(),
                    max: get_page_num()
                });
            }
        }
    }


    const save_imgs = init_save_imgs();


    async function walk_through() {
        // 隐藏按钮
        utils.toggle_btn(5);
        // 隐藏文档页面
        wk$("#contentcontainer")[0].setAttribute("style", "visibility: hidden;");

        const total = get_page_num();
        const input = wk$("#page_cur")[0];
        
        for (let i = 1; i <= total; i++) {
            utils.to_page(input, i, "keydown");
            await utils.wait_until(
                () => {
                    const page = wk$(`#page_${i}`)[0];
                    const contents = wk$.call(page, `.canvas_loaded, img`);
                    return contents.length > 0;
                },
                5000
            );
        }

        // 显示文档页面
        wk$("#contentcontainer")[0].removeAttribute("style");
    }


    function main_page() {
        // 创建脚本启动按钮
        utils.create_btns();
        
        utils.onclick(
            save_imgs("pdf"), 1, "合并图片为PDF"
        );
        
        utils.onclick(
            save_canvases("pdf"), 2, "合并画布为PDF"
        );
        utils.toggle_btn(2);

        utils.onclick(
            save_imgs("zip"), 3, "打包图片到ZIP"
        );
        utils.toggle_btn(3);
        
        utils.onclick(
            save_canvases("zip"), 4, "打包画布到ZIP"
        );
        utils.toggle_btn(4);

        utils.onclick(
            walk_through, 5, "自动浏览页面"
        );
        utils.toggle_btn(5);
    }



    function init_background() {
        const sock = new utils.Socket(window.top);

        /**
         * @param {MessageEvent} event 
         */
        async function on_server_msg(event) {
            if (event.data.author !== "wk") return;
        
            const { title, base_url, max, type } = event.data;
            const urls = Array
                .from(utils.range(1, max + 1))
                .map(i => (base_url + i));
            
            const imgs = await utils.img_urls_to_pdf(
                urls, title, 0, false, true
            );
        
            switch (type) {
                case "pdf":
                    await utils.img_blobs_to_pdf(imgs, title);
                    break;
            
                case "zip":
                    const ext = imgs[0].type ? imgs[0].type.split("/")[1] : "png";
                    await utils.blobs_to_zip(
                        imgs, "page", ext, title
                    );
                    break;

                default:
                    utils.raise(`未知 type: ${type}`);
                    break;
            }

            sock.talk({
                author: "wk",
                action: "finish"
            });
            sock.notListen(on_server_msg);
        }
        
        return async function() {
            sock.listen(on_server_msg);
            await sock.connect(true);
        }
    }


    const background = init_background();


    /**
     * 豆丁文档下载策略
     */
    function docin() {
        const host = location.hostname;
        switch (host) {
            case "jz.docin.com":
            case "www.docin.com":
                main_page();
                break;

            case "docimg1.docin.com":
                background();
                break;
        
            default:
                console.log(`未知域名: ${host}`);
                break;
        }
    }

    function jumpToHost() {
        // https://swf.ishare.down.sina.com.cn/1DrH4Qt2cvKd.jpg?ssig=DUf5x%2BXnKU&Expires=1673867307&KID=sina,ishare&range={}-{}
        let url = wk$(".data-detail img, .data-detail embed")[0].src;
        if (!url) {
            alert("找不到图片元素");
            return;
        }

        let url_obj = new URL(url);
        let path = url_obj.pathname.slice(1);
        let query = url_obj.search.slice(1).split("&range")[0];
        let title = document.title.split(" - ")[0];
        let target = `${url_obj.protocol}//${url_obj.host}?path=${path}&fname=${title}&${query}`;
        // https://swf.ishare.down.sina.com.cn/
        globalThis.open(target, "hostage");
        // 然后在跳板页面发起对图片的请求
    }


    /**
     * 爱问文库下载跳转策略
     */
    function ishare() {
        // 创建按钮区
        utils.create_btns();

        // btn_1: 识别文档类型 -> 导出PDF
        utils.onclick(jumpToHost, 1, "到下载页面");
        // btn_2: 不支持爱问办公
        utils.onclick(() => null, 2, "不支持爱问办公");
        // utils.toggleBtnStatus(4);
    }

    /**
     * 返回包含对于数量svg元素的html元素
     * @param {string} data
     * @returns {HTMLDivElement} article
     */
    function _createDiv(data) {
        let num = utils.count_sub_str(data, data.slice(0, 10));
        let article = document.createElement("div");
        article.id = "article";
        article.innerHTML = `
        <style class="wk-settings">
            body {
                margin: 0px;
                width: 100%;
                background-color: rgb(95,99,104);
            }
            #article {
                width: 100%;
                display: flex;
                flex-direction: row;
                justify-content: space-around;
            }
            #root-box {
                display: flex;
                flex-direction: column;
                background-color: white;
                padding: 0 2em;
            }
            .gap {
                height: 50px;
                width: 100%;
                background-color: transparent;
            }
        </style>
        <div id="root-box">
        ${
            `<object class="svg-box"></object>
            <div class="gap"></div>`.repeat(num)
        }
    `;
        // 移除最后一个多出的gap
        Array.from(article.querySelectorAll(".gap")).at(-1).remove();
        return article;
    }


    function setGap(height) {
        let style = wk$(".wk-settings")[0].innerHTML;
        wk$(".wk-settings")[0].innerHTML = style.replace(
            /[.]gap.*?{.*?height:.+?;/s,
            `.gap { height: ${parseInt(height)}px;`    
        );
    }


    function setGapGUI() {
        let now = getComputedStyle(wk$(".gap")[0]).height;
        let new_h = prompt(`当前间距:${now}\n请输入新间距:`);
        if (new_h) {
            setGap(new_h);
        }
    }


    function getSVGtext(data) {
        let div = document.createElement("div"); 
        div.innerHTML = data;
        return div.textContent;
    }


    function toDisplayMode1() {
        let content = globalThis["ishareJS"].content_1;
        if (!content) {
            content = globalThis["ishareJS"].text
            .replace(/\n{2,}/g, "<hr>")
            .replace(/\n/g, "<br>")
            .replace(/\s/g, "&nbsp;")
            .replace(/([a-z])([A-Z])/g, "$1 $2");  // 英文简单分词

            globalThis["ishareJS"].content_1 = content;
        }

        wk$("#root-box")[0].innerHTML = content;
    }


    function toDisplayMode2() {
        let content = globalThis["ishareJS"].content_2;
        if (!content) {
            content = globalThis["ishareJS"].text
                .replace(/\n{2,}/g, "<hr>")
                .replace(/\n/g, "")
                .replace(/\s/g, "&nbsp;")
                .replace(/([a-z])([A-Z])/g, "$1 $2")
                .split("<hr>")
                .map(paragraph => `<p>${paragraph}</p>`)
                .join("");
            
                globalThis["ishareJS"].content_2 = content;
            wk$(".wk-settings")[0].innerHTML += `
            #root-box > p {
                text-indent: 2em;
                width: 40em;
                word-break: break-word;
            }
        `;
        }

        wk$("#root-box")[0].innerHTML = content;
    }


    function changeDisplayModeWrapper() {
        let flag = true;

        function inner() {
            if (flag) {
                toDisplayMode1();
            } else {
                toDisplayMode2();
            }
            flag = !flag;
        }
        return inner;
    }


    function handleSVGtext() {
        globalThis["ishareJS"].text = getSVGtext(
            globalThis["ishareJS"].data
        );

        let change = changeDisplayModeWrapper();
        utils.onclick(change, 4, "切换显示模式");

        utils.toggle_btn(2);
        utils.toggle_btn(3);
        utils.toggle_btn(4);
        change();
    }


    /**
     * 处理svg的url
     * @param {string} svg_url 
     */
    async function handleSVGurl(svg_url) {
        let resp = await fetch(svg_url);
        let data = await resp.text();
        globalThis["ishareJS"].data = data;

        let sep = data.slice(0, 10);
        let svg_texts = data
            .split(sep)
            .slice(1)
            .map(svg_text => sep + svg_text);

        console.log(`共 ${svg_texts.length} 张图片`);

        let article = _createDiv(data);
        let boxes = article.querySelectorAll(".svg-box");
        boxes.forEach((obj, i) => {
            let blob = new Blob([svg_texts[i]], {type: "image/svg+xml"});
            let url = URL.createObjectURL(blob);
            obj.data = url;
            URL.revokeObjectURL(blob);
        });

        let body = wk$("body")[0];
        body.innerHTML = "";
        body.appendChild(article);

        utils.create_btns();
        utils.onclick(utils.print_page, 1, "打印页面到PDF");
        utils.onclick(setGapGUI, 2, "重设页间距");
        utils.onclick(handleSVGtext, 3, "显示空白点我");

        utils.toggle_btn(2);
        utils.toggle_btn(3);
    }


    /**
     * 取得图片下载地址
     * @param {string} fname 
     * @param {string} path
     * @returns 
     */
    function getImgUrl(fname, path) {
        if (!fname) {
            throw new Error("URL Param `fname` does not exist.");
        } 
        return location.href
            .replace(/[?].+?&ssig/, "?ssig")
            .replace("?", path + "?");
    }


    /**
     * 下载整个图片包
     * @param {string} img_url
     * @returns 
     */
    async function getData(img_url) {   
        let resp = await fetch(img_url);
        // window.data = await resp.blob();
        // throw Error("stop");
        let buffer = await resp.arrayBuffer();
        return new Uint8Array(buffer);
    }


    /**
     * 分切图片包为若干图片
     * @param {Uint8Array} data 多张图片合集数据包
     * @returns {Array<Uint8Array>} 图片列表
     */
    function parseData(data) {
        // 判断图像类型/拿到文件头
        let head = data.slice(0, 8);
        return utils.split_files_by_head(data, head);
    }


    /**
     * 图像Uint8数组列表合并然后导出PDF
     * @param {string} fname
     * @param {Array<Uint8Array>} img_data_list 
     */
    async function imgDataArrsToPDF(fname, img_data_list) {
        return utils.imgs_to_pdf(
            img_data_list,
            fname
        );
    }


    /**
     * 
     * @param {string} fname 文件名
     * @param {Array<Uint8Array>} img_data_list 数据列表
     */
    async function saveAsZip(fname, img_data_list) {
        await utils.blobs_to_zip(
            img_data_list,
            "page",
            "png",
            fname
        );
    }


    /**
     * 取得图片集合体并切分,如果是 SVG 则对应处理
     * @returns {Array<Uint8Array>} imgs
     */
    async function getImgs() {
        let [fname, path] = [
            window.ishareJS.fname,
            window.ishareJS.path
        ];

        let img_url = getImgUrl(fname, path);

        // 处理svg
        if (path.includes(".svg")) {
            document.title = fname;
            await handleSVGurl(img_url);
            return;
        }
        // 处理常规图像
        let data = await getData(img_url);
        let img_data_list = parseData(data);
        console.log(`共 ${img_data_list.length} 张图片`);

        window.ishareJS.imgs = img_data_list;

        // 下载完成,可以导出
        utils.onclick(exportPDF$3, 2, "下载并导出PDF");
        utils.toggle_btn(1);
        utils.toggle_btn(2);
    }


    async function exportPDF$3() {
        let args = [
            window.ishareJS.fname,
            window.ishareJS.imgs
        ];

        try {
            await imgDataArrsToPDF(...args);
        } catch(e) {
            console.error(e);
            
            // 因 jsPDF 字符串拼接溢出导致的 Error
            if (`${e}`.includes("RangeError: Invalid string length")) {
                // 提示失败
                alert("图片合并为 PDF 时失败,请尝试下载图片压缩包");
                // 备选方案:导出图片压缩包
                utils.onclick(
                    () => saveAsZip(...args),
                    3,
                    "导出ZIP"
                );
                utils.toggle_btn(3);  // 显示导出ZIP按钮
                utils.toggle_btn(2);  // 隐藏导出PDF按钮
            } else {
                throw e;
            }
        }

    }


    function showHints() {
        wk$("h1")[0].textContent = "wk 温馨提示";
        wk$("p")[0].innerHTML = [
            "下载 270 页的 PPT (70 MB) 需要约 30 秒",
            "请耐心等待,无需反复点击按钮",
            "如果很久没反应,请加 QQ 群反馈问题"
        ].join("<br>");
        wk$("hr")[0].nextSibling.textContent = "403 Page Hostaged By Wenku Doc Downloader";
    }


    /**
     * 爱问文库下载策略
     */
    async function ishareData() {
        // 全局对象
        globalThis["ishareJS"] = {
            data: "",
            imgs: [],
            text: "",
            content_1: "",
            content_2: "",
            fname: utils.get_param("fname"),
            path: utils.get_param("path")
        };

        // 显示提示
        showHints();

        // 创建按钮区
        utils.create_btns();

        // btn_1: 识别文档类型,处理SVG或下载数据
        utils.onclick(getImgs, 1, "下载数据");
    }

    /**
     * 提供提示信息
     */
    function showTips$1() {
        const h2 = document.createElement("h2");
        h2.id = "wk-tips";
        document.body.append(h2);
    }


    /**
     * 更新文字到 h2 元素
     * @param {string} text 
     */
    function update(text) {
        wk$("#wk-tips")[0].textContent = text;
    }


    /**
     * 被动连接,取出数据,请求并分割图片,导出PDF
     */
    function mainTask() {
        const sock = new utils.Socket(opener);
        sock.listen(async e => {
            if (e.data.wk && e.data.action) {
                update("图片下载中,请耐心等待...");

                const url = e.data.img_url;
                const resp = await fetch(url);
                update("图片下载完成,正在解析...");

                const buffer = await resp.arrayBuffer();
                const whole_data = new Uint8Array(buffer);
                update("图片解析完成,正在合并...");
                
                await utils.imgs_to_pdf(
                    utils.split_files_by_head(whole_data),
                    e.data.title
                );
                update("图片合并完成,正在导出 PDF...");
            }
        });
        sock.connect(true);
    }


    /**
     * 爱问文库图片下载策略v2
     * @returns 
     */
    function ishareData2() {
        showTips$1();
        if (!(window.opener && window.opener.window)) {
            update("wk: 抱歉,页面出错了");
            return;
        }
        mainTask();
    }

    function getPageNum() {
        // ' / 6 ' -> ' 6 '
        return parseInt(
            wk$("span.counts")[0].textContent.split("/")[1]
        );
    }


    function jumpToHostage() {
        const
            // '/fileroot/2019-9/23/73598bfa-6b91-4cbe-a548-9996f46653a2/73598bfa-6b91-4cbe-a548-9996f46653a21.gif'
            url = new URL(wk$("#pageflash_1 > img")[0].src),
            num = getPageNum(),
            // '七年级上册地理期末试卷精编.doc-得力文库'
            fname = document.title.slice(0, -5),
            path = url.pathname,
            tail = "1.gif";
        
        if (!path.endsWith(tail)) {
            throw new Error(`url尾部不为【${tail}】!path:【${path}】`);
        }
        const base_path = path.slice(0, -5);
        open(`${url.protocol}//${url.host}/?num=${num}&lmt=${lmt}&fname=${fname}&path=${base_path}`);
    }


    function deliwenku() {
        utils.create_btns();
        utils.onclick(jumpToHostage, 1, "到下载页面");
    }

    function showTips() {
        const body = `
        <style>
            h1 { color: black; } 
            #main {
                margin: 1vw 5%;
                border-radius: 10%;
            }
            p { font-size: large; }
            .info {
                color: rgb(230,214,110);
                background: rgb(39,40,34);
                text-align: right;
                font-size: medium;
                padding: 1vw;
                border-radius: 4px;
            }
        </style>
        <div id="main">
            <h1>wk: 跳板页面</h1>
            <p>有时候点一次下载等半天没反应,就再试一次</p>
            <p>如果试了 2 次还不行加 QQ 群反馈吧...</p>
            <p>导出的 PDF 如果页面数量少于应有的,那么意味着免费页数就这么多,我也爱莫能助</p>
            <p>短时间连续使用导出按钮会导致 IP 被封禁</p>
            <hr>
            <div class="info">
                文档名称:${deliJS.fname}<br>
                原始文档页数:${deliJS.num}<br>
                最大免费页数:${deliJS.lmt}<br>
            </div>
        </div>`;
        document.title = utils.get_param("fname");    document.body.innerHTML = body;
    }


    /**
     * url生成器
     * @param {string} base_url 
     * @param {number} num 
     */
    function* genURLs(base_url, num) {
        for (let i=1; i<=num; i++) {
            yield `${base_url}${i}.gif`;
        }
    }


    function genBaseURL(path) {
        return `${location.protocol}//${location.host}${path}`;
    }


    function parseParamsToDeliJS() {
        const
            base_url = genBaseURL(utils.get_param("path")),
            fname = utils.get_param("fname"),
            num = parseInt(utils.get_param("num"));

        let lmt = parseInt(utils.get_param("lmt"));
        lmt = lmt > 3 ? lmt : 20;
        lmt = lmt > num ? num : lmt;

        window.deliJS = {
            base_url,
            num,
            fname,
            lmt
        };
    }


    async function exportPDF$2() {
        utils.toggle_btn(1);
        await utils.run_with_prog(
            1, () => utils.img_urls_to_pdf(
                genURLs(deliJS.base_url, deliJS.num),
                deliJS.fname,
                deliJS.lmt,
                true  // 请求完成后清理控制台
            )
        );
        utils.toggle_btn(1);
    }


    /**
     * 得力文库跳板页面下载策略
     */
    async function deliFile() {
        // 从URL解析文档参数
        parseParamsToDeliJS();
        // 显示提示
        showTips();

        // 创建按钮区
        utils.create_btns();
        // btn_1: 导出PDF
        utils.onclick(exportPDF$2, 1, "导出PDF");
    }

    function readAll360Doc() {
        // 展开文档
        document.querySelector(".article_showall a").click();
        // 隐藏按钮
        utils.toggle_btn(1);
        // 显示按钮
        utils.toggle_btn(2);
        utils.toggle_btn(3);
        utils.toggle_btn(4);
    }


    function saveText_360Doc() {
        // 捕获图片链接
        let images = wk$("#artContent img");
        let content = [];

        for (let i = 0; i < images.length; i++) {
            let src = images[i].src;
            content.push(`图${i+1},链接:${src}`);
        }
        // 捕获文本
        let text = wk$("#artContent")[0].textContent;
        content.push(text);

        // 保存纯文本文档
        let title = wk$("#titiletext")[0].textContent;
        utils.save(`${title}.txt`, content.join("\n"));
    }


    /**
     * 使文档在页面上居中
     * @param {string} selector 文档容器的css选择器
     * @param {string} default_offset 文档部分向右偏移的百分比(0-59)
     * @returns 偏移值是否合法
     */
    function centre(selector, default_offset) {
        const elem = wk$(selector)[0];
        const offset = prompt("请输入偏移百分位:", default_offset);
        
        // 如果输入的数字不在 0-59 内,提醒用户重新设置
        if (offset.length === 1 && offset.search(/[0-9]/) !== -1) {
            elem.style.marginLeft = offset + "%";
            return true;
        }

        if (offset.length === 2 && offset.search(/[1-5][0-9]/) !== -1) {
            elem.style.marginLeft = offset + "%";
            return true;
        }

        alert("请输入一个正整数,范围在0至59之间,用来使文档居中");
        return false;
    }


    function printPage360Doc() {
        if (!confirm("确定每页内容都加载完成了吗?")) {
            return;
        }
        // # 清理并打印360doc的文档页
        // ## 移除页面上无关的元素
        let selector = ".fontsize_bgcolor_controler, .atfixednav, .header, .a_right, .article_data, .prev_next, .str_border, .youlike, .new_plbox, .str_border, .ul-similar, #goTop2, #divtort, #divresaveunder, .bottom_controler, .floatqrcode";
        let elem_list = wk$(selector);
        let under_doc_1, under_doc_2;
        try {
            under_doc_1 = wk$("#bgchange p.clearboth")[0].nextElementSibling;
            under_doc_2 = wk$("#bgchange")[0].nextElementSibling.nextElementSibling;
        } catch (e) {}
        // 执行移除
        for (let elem of elem_list) {
            utils.remove(elem);
        }
        utils.remove(under_doc_1);
        utils.remove(under_doc_2);
        // 执行隐藏
        wk$("a[title]")[0].style.display = "none";

        // 使文档居中
        alert("建议使用:\n偏移量: 20\n缩放: 默认\n");
        if (!centre(".a_left", "20")) {
            return; // 如果输入非法,终止函数调用
        }
        // 隐藏按钮,然后打印页面
        utils.print_page();
    }


    /**
     * 阻止监听器生效
     * @param {Event} e 
     */
    function stopSpread(e) {
        e.stopImmediatePropagation();
        e.stopPropagation();
    }


    /**
     * 阻止捕获事件
     */
    function stopCapturing() {
        ["click", "mouseup"].forEach(
            type => {
                document.body.addEventListener(type, stopSpread, true);
                document["on" + type] = undefined;
            }
        );
        
        ["keypress", "keydown"].forEach(
            type => {
                window.addEventListener(type, stopSpread, true);
                window["on" + type] = undefined;
            }
        );
    }


    /**
     * 重置图像链接和最大宽度
     * @param {Document} doc
     */
    function resetImg(doc=document) {
        wk$.call(doc, "img").forEach(
            elem => {
                elem.style.maxWidth = "100%";
                for (let attr of elem.attributes) {
                    if (attr.name.endsWith("-src")) {
                        elem.setAttribute("src", attr.value);
                        break;
                    }
                }
            }
        );
    }


    /**
     * 仅保留全屏文档
     */
    function getFullScreen() {
        FullScreenObj.init();
        wk$("#artContent > p:nth-child(3)")[0]?.remove();
        let data = wk$("#artfullscreen__box_scr > table")[0].outerHTML;
        window.doc360JS = { data };
        let html_str = `
        <html><head></head><body style="display: flex; flex-direction: row; justify-content: space-around">
            ${data}
        </body><html>
    `;
        wk$("html")[0].replaceWith(wk$("html")[0].cloneNode());
        wk$("html")[0].innerHTML = html_str;
        resetImg();
    }


    function cleanPage() {
        getFullScreen();
        stopCapturing();
    }


    /**
     * 360doc个人图书馆下载策略
     */
    function doc360() {
        // 创建按钮区
        utils.create_btns();
        // btn_1: 展开文档
        utils.onclick(readAll360Doc, 1);
        // btn_2: 导出纯文本
        utils.onclick(saveText_360Doc, 2, "导出纯文本");
        // btn_3: 打印页面到PDF
        utils.onclick(printPage360Doc, 3, "打印页面到PDF");
        // btn_3: 清理页面
        utils.onclick(cleanPage, 4, "清理页面(推荐)");
    }

    async function getPDF() {
        if (!window.DEFAULT_URL) {
            alert("当前文档无法解析,请加 QQ 群反馈");
            return;
        }
        let title = document.title.split(" - ")[0] + ".pdf";
        let blob = await utils.xhr_get_blob(DEFAULT_URL);
        utils.save(title, blob);
    }


    function mbalib() {
        utils.create_btns();
        utils.onclick(getPDF, 1, "下载PDF");
    }

    /**
     * 判断是否进入预览模式
     * @returns Boolean
     */
    function isInPreview() {
        let p_elem = wk$("#preview_tips")[0];
        if (p_elem && p_elem.style && p_elem.style.display === "none") {
            return true;
        }
        return false;
    }


    /**
     * 确保进入预览模式
     */
    async function ensureInPreview() {
        while (!isInPreview()) {
            // 如果没有进入预览,则先进入
            if (typeof window.preview !== "function") {
                alert("脚本失效,请加 QQ 群反馈");
                throw new Error("preview 全局函数不存在");
            }

            await utils.sleep(500);
            preview();
        }
    }


    /**
     * 前往页码
     * @param {number} page_num 
     */
    function toPage(page_num) {
        // 先尝试官方接口,不行再用模拟的
        try {
            Viewer._GotoPage(page_num);
        } catch(e) {
            console.error(e);
            utils.to_page(
                wk$("#pageNumInput")[0],
                page_num,
                "keydown"
            );
        }
    }


    /**
     * 展开全文预览,当展开完成后再次调用时,返回true
     * @returns 
     */
    async function walkThrough$1() {
        // 隐藏页面
        wk$("#pageContainer")[0].style.display = "none";

        // 逐页加载
        let lmt = window.dugenJS.lmt;
        for (let i of utils.range(1, lmt + 1)) {
            toPage(i);
            await utils.wait_until(
                () => wk$(`#outer_page_${i}`)[0].style.width.endsWith("px")
            );
        }

        // 恢复显示
        wk$("#pageContainer")[0].style.display = "";
        console.log(`共 ${lmt} 页加载完毕`);
    }


    /**
     * 返回当前未加载页面的页码
     * @returns not_loaded
     */
    function getNotloadedPages() {
        // 已经取得的页码
        let pages = document.querySelectorAll("[id*=pageflash_]");
        let loaded = new Set();
        pages.forEach((page) => {
            let id = page.id.split("_")[1];
            id = parseInt(id);
            loaded.add(id);
        });
        // 未取得的页码
        let not_loaded = [];
        for (let i of utils.range(1, window.dugenJS.lmt + 1)) {
            if (!loaded.has(i)) {
                not_loaded.push(i);
            }
        }
        return not_loaded;
    }


    /**
     * 取得全部文档页面的链接,返回urls;如果有页面未加载,则返回null
     * @returns
     */
    function getImgUrls() {
        let pages = wk$("[id*=pageflash_]");
        // 尚未浏览完全部页面,返回false
        if (pages.length < window.dugenJS.lmt) {
            let hints = [
                "尚未加载完全部页面",
                "以下页面需要浏览并加载:",
                getNotloadedPages().join(",")
            ];
            alert(hints.join("\n"));
            return [false, []];
        }
        // 浏览完全部页面,返回urls
        return [true, pages.map(page => page.querySelector("img").src)];
    }


    function exportImgUrls() {
        let [ok, urls] = getImgUrls();
        if (!ok) {
            return;
        }
        utils.save("urls.csv", urls.join("\n"));
    }


    function exportPDF$1() {
        let [ok, urls] = getImgUrls();
        if (!ok) {
            return;
        }
        let title = document.title.split("-")[0];
        return utils.run_with_prog(
            3, () => utils.img_urls_to_pdf(urls, title)
        );
    }


    /**
     * dugen文档下载策略
     */
    async function dugen() {
        await ensureInPreview();
        // 全局对象
        window.dugenJS = {
            lmt: window.lmt ? window.lmt : 20
        };

        // 创建按钮区
        utils.create_btns();

        // 绑定监听器
        // 按钮1:展开文档
        utils.onclick(walkThrough$1, 1, "加载可预览页面");
        // 按钮2:导出图片链接
        utils.onclick(exportImgUrls, 2, "导出图片链接");
        utils.toggle_btn(2);
        // 按钮3:导出PDF
        utils.onclick(exportPDF$1, 3, "导出PDF");
        utils.toggle_btn(3);
    }

    // 域名级全局常量
    const img_tasks = [];


    /**
     * 取得文档类型
     * @returns {String} 文档类型str
     */
    function getDocType() {
        const
            // ["icon", "icon-format", "icon-format-doc"]
            elem = wk$(".title .icon.icon-format")[0],
            // "icon-format-doc"
            cls = elem.classList[2];    
        return cls.split("-")[2];
    }


    /**
     * 判断文档类型是否为type_list其中之一
     * @returns 是否为type
     */
    function isTypeof(type_list) {
        const type = getDocType();
        if (type_list.includes(type)) {
            return true;
        }
        return false;
    }


    /**
     * 判断文档类型是否为PPT
     * @returns 是否为PPT
     */
    function is_ppt() {
        return isTypeof(["ppt", "pptx"]);
    }


    /**
     * 判断文档类型是否为Excel
     * @returns 是否为Excel
     */
    function is_excel() {
        return isTypeof(["xls", "xlsm", "xlsx"]);
    }


    /**
     * 取得未加载页面的页码
     * @returns {Array} not_loaded 未加载页码列表
     */
    function getNotLoaded() {
        const loaded = wk$("[data-id] img[src]").map(
            img => parseInt(
                img.closest("[data-id]").getAttribute("data-id")
            )
        );
        return Array.from(
            utils.diff(
                utils.range(1, window.book118JS.page_counts + 1),
                loaded
            )
        );
    }


    /**
     * 取得全部文档页的url
     * @returns [<是否全部加载>, <urls列表>, <未加载页码列表>]
     */
    function getUrls() {
        const urls = wk$("[data-id] img[src]").map(
            img => img.src
        );
        // 如果所有页面加载完毕
        if (urls.length === book118JS.page_counts) {
            return [true, urls, []];
        }
        // 否则收集未加载页面的url
        return [false, urls, getNotLoaded()];
    }


    /**
     * 展开全文
     */
    async function walkThrough() {
        // 遍历期间隐藏按钮区
        utils.toggle_box();

        // 取得总页码
        // preview.getPage()
        // {current: 10, actual: 38, preview: 38, remain: 14}
        const { preview: all } = preview.getPage();
        for (let i = 1; i <= all; i++) {
            // 逐页加载
            preview.jump(i);
            await utils.wait_until(
                () => wk$(`[data-id="${i}"] img`)[0].src, 1000
            );
        }
        console.log("遍历完成");
        utils.toggle_box();
    }


    /**
     * btn_2: 导出图片链接
     */
    function wantUrls() {
        let [flag, urls, escaped] = getUrls();
        // 页面都加载完毕,下载urls
        if (!flag) {
            // 没有加载完,提示出未加载好的页码
            const hint = [
                "仍有页面没有加载",
                "请浏览并加载如下页面",
                "是否继续导出图片链接?",
                "[" + escaped.join(",") + "]"
            ].join("\n");
            // 终止导出
            if (!confirm(hint)) {
                return
            }
        }
        utils.save("urls.csv", urls.join("\n"));
    }


    /**
     * 打开PPT预览页面
     */
    async function open_iframe() {
        wk$(".front a")[0].click();
        const iframes = await wk$$("iframe.preview-iframe");
        window.open(iframes[0].src);
    }


    /**
     * 取得最大页码
     * @returns {number} 最大页码
     */
    function getPageCounts$1() {
        return window?.preview?.getPage()?.preview || NaN;
    }


    /**
     * 原创力文档(非PPT或Excel)下载策略
     */
    async function common_doc() {
        await utils.wait_until(
            () => !!wk$(".counts")[0]
        );

        // 创建全局对象
        window.book118JS = {
            doc_type: getDocType(),
            page_counts: getPageCounts$1()
        };

        // 处理非PPT文档
        // 创建按钮组
        utils.create_btns();
        // 绑定监听器到按钮
        // 按钮1:加载全文
        utils.onclick(walkThrough, 1, "加载全文");
        // 按钮2:导出图片链接
        utils.onclick(wantUrls, 2, "导出图片链接");
        utils.toggle_btn(2);
    }


    /**
     * @returns {string}
     */
    function table_to_tsv() {
        return wk$("table").map(table => {
            // 剔除空表和行号表
            const len = table.rows.length;
            if (len > 1000 || len === 1) {
                return "";
            }

            // 遍历行
            return [...table.rows].map(row => {
                // 遍历列(单元格)
                return [...row.cells].map(cell => {
                    // 判断单元格是否存储图片
                    const img = cell.querySelector("img");
                    if (img) {
                        // 如果是图片,保存图片链接
                        return img.src;
                    }
                    
                    // 否则保存单元格文本
                    return cell
                        .textContent
                        .trim()
                        .replace(/\n/g, "  ")
                        .replace(/\t/g, "    ");
                }).join("\t");
            }).join("\n").trim();
        }).join("\n\n---\n\n");
    }


    /**
     * 下载当前表格内容,保存为csv(utf-8编码)
     */
    function wantEXCEL() {
        const tsv = table_to_tsv();
        const bytes = utils.encode_to_gbk(tsv);
        const fname = "原创力表格.tsv";
        utils.save(fname, bytes);
    }


    /**
     * 在Excel预览页面给出操作提示
     */
    function help$1() {
        const hint = [
            "【导出表格到TSV】只能导出当前 sheet",
            "如果有多张 sheet 请在每个 sheet 上用按钮分别导出 TSV",
            "TSV 文件请用记事本或 Excel 打开",
            "TSV 不能存储图片,所以用图片链接代替",
            "或使用此脚本复制表格到剪贴板:",
            "https://greasyfork.org/zh-CN/scripts/469550",
        ];
        alert(hint.join("\n"));
    }


    /**
     * 原创力文档(EXCEL)下载策略
     */
    function excel() {
        // 创建按钮区
        utils.create_btns();
        // 绑定监听器到按钮
        utils.onclick(wantEXCEL, 1, "导出表格到TSV");
        utils.onclick(help$1, 2, "使用说明");
        // 显示按钮
        utils.toggle_btn(2);
    }


    /**
     * ------------------------------ PPT 策略 ---------------------------------
     */


    /**
     * 返回当前页码
     * @returns {number}
     */
    function cur_page_num() {
        return parseInt(
            wk$("#PageIndex")[0].textContent
        );
    }


    function add_page() {
        const view = wk$("#view")[0];
        view.setAttribute("style", "");

        const i = cur_page_num() - 1;
        const cur_view = wk$(`#view${i}`)[0];

        img_tasks.push(
            html2canvas(cur_view)
        );
        utils.btn(1).textContent = `截图: ${img_tasks.length}`;
    }


    function reset_tasks() {
        img_tasks.splice(0);
        utils.btn(1).textContent = `截图: 0`;
    }


    function canvas_to_blob(canvas) {
        return utils.canvas_to_blob(canvas);
    }


    async function export_imgs_as_pdf() {
        alert("正在合并截图,请耐心等待");
        utils.toggle_btn(3);

        try {
            const imgs = await utils.gather(img_tasks);
            const blobs = await utils.gather(
                imgs.map(canvas_to_blob)
            );

            if (!blobs.length) {
                alert("你尚未截取任何页面!");
            } else {
                await utils.img_blobs_to_pdf(blobs, "原创力幻灯片");
            }
        } catch(err) {
            console.error(err);
        }
        
        utils.toggle_btn(3);
    }



    function ppt() {
        utils.create_btns();

        const btn1 = utils.btn(1);
        btn1.onclick = add_page;
        btn1.textContent = "截图当前页面";

        utils.onclick(reset_tasks, 2, "清空截图");
        utils.onclick(export_imgs_as_pdf, 3, "合并为PDF");

        utils.toggle_btn(2);
        utils.toggle_btn(3);
    }


    /**
     * 原创力文档下载策略
     */
    function book118() {
        const host = window.location.hostname;

        if (host === 'max.book118.com') {
            if (is_excel()) {
                utils.create_btns();
                utils.onclick(open_iframe, 1, "访问EXCEL");
            } else if (is_ppt()) {
                utils.create_btns();
                utils.onclick(open_iframe, 1, "访问PPT");
            } else {
                common_doc();
            }
        
        } else if (wk$("#ppt")[0]) {
            if (window.top !== window) return;
            ppt();
        
        } else if (wk$(`[src*="excel.min.js"]`)[0]) {
            excel();
        
        } else {
            console.log(`wk: Unknown host: ${host}`);
        }
    }

    // test url: https://openstd.samr.gov.cn/bzgk/gb/newGbInfo?hcno=E86BBCE32DA8E67F3DA04ED98F2465DB


    /**
     * 绘制0x0的bmp, 作为请求失败时返回的page
     * @returns {Promise<ImageBitmap>} blank_page
     */
    async function blankBMP() {
        let canvas = document.createElement("canvas");
        [canvas.width, canvas.height] = [0, 0];
        return createImageBitmap(canvas);
    }


    /**
     * resp导出bmp
     * @param {string} page_url 
     * @param {Promise<Response> | ImageBitmap} pms_or_bmp 
     * @returns {Promise<ImageBitmap>} page
     */
    async function respToPage(page_url, pms_or_bmp) {
        let center = globalThis.gb688JS;
        // 此时是bmp
        if (pms_or_bmp instanceof ImageBitmap) {
            return pms_or_bmp;
        }

        // 第一次下载, 且无人处理
        if (!center.pages_status.get(page_url)) {
            // 处理中, 设为占用
            center.pages_status.set(page_url, 1);

            // 处理
            let resp;
            try {
                resp = await pms_or_bmp;
            } catch(err) {
                console.log("下载页面失败");
                console.error(err);
                return blankBMP();
            }

            let page_blob = await resp.blob();
            let page = await createImageBitmap(page_blob);
            center.pages.set(page_url, page);
            
            // 处理结束, 设为释放
            center.pages_status.set(page_url, 0);
            return page;
        }

        // 有人正在下载且出于处理中
        while (center.pages_status.get(page_url)) {
            await utils.sleep(500);
        }
        return center.pages.get(page_url);
    }


    /**
     * 获得PNG页面
     * @param {string} page_url 
     * @returns {Promise<ImageBitmap>} bmp
     */
    async function getPage(page_url) {
        // 如果下载过, 直接返回缓存
        let pages = globalThis.gb688JS.pages;
        if (pages.has(page_url)) {
            return respToPage(page_url, pages.get(page_url));
        }

        // 如果从未下载过, 就下载
        let resp = fetch(page_url, {
            "headers": {
                "accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
                "accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
                "proxy-connection": "keep-alive"
            },
            "referrer": location.href,
            "referrerPolicy": "strict-origin-when-cross-origin",
            "body": null,
            "method": "GET",
            "mode": "cors",
            "credentials": "include"
        });
        pages.set(page_url, resp);
        return respToPage(page_url, resp);
    }


    /**
     * 返回文档页div的裁切和粘贴位置信息: [[cut_x, cut_y, paste_x%, paset_y%],...]
     * @param {HTMLDivElement} page_div 文档页元素
     * @returns {Array<Array<number>>} positions
     */
    function getPostions(page_div) {
        let positions = [];

        Array.from(page_div.children).forEach(span => {
            // 'pdfImg-3-8' -> {left: 30%; top: 80%;}
            let paste_pos = span.className.split("-").slice(1).map(
                v => parseInt(v) / 10
            );
            // '-600px 0px' -> [600, 0]
            let cut_pos = span.style.backgroundPosition.split(" ").map(
                v => Math.abs(parseInt(v))
            );
            positions.push([...cut_pos, ...paste_pos]);
        });
        return positions;
    }


    /**
     * 取得文档页的图像url
     * @param {HTMLDivElement} page_div 
     * @returns {string} url
     */
    function getPageURL(page_div) {
        // 拿到目标图像url
        let path = location.pathname.split("/").slice(0, -1).join("/");
        let prefix = location.origin + path + "/";
        let url = page_div.getAttribute("bg");
        if (!url) {
            // 'url("viewGbImg?fileName=VS72l67k0jw5g3j0vErP8DTsnWvk5QsqnNLLxaEtX%2FM%3D")'
            url = page_div.children[0].style.backgroundImage.split('"')[1];
        }
        return prefix + url;
    }


    /**
     * 下载目标图像并拆解重绘, 返回canvas
     * @param {number} i 第 i 页 (从0开始)
     * @param {HTMLDivElement} page_div
     * @returns {Promise<Array>} [页码, Canvas]
     */
    async function getAndDrawPage(i, page_div) {
        // 拿到目标图像
        let url = getPageURL(page_div);
        let page = await getPage(url);

        // 绘制空白A4纸背景
        const [page_w, page_h] = [
            parseInt(page_div.style.width),
            parseInt(page_div.style.height)
        ];
        let bg = document.createElement("canvas");
        bg.width = page_w;  // 注意canvas作为取景框的大小
        bg.height = page_h;  // 如果不设置等于一个很小的取景框(300x300)
        
        let bg_ctx = bg.getContext("2d");
        bg_ctx.fillStyle = "white";
        bg_ctx.fillRect(0, 0, page_w, page_h);

        // 逐个区块剪切取出并粘贴
        const [part_w, part_h] = [page_w / 10, page_h / 10];
        getPostions(page_div).forEach(pos => {
            bg_ctx.drawImage(
                page,  // image source
                pos[0],  // source x
                pos[1],  // source y
                part_w,  // source width
                part_h,  // source height
                pos[2] * page_w,  // destination x = left: x%
                pos[3] * page_h,  // destination y = top: y%
                part_w,  // destination width
                part_h  // destination height
            );
        });
        // });
        return [i, bg];
    }


    /**
     * 页面批量请求、裁剪重绘, 合成PDF并下载
     */
    async function turnPagesToPDF() {
        // 渲染每页
        const tasks = wk$("#viewer .page").map(
            (page_div, i) => getAndDrawPage(i, page_div)
        );
        
        // 等待每页渲染完成后,排序
        const results = await utils.gather(tasks);
        results.sort((prev, next) => prev[0] - next[0]);
        
        // 合并为PDF并导出
        return utils.imgs_to_pdf(
            results.map(item => item[1]),
            // '在线预览|GB 14023-2022'
            document.title.split("|")[1]
        );
    }


    /**
     * 提示预估下载耗时,然后下载
     */
    function hintThenDownload$1() {
        // '/93'
        let page_num = parseInt(wk$("#numPages")[0].textContent.slice(1));
        let estimate = Math.ceil(page_num / 3);
        alert(`页数: ${page_num},预计花费: ${estimate}秒;如遇网络异常可能更久\n请勿反复点击按钮;如果无法导出请 QQ 群反馈`);
        turnPagesToPDF();
    }


    /**
     * gb688文档下载策略
     */
    async function gb688() {
        // 创建全局对象
        globalThis.gb688JS = {
            pages: new Map(),  // {url: bmp}
            pages_status: new Map()  // {url: 0或1} 0释放, 1占用
        };

        // 创建按钮区
        utils.create_btns();
        // 绑定监听器
        // 按钮1:导出PDF
        utils.onclick(hintThenDownload$1, 1, "导出PDF");
    }

    function getPageCounts() {
        // " / 39"
        const counts_str = wk$(".counts")[0].textContent.split("/")[1];
        const counts = parseInt(counts_str);
        return counts > 20 ? 20 : counts;
    }


    /**
     * 返回图片基础路径
     * @returns {string} base_url
     */
    function getImgBaseURL() {
        return wk$("#dp")[0].value;
    }


    function* genImgURLs$1() {
        let counts = getPageCounts();
        let base_url = getImgBaseURL();
        for (let i = 1; i <= counts; i++) {
            yield base_url + `${i}.gif`;
        }
    }


    /**
     * 下载图片,转为canvas,合并为PDF并下载
     * @returns {Promise<void>}
     */
    function fetchThenExportPDF() {
        // db2092-2014-河北特种设备使用安全管理规范_安全文库网safewk.com
        let title = document.title.split("_")[0];
        return utils.img_urls_to_pdf(genImgURLs$1(), title);
    }


    /**
     * 提示预估下载耗时,然后下载
     */
    function hintThenDownload() {
        let hint = [
            "只能导出可预览的页面(最多20页)",
            "请勿短时间反复点击按钮,导出用时大约不到 10 秒",
            "点完后很久没动静请至 QQ 群反馈"
        ];
        alert(hint.join("\n"));
        return utils.run_with_prog(
            1, fetchThenExportPDF
        );
    }


    /**
     * safewk文档下载策略
     */
    async function safewk() {
        // 创建按钮区
        utils.create_btns();
        // 绑定监听器
        // 按钮1:导出PDF
        utils.onclick(
            hintThenDownload, 1, "导出PDF"
        );
    }

    /**
     * 跳转到页码
     * @param {string | number} num 
     */
    function _to_page(num) {
        if (window.WebPreview
            && WebPreview.Page
            && WebPreview.Page.jump
        ) {
            WebPreview.Page.jump(parseInt(num));
        } else {
            console.error("window.WebPreview.Page.jump doesn't exist");
        }
    }


    /**
     * 跳转页码GUI版
     */
    function to_page() {
        let num = prompt("请输入要跳转的页码")?.trim();
        if (/^[0-9]+$/.test(num)) {
            _to_page(num);
        } else {
            console.log(`输入值 [${num}] 不是合法整数`);
        }
    }


    function capture_urls() {
        if (!confirm(
            "只能导出已经预览页面的链接,是否继续?"
        )) return;

        let imgs = wk$("[data-id] img");
        if (imgs.length === 0) {
            imgs = wk$("img[data-page]");
        }
        console.log(imgs);

        const urls = imgs.map(img => {
            const src = img.dataset.src || img.src;
            if (!src) return;
            return src.startsWith("//") ? "https:" + src : src
        });
        
        const lacked = [];
        const existed = urls.filter((url, i) => {
            if (url) return true;
            lacked.push(i + 1);
        });

        utils.save_urls(existed);
        alert(
            `已经浏览的页面中有 ${lacked.length} 页图片尚未加载,` +
            `已经从结果中剔除。\n它们的页码是:\n${lacked}`
        );
    }


    function* genImgURLs() {
        const params = window?.previewParams;
        if (!params) throw new Error(
            "接口为空: window.previewParams"
        );

        let i = -4;
        const
            base = "https://openapi.renrendoc.com/preview/getPreview?",
            query = {
                temp_view: 0,
                jsoncallback: "a",
                callback: "b",
                encrypt: params.encrypt,
                doc_id: params.doc_id,
                get _() { return Date.now() },
                get start() { return i += 5; },
            };
        
        while (true) {
            const keys = Reflect.ownKeys(query);
            yield base + keys.map(
                key => `${key}=${query[key]}`
            ).join("&");
        }
    }


    async function _fetch_preview_urls() {
        let
            is_empty = true,
            switch_counts = 0,
            previews = [];
        
        for (const [i, url] of utils.enumerate(genImgURLs())) {
            const resp = await fetch(url);
                utils.raise_for_status(resp);
                const raw_data = await resp.text(),
                data = raw_data.slice(2, -1),
                img_urls = JSON
                    .parse(data)
                    .data
                    ?.preview_list
                    ?.map(pair => pair.url);
            if (!img_urls) break;

            previews = previews.concat(...img_urls);
            utils.update_popup(`已经请求 ${i + 1} 组图片链接`);
            
            if (is_empty !== (img_urls.length ? false : true)) {
                is_empty = !is_empty;
                switch_counts++;
            }
            if (switch_counts === 2) break;

            await utils.sleep(1000);
        }
        
        const
            params = window.previewParams,
            free = params.freepage || 20,
            base = params.pre || wk$(".page img")[0].src.slice(0, -5),
            free_urls = Array.from(
                utils.range(1, free + 1)
            ).map(
                n => `${base}${n}.gif`
            );

        const urls = free_urls.concat(...previews);
        utils.save_urls(urls);
    }


    function fetch_preview_urls() {
        return utils.run_with_prog(
            3, _fetch_preview_urls
        );
    }


    function help() {
        alert(
            "【捕获】和【请求】图片链接的区别:\n" + 
            " - 【捕获】是从当前已经加载的文档页中提取图片链接\n" + 
            " - 【请求】是使用官方接口直接下载图片链接\n" + 
            " - 【捕获】使用麻烦,但是稳定\n" + 
            " - 【请求】使用简单,速度快,但可能失效"
        );
    }


    /**
     * 人人文档下载策略
     */
    async function renrendoc() {
        utils.create_btns();
        
        utils.onclick(to_page, 1, "跳转到页码");
        utils.onclick(capture_urls, 2, "捕获图片链接");
        utils.onclick(fetch_preview_urls, 3, "请求图片链接");
        utils.onclick(help, 4, "使用说明");

        utils.toggle_btn(2);
        utils.toggle_btn(3);
        utils.toggle_btn(4);
    }

    /**
     * 取得全部图片连接
     * @returns {Array<string>}
     */
    function get_img_urls() {
        const src = wk$("#page1 img")[0]?.src;

        // 适用于图片类型
        if (src) {
            const path = src.split("?")[0].split("/").slice(3, -1).join("/");
            const origin = new URL(location.href).origin;
        
            const urls = window.htmlConfig.fliphtml5_pages.map(obj => {
                const fname = obj.n[0].split("?")[0].split("/").at(-1);
                return `${origin}/${path}/${fname}`;
            });
            const unique = [...new Set(urls)];
        
            window.img_urls = unique;
            return unique;
        }

        // 适用于其他类型
        const relative_path = wk$(".side-image img")[0].getAttribute("src").split("?")[0];
        // ../files/large/
        const relative_dir = relative_path.split("/").slice(0, -1).join("/") + "/";

        const base = location.href;
        const urls = window.htmlConfig.fliphtml5_pages.map(obj => {
            // "../files/large/d8b6c26f987104455efb3ec5addca7c9.jpg"
            const path = relative_dir + obj.n[0].split("?")[0];
            const url = new URL(path, base);
            // https://book.yunzhan365.com/mctl/itid/files/large/d8b6c26f987104455efb3ec5addca7c9.jpg
            return url.href.replace("/thumb/", "/content-page/");
        });

        window.img_urls = urls;
        return urls;
    }


    function imgs_to_pdf() {
        const urls = get_img_urls();
        const title = document.title;
        const task = () => utils.img_urls_to_pdf(urls, title);

        utils.run_with_prog(1, task);
        alert(
            "正在下载图片,请稍等,时长取决于图片数量\n" +
            "如果导出的文档只有一页空白页,说明当前文档不适用"
        );
    }


    /**
     * 将数组中的连续数字描述为字符串
     * 例如 [1, 2, 3, 5] => "1 - 3, 5"
     * @param {number[]} nums 整数数组
     * @returns {string} 描述数组的字符串
     */
    function describe_nums(nums) {
        let result = "";
        let start = nums[0];
        let end = nums[0];
        
        for (let i = 1; i < nums.length; i++) {
            if (nums[i] === end + 1) {
                end = nums[i];
            } else {
                if (start === end) {
                result += start + ", ";
                } else {
                result += start + " - " + end + ", ";
                }
                start = nums[i];
                end = nums[i];
            }
        }
        
        if (start === end) {
            result += start;
        } else {
            result += start + " - " + end;
        }
        
        return result;
    }


    /**
     * 取得总页码(作为str)
     * @returns {string}
     */
    function get_total() {
        const total = window?.bookConfig?.totalPageCount;
        if (total) {
            return String(total);
        }
        return wk$("#tfPageIndex input")[0].value.split("/")[1].trim();
    }


    /**
     * 下载稀疏数组的pdf数据,每个元素应该是 [pdf_blob, pwd_str]
     * @param {Array} pdfs_data 
     */
    async function data_to_zip(pdfs_data) {
        // 导入jszip
        await utils.blobs_to_zip([], "empty", "dat", "empty", false);

        // 分装截获的数据
        const page_nums = Object.keys(pdfs_data)
            .map(index => parseInt(index) + 1);
        const len = page_nums.length;
        const pwds = new Array(len + 1);
        pwds[0] = "page-num,password";
        
        // 创建压缩包,归档加密的PDF页面
        const zip = new window.JSZip();
        const total = get_total();
        const digits = total.length;

        // 归档
        for (let i = 0; i < len; i++) {
            // 页码左侧补零
            const page_no = page_nums[i];
            const page_no_str = page_no.toString().padStart(digits, "0");
            // 记录密码
            pwds[i+1] = `${page_no_str},${pdfs_data[page_no - 1][1]}`;
            // 添加pdf内容到压缩包
            const blob = pdfs_data[page_no - 1][0];
            zip.file(`page-${page_no_str}.pdf`, blob, { binary: true });
        }
        console.log("zip:", zip);

        // 添加密码本到压缩包
        const pwds_blob = new Blob([pwds.join("\n")], { type: "text/plain" });
        zip.file(`密码本.txt`, pwds_blob, { binary: true });
        
        // 下载
        console.info("正在合成压缩包并导出,请耐心等待几分钟......");
        const zip_blob = await zip.generateAsync({ type: "blob" });
        utils.save(`${document.title}.zip`, zip_blob, "application/zip");
    }


    /**
     * 下载多个pdf为一个压缩包,其中包含一个密码本
     * @param {PointerEvent} event
     */
    async function export_zip(event) {
        // 异常判断
        if (!window.pdfs_data) utils.raise(`pdfs_data 不存在!`);

        // 确认是否继续导出PDF
        const page_nums = Object.keys(pdfs_data)
            .map(index => parseInt(index) + 1);
        const donwload = confirm(
            `已经捕获 ${page_nums.length} 个页面,是否导出?\n` +
            `已捕获的页码:${describe_nums(page_nums)}\n` + 
            `(如果某页缺失可以先多向后翻几页,然后翻回来,来重新加载它)`
        );
        if (!donwload) return;
        
        // 隐藏按钮
        const btn = event.target;
        btn.style.display = "none";

        // 下载压缩包
        await data_to_zip(pdfs_data);

        // 显示按钮
        btn.style.display = "block";
    }


    function steal_pdf_when_page_loaded() {
        // 共用变量
        // 存放pdf数据,[[<pdf_blob>, <pwd_str>], ...]
        window.pdfs_data = [];
        // 代表当前页码
        let page_no = NaN;

        // hook PdfLoadingTask.prototype.start
        const _start = PdfLoadingTask.prototype.start;
        wk$._start = _start;
        PdfLoadingTask.prototype.start = function() {
            // 取得页码
            page_no = this.index;

            // 如果不存在此页,则准备捕获此页面
            if (!pdfs_data[page_no - 1]) {
                pdfs_data[page_no - 1] = [];
            }
            
            return _start.call(this);
        };

        // hook getBlob
        const _get_blob = getBlob;
        wk$._get_blob = _get_blob;
        window.getBlob = async function(param) {
            const result = await _get_blob.call(this, param);
            // 如果当前页面需要捕获,则设置对应项的密码
            if (page_no > 0) {
                const resp = await fetch(result.url);
                const blob = await resp.blob();

                pdfs_data[page_no - 1] = [blob, result.password];
                page_no = NaN;
            } 
            return result;
        };

        utils.onclick(export_zip, 1, "导出PDF压缩包");
    }


    /**
     * 请求 url 并将资源转为 [pdf_blob, password_str]
     * @param {string} url
     * @returns {Array} 
     */
    async function url_to_item(url) {
        // 取得pdf数据
        const resp = await fetch(url);
        const buffer = await resp.arrayBuffer();
        const bytes = new Uint8Array(buffer);
        const len = bytes.length;

        // 更新进度
        window.downloaded_count++;
        window.downloaded_size += len;
        console.log(
            `已经下载了 ${downloaded_count} 页,\n` +
            `累计下载了 ${(downloaded_size / 1024 / 1024).toFixed(1)} MB`
        );

        // 取出密钥
        const pwd = new Uint8Array(6);
        pwd.set(bytes.subarray(1080, 1083));
        pwd.set(bytes.subarray(-1003, -1000), 3);
        const pwd_str = new TextDecoder().decode(pwd);

        // 解密出数据
        const pdf = bytes.subarray(1083, -1003);
        pdf.subarray(0, 4000).forEach((byte, i) => {
            pdf[i] = 255 - byte;
        });
        return [
            new Blob([pdf, pdf.subarray(4000)], { type: "application/pdf" }),
            pwd_str
        ];
    }


    /**
     * 直接下载并解析原始数据,导出PDF压缩包
     * @param {PointerEvent} event
     */
    async function donwload_zip(event) {
        // 隐藏按钮
        const btn = event.target;
        btn.style.display = "none";
        
        // 共用进度变量
        window.downloaded_count = 0;
        window.downloaded_size = 0;

        // 取得数据地址
        const urls = get_img_urls()
            .map(url => url.replace("/thumb/", "/content-page/"));
        // 批量下载
        const item_tasks = urls.map(url_to_item);
        const items = await utils.gather(item_tasks);
        // 导出ZIP
        await data_to_zip(items);

        // 显示按钮
        btn.style.display = "block";
    }


    /**
     * 导出图片到PDF
     */
    function judge_file_type() {
        const ext = window
            ?.htmlConfig
            ?.fliphtml5_pages[0]
            ?.n[0]
            ?.split("?")[0]
            ?.split(".").at(-1);

        console.log("ext:", ext);

        if (["zip"].includes(ext)
            && window?.PdfLoadingTask
            && window?.getBlob) {

            utils.onclick(steal_pdf_when_page_loaded, 1, "开始捕获");
            utils.onclick(donwload_zip, 2, "下载PDF压缩包");
            utils.toggle_btn(2);
        }
        else if (wk$("#page1 img")[0]) {
            utils.onclick(imgs_to_pdf, 1, "导出PDF");
        }
        else {
            utils.onclick(() => null, 1, "此文档不适用");
        }
    }


    /**
     * 云展网文档下载策略
     */
    async function yunzhan365() {
        // 根据网址分别处理
        if (location.pathname.startsWith("/basic")) {
            return;
        }

        // 创建脚本启动按钮
        utils.create_btns();
        judge_file_type();
    }

    /**
     * 导出图片链接
     */
    function exportURLs$1() {
        const all = parseInt(
            wk$("[class*=total]")[0]
        );
        const imgs = wk$("[class*=imgContainer] img");
        const got = imgs.length;

        if (got < all) {
            if (!confirm(
                `当前浏览页数:${got},总页数:${all}\n建议浏览剩余页面以导出全部链接\n是否继续导出链接?`
            )) {
                return;
            }
        }
        utils.save_urls(
            imgs.map(img => img.src)
        );
    }


    /**
     * 360文库文档下载策略
     */
    function wenku360() {
        utils.create_btns();
        utils.onclick(
            exportURLs$1, 1, "导出图片链接"
        );

        // utils.onclick(
        //     callAgent, 2, "导出PDF"
        // );
        // utils.toggle_btn(2);
    }

    async function getFileInfo() {
        const
            uid = new URL(location.href).searchParams.get("contentUid"),
            resp = await fetch("https://zyjy-resource.webtrn.cn/sdk/api/u/open/getResourceDetail", {
                "headers": {
                    "accept": "application/json, text/javascript, */*; q=0.01",
                    "content-type": "application/json",
                },
                "referrer": "https://jg.class.com.cn/",
                "body": `{"params":{"contentUid":"${uid}"}}`,
                "method": "POST",
            }),
            data = await resp.json(),
            url = data["data"]["downloadUrl"],
            fname = data["data"]["title"];

        let ext;
        try {
            // validate the URL format 
            // and get the file format
            ext = new URL(url).pathname.split(".").at(-1);
        } catch(e) {
            console.log(data);
            throw new Error("API changed, the script is invalid now.");
        }
        return { url, fname, ext };
    }


    /**
     * 保存文件
     * @param {{fname: string, url: string, ext: string}} info 
     */
    async function saveFile(info) {
        const
            resp = await fetch(info.url),
            blob = await resp.blob();
        utils.save(info.fname + `.${info.ext}`, blob);
    }


    /**
     * 劫持保存网页,改为保存文件
     * @param {KeyboardEvent} e 
     */
    function onCtrlS(e) {
        if (e.code === "KeyS" &&
            e.ctrlKey) {
            console.log("ctrl + s is captured!!");
            getFileInfo().then(info => saveFile(info));

            e.preventDefault();
            e.stopImmediatePropagation();
            e.stopPropagation();
        }
    }


    /**
     * 技工教育网文档策略
     */
    function jg() {
        window.addEventListener(
            "keydown", onCtrlS, true
        );
    }

    async function estimateTimeCost() {
        wk$(".w-page").at(-1).scrollIntoView();
        await utils.sleep(1000);

        let total = wk$("#pageNumber-text")[0].textContent.split("/")[1];
        total = parseInt(total);
        return confirm([
            "注意,一旦开始截图就无法停止,除非刷新页面。",
            "浏览器窗口最小化会导致截图提前结束!",
            "建议将窗口最大化,这将【显著增大清晰度和文件体积】",
            `预计耗时 ${1.1 * total} 秒,是否继续?`,
        ].join("\n"));
    }


    /**
     * 逐页捕获canvas
     * @returns {Promise<Array<Blob>>}
     */
    async function collectAll() {
        const imgs = [];
        let div = wk$(".w-page")[0];
        let i = 0;
        
        while (true) {
            // 取得 div
            const anchor = Date.now();
            while (!div && (Date.now() - anchor < 1000)) {
                console.log(`retry on page ${i+1}`);
                await utils.sleep(200);
            }
            if (!div) throw new Error(
                `can not fetch <div>: page ${i}`
            );
            
            // 移动到 div
            div.scrollIntoView({ behavior: "smooth" });
            await utils.sleep(1000);
            
            // 取得 canvas
            let canvas = wk$.call(div, "canvas")[0];
            let j = 0;
            while (!canvas && j < 100) {
                div = div.nextElementSibling;
                canvas = wk$.call(div, "canvas")[0];
                j++;
            }
            if (!div)  throw new Error(
                `can not fetch <div>: page ${i}*`
            );

            // 存储 canvas
            imgs.push(
                await utils.canvas_to_blob(canvas)
            );
            console.log(`canvas stored: ${++i}`);

            // 下一轮循环
            div = div.nextElementSibling;
            if (!div) break;
        }
        console.log("done");
        return imgs;
    }


    /**
     * 放大或缩小文档画面
     * @param {boolean} up 
     */
    async function scale(up) {
        let s = "#magnifyBtn";
        if (!up) {
            s = "#shrinkBtn";
        }
        const btn = wk$(s)[0];
        for (let _ of utils.range(10)) {
            btn.click();
            await utils.sleep(500);
        }
    }


    /**
     * 获取全部canvas,显示功能按钮
     * @returns 
     */
    async function prepare() {
        if (! await estimateTimeCost()) {
            return;
        }

        // 隐藏按钮
        utils.toggle_btn(1);
        // 放大画面
        await scale(true);

        let imgs;
        try {
            imgs = await collectAll();
        } catch(e) {
            console.error(e);
        } finally {
            // 缩小画面
            scale(false);
        }
        
        // window.imgs = imgs;
        // 显示功能按钮
        const fname = "技工教育网文档";
        utils.onclick(
            () => utils.img_blobs_to_pdf(imgs, fname),
            2,
            "导出PDF"
        );
        utils.toggle_btn(2);

        utils.onclick(
            () => utils.blobs_to_zip(imgs, "page", "png", fname),
            3,
            "导出ZIP"
        );
        utils.toggle_btn(3);
    }


    /**
     * 技工教育文档预览页面策略
     */
    function jgPreview() {
        utils.create_btns();
        utils.onclick(
            prepare, 1, "截图文档"
        );
    }

    /**
     * 取得文档标题
     * @returns {string}
     */
    function getTitle() {
        return document.title.slice(0, -4);
    }


    /**
     * 取得基础URL
     * @returns {string}
     */
    function getBaseURL$1() {
        return wk$("#dp")[0].value;
    }


    /**
     * 获取总页码
     * @returns {number}
     */
    function getTotalPageNum() {
        const num = wk$(".shop3 > li:nth-child(3)")[0]
            .textContent
            .split("/")[1]
            .trim();
        return parseInt(num);
    }


    /**
     * 返回图片链接生成器
     * @param {string} base 基础图片链接地址
     * @param {number} max 最大数量
     * @returns {Generator<string, void, unknown>}
     */
    function* imgURLsMaker(base, max) {
        for (let i of utils.range(1, max + 1)) {
            yield `${base}${i}.gif`;
        }
    }


    /**
     * 取得当前页面全部图片链接(生成器)
     * @returns {Generator<string, void, unknown>}
     */
    function getImgURLs() {
        const
            base = getBaseURL$1(),
            total = getTotalPageNum();
        return imgURLsMaker(base, total)
    }


    function exportPDF() {
        const urls = getImgURLs();
        const title = getTitle();
        return utils.run_with_prog(
            2, () => utils.img_urls_to_pdf(urls, title)
        );
    }


    function exportURLs() {
        const urls = getImgURLs();
        utils.save_urls(urls);
    }


    /**
     * 文库吧文档下载策略
     */
    function wenkub() {
        utils.create_btns();
        
        utils.onclick(
            exportURLs, 1, "导出图片链接"
        );

        utils.onclick(
            exportPDF, 2, "导出PDF(测试)"
        );
        utils.toggle_btn(2);
    }

    const KEY = "5zAUzyJv5xLoYyCCBJdxVw==";


    function* pageURLGen() {
        const
            url = new URL(location.href),
            params = url.searchParams,
            base = url.origin + (window.basePath || "/manuscripts/pdf"),
            type = params.get("type") || "pdf",
            id = params.get("id")
                || new URL(wk$("#pdfContent")[0].src).searchParams.get("id")
                || utils.raise("书本ID未知");
        
        let i = 0;
        let cur_url = "";
        
        if (window.wk_sklib_url) {
            console.log(`sklib 使用自定义 url: ${window.wk_sklib_url}`);

            while (true) {
                cur_url = window.wk_sklib_url.replace("{id}", id).replace("{index}", `${i}`);
                yield [i, cur_url];
                console.log("wk: target:", cur_url);
                i++;
            }
        } else {
            while (true) {
                cur_url = `${base}/data/${type}/${id}/${i}?random=null`;
                yield [i, cur_url];
                console.log("wk: target:", cur_url);
                i++;
            }
        }

    }


    async function get_bookmarks() {
        const url = new URL(location.origin);
        const id = utils.get_param("id");
        url.pathname = `/manuscripts/pdf/catalog/pdf/${id}`;
        const resp = await fetch(url.href);
        const data = await resp.json();
        const bookmarks = JSON.parse(data.data).outline;
        return bookmarks;
    }


    async function save_bookmarks() {
        const bookmarks = await get_bookmarks();
        const text = JSON.stringify(bookmarks, null, 2);
        utils.save("bookmarks.json", text, { type: "application/json" });
    }


    function decrpyt_pdf_data(encrypted_b64_data, b64_key) {
        console.info("CryptoJS:", window.CryptoJS);

        const key = CryptoJS.enc.Base64.parse(b64_key);
        const decrypted = CryptoJS.AES.decrypt(encrypted_b64_data, key, {
            mode: CryptoJS.mode.ECB,
            padding: CryptoJS.pad.Pkcs7,
        });
        const decrypted_b64 = CryptoJS.enc.Base64.stringify(decrypted).toString();
        return atob(decrypted_b64);
    }

    /**
     * 下载所有pdf文件数据,返回字节串数组
     * @returns {Promise<Array<Uint8Array>>}
     */
    async function fetch_all_pdfs() {
        // 如果已经下载完成,则直接返回之前的结果
        if (window.download_finished) {
            return window.pdfs;
        }

        // 显示进度的按钮
        const prog_btn = utils.btn(3);
        window.download_finished = false;

        // 存储pdf字节串
        const pdfs = [];
        let
            last_digest = NaN,
            size = NaN;

        // 读取每个PDF的页数
        if (window.loadPdfInfo) {
            try {
                const resp = await loadPdfInfo();
                const info = JSON.parse(resp.data);
                size = parseInt(info.size) || size;
            } catch(e) {
                console.error(e);
            }
        }

        for (const [i, url] of pageURLGen()) {
            // 取得数据
            const b64_data = await fetch(url).then(resp => resp.text());
            // 如果获取完毕,则退出
            if (!b64_data.length) break;
            // 计算摘要
            const digest = utils.crc32(b64_data);
            // 如果摘要重复了,说明到达最后一页,退出
            if (digest === last_digest) break;
            // 否则继续
            last_digest = digest;
            const decrypted_b64_data = decrpyt_pdf_data(b64_data, KEY);
            const decrpyt_data = utils.b64_to_bytes(decrypted_b64_data);
            pdfs.push(decrpyt_data);

            // 更新进度
            const progress = `已经获取 ${i + 1} 组页面,每组`
                + (size ? ` ${size} 页` : '页数未知');
            console.info(progress);
            prog_btn.textContent = `${i + 1} 组 / ${size} 页`;
        }

        window.pdfs = pdfs;
        window.download_finished = true;
        return pdfs;
    }


    /**
     * @param {Function} async_fn 
     * @returns {Function}
     */
    function toggle_dl_btn_wrapper(async_fn) {
        return async function(...args) {
            utils.toggle_btn(1);
            utils.toggle_btn(2);
            await async_fn(...args);
            utils.toggle_btn(1);
            utils.toggle_btn(2);
        }
    }


    async function download_pdf$1() {
        alert(
            "如果看不到进度条请使用开发者工具(F12)查看日志\n" +
            "如果文档页数过多可能导致合并PDF失败\n" +
            "此时请使用【下载PDF数据集】按钮"
        );

        const pdfs = await fetch_all_pdfs();
        const combined = await utils.join_pdfs(pdfs);
        utils.save(
            document.title + ".pdf",
            combined,
            "application/pdf"
        );
        utils.btn(3).textContent = "进度条";
    }

    download_pdf$1 = toggle_dl_btn_wrapper(download_pdf$1);


    async function download_data_bundle() {
        alert(
            "下载的是 <文档名称>.dat 数据集\n" +
            "等价于若干 PDF 的文件顺序拼接\n" +
            "请使用工具切割并合并为一份 PDF\n" +
            "工具(pdfs-merger)链接在脚本主页"
        );

        const pdfs = await fetch_all_pdfs();
        const blob = new Blob(pdfs, { type: "application/octet-stream" });
        const url = URL.createObjectURL(blob);
        
        const a = document.createElement("a");
        a.download = document.title + ".dat";
        a.href = url;
        a.click();

        URL.revokeObjectURL(url);
        console.log("pdf数据集", blob);
    }

    download_data_bundle = toggle_dl_btn_wrapper(download_data_bundle);


    function sdlib() {
        const url = new URL(location.href);
        const encrypted_id = url.pathname.split("/")[2];
        window.basePath = `/https/${encrypted_id}${basePath}`;
    }


    /**
     * 钩子函数,启动于主函数生效时,便于不同网站微调
     */
    function load_hooks() {
        const host_to_fn = {
            "gwfw.sdlib.com": sdlib,
        };
        const fn = host_to_fn[location.hostname];
        if (fn) {
            // 如果存在对应 hook 函数,则调用,否则忽略
            fn();
        }
    }


    /**
     * 中国社会科学文库文档策略
     */
    function sklib() {
        // 如果存在 pdf iframe 则在 iframe 中调用自身
        const iframe = wk$("iframe#pdfContent")[0];
        if (iframe) return;
        
        // 加载钩子,方便适应不同网站
        load_hooks();

        // 创建按钮区
        utils.create_btns();
        // 设置功能
        utils.onclick(download_pdf$1, 1, "下载PDF");
        utils.onclick(download_data_bundle, 2, "下载PDF数据集");
        utils.onclick(() => false, 3, "进度条");
        utils.onclick(save_bookmarks, 4, "下载书签");
        // 显示按钮
        utils.toggle_btn(2);
        utils.toggle_btn(3);
        utils.toggle_btn(4);
        // 设置按钮样式
        utils.btn(3).style.pointerEvents = "none";
    }

    /**
     * 返回基础图片地址,接上 <页码>.gif 即为完整URL
     * @returns {string}
     */
    function getBaseURL() {
        const
            elem = wk$("#page_1 img")[0],
            src = elem.src;

        if (!src) {
            alert("当前页面不能解析!");
            return;
        }
        if (!src.endsWith("1.gif")) {
            alert("当前文档不能解析!");
            throw new Error("第一页图片不以 1.gif 结尾");
        }
        return src.slice(0, -5);
    }


    function* imgURLGen() {
        const
            base = getBaseURL(),
            max = parseInt(
                // ' / 23 '
                wk$(".counts")[0].textContent.split("/")[1]
            );

        for (const i of utils.range(1, max + 1)) {
            yield `${base}${i}.gif`;
        }
    }


    function getURLs() {
        utils.save_urls(
            imgURLGen()
        );
    }


    function jinchutou() {
        utils.create_btns();
        utils.onclick(
            getURLs, 1, "导出图片链接"
        );
    }

    // http://www.nrsis.org.cn/mnr_kfs/file/read/55806d6159b7d8e19e633f05fa62fefa


    function get_pdfs() {
        // 34
        const size = window?.Page.size;
        if (!size) utils.raise("无法确定总页码");

        // '/mnr_kfs/file/readPage'
        const path = window
            ?.loadPdf
            .toString()
            .match(/url:'(.+?)',/)[1];
        if (!path) utils.raise("无法确定PDF路径");

        const code = location.pathname.split("/").at(-1);

        const tasks = [...utils.range(1, size + 1)].map(
            async i => {
                const resp = await fetch(path + "?wk=true", {
                    "headers": {
                        "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
                    },
                    "body": `code=${code}&page=${i}`,
                    "method": "POST",
                });

                if (!resp.ok) utils.raise(`第 ${i} 页获取失败!`);
                utils.update_popup(`已经获取第 ${i} 页`);

                const b64_str = await resp.text();
                return utils.b64_to_bytes(b64_str);
            }
        );
        return utils.gather(tasks);
    }


    function get_title() {
        return document.title.slice(0, -5);
    }


    function download_pdf() {
        utils.run_with_prog(1, async () => {
            const pdfs = await get_pdfs();
            debugger;
            const pdf = await utils.join_pdfs(pdfs);
            utils.save(
                get_title(), pdf, "application/pdf"
            );
        });
    }


    function add_style() {
        const style = `
    <style>
        #nprogress .nprogress-spinner-icon.forbidden {
            border-top-color: #b171ff;
            border-left-color: #bf8aff;
            animation: nprogress-spinner 2.4s linear infinite;
        }
    </style>
    `;
        document.body.insertAdjacentHTML(
            "beforeend", style
        );
    }


    function init_forbid_origin_pdf_fetch() {
        console.log("hooked xhr.open");

        // 修改转圈图标
        wk$(".nprogress-spinner-icon")[0]
            .classList.add("forbidden");

        const open = XMLHttpRequest.prototype.open;

        // 重写 XMLHttpRequest.prototype.open 方法
        XMLHttpRequest.prototype.open = function() {
            const args = Array.from(arguments);
            const url = args[1];

            if (!(url.includes("readPage") &&
                !url.includes("wk=true")
            )) return;
            
            this.send = () => undefined;
            open.apply(this, args);
        };

        return function regain_open() {
            const url = new URL(location.href);
            url.searchParams.set("intercept", "0");
            location.assign(url.toString());
        }
    }


    /**
     * nrsis 文档策略
     */
    function nrsis() {
        utils.create_btns();
        utils.onclick(download_pdf, 1, "下载PDF");
        
        if (!utils.get_param("intercept")) {
            add_style();
            const regain_open = init_forbid_origin_pdf_fetch();
            utils.onclick(regain_open, 2, "恢复页面加载");
            utils.toggle_btn(2);
        }
    }

    // ==UserScript==
    // @name         先晓书院PDF下载
    // @namespace    http://tampermonkey.net/
    // @version      0.1
    // @description  先晓书院PDF下载,仅对PDF预览有效
    // @author       2690874578@qq.com
    // @match        https://xianxiao.ssap.com.cn/index/rpdf/read/id/*/catalog_id/0.html?file=*
    // @require      https://greasyfork.org/scripts/445312-wk-full-cli/code/wk-full-cli.user.js
    // @icon         https://www.google.com/s2/favicons?sz=64&domain=xianxiao.ssap.com.cn
    // @grant        none
    // @run-at       document-idle
    // @license      GPL-3.0-only
    // ==/UserScript==



    /**
     * @param {number} begin 
     * @param {number} end 
     * @param {() => void} onload 
     * @returns {Promise<ArrayBuffer>}
     */
    async function fetch_file_chunk(url, begin, end, onload) {
        const resp = await fetch(url, {
            headers: { "Range": `bytes=${begin}-${end}` }
        });
        const buffer = await resp.arrayBuffer();
        onload();
        return buffer;
    }


    /**
     * 取得文档 ID
     * @returns {string}
     */
    function make_pdf_url() {
        const get_value = (key) => {
            const regex = new RegExp(`(?<=${key}=)[0-9]+`);
            return location.search.match(regex)[0];
        };
        const id = get_value("id");
        const catalog_id = get_value("catalog_id");
        return `${location.origin}/rpdf/pdf/id/${id}/catalog_id/${catalog_id}.pdf`;
    }


    /**
     * @param {string} url 
     * @returns {Promise<number>}
     */
    async function get_file_size(url) {
        const resp = await fetch(url, {
            headers: { "Range": `bytes=0-1` }
        });
        const size_text = resp.headers.get("content-range").split("/")[1];
        return parseInt(size_text);
    }


    /**
     * @param {PointerEvent} event 
     */
    async function export_pdf(event) {
        const btn = event.target;

        // 准备请求
        const url = make_pdf_url();
        const size = await get_file_size(url);
        const chunk = 65536;
        const times = Math.floor(size / chunk);
        
        // 准备进度条
        let finished = 0;
        const update_progress = () => {
            finished++;
            const loaded = ((finished * chunk) / 1024 / 1024).toFixed(2);
            const text = `已下载 ${loaded} MB`;
            utils.print(`chunk<${finished}>:`, text);
            btn.textContent = text;
        };

        // 分片请求PDF
        const tasks = [];
        for (let i = 0; i < times; i++) {
            tasks[i] = fetch_file_chunk(
                url,
                i * chunk,
                (i + 1) * chunk - 1,
                update_progress,
            );
        }

        // 请求最后一片
        const tail = size % chunk;
        tasks[times] = fetch_file_chunk(
            url,
            size - tail,
            size - 1,
            update_progress,
        );

        // 等待下载完成
        const buffers = await utils.gather(tasks);
        utils.print("--------全部下载完成--------");
        utils.print("全部数据分片:", { get data() { return buffers; } });

        // 导出PDF
        const blob = new Blob(buffers);
        const fname = top.document.title.split("_")[0] + ".pdf";
        utils.save(fname, blob, "application/pdf");
    }


    /**
     * 先晓书院 文档策略
     */
    function xianxiao() {
        utils.print("进入<先晓书院PDF下载>脚本");
        utils.create_btns();
        utils.onclick(export_pdf, 1, "下载PDF");
    }

    function hook_log() {
        // 保证 console.log 可用性
        const con = window.console;
        const { log, info, warn, error } = con;

        // 对于 console.log 能 hook 则 hook
        if (Object.getOwnPropertyDescriptor(window, "console").configurable
        && Object.getOwnPropertyDescriptor(con, "log").configurable) {
            // 保证 console 不能被改写
            Object.defineProperty(window, "console", {
                get: function() { return con; },
                set: function(value) {
                    log.call(con, "window.console 想改成", value, "?没门!");
                },
                enumerable: false,
                configurable: false,
            });

            // 保证日志函数不被改写
            const fn_map = { log, info, warn, error };
            Object.getOwnPropertyNames(fn_map).forEach((prop) => {
                Object.defineProperty(con, prop, {
                    get: function() { return fn_map[prop]; },
                    set: function(value) {
                        log.call(con, `console.${prop} 想改成`, value, "?没门!");
                    },
                    enumerable: false,
                    configurable: false,
                });
            });
        }
    }


    /**
     * 主函数:识别网站,执行对应文档下载策略
     */
    function main(host=null) {
        // 绑定函数到全局
        window.wk_main = main;

        // 显示当前位置
        host = host || location.hostname;
        const url = new URL(location.href);
        const params = url.searchParams;
        const path = url.pathname;

        hook_log();    
        console.log(`当前 host: ${host}\n当前 url: ${url.href}`);

        if (host.includes("docin.com")) {
            docin();
        } else if (host === "swf.ishare.down.sina.com.cn") {
            if (params.get("wk") === "true") {
                ishareData2();
            } else {
                ishareData();
            }
        } else if (host.includes("ishare.iask")) {
            ishare();
        } else if (host === "www.deliwenku.com") {
            deliwenku();
        } else if (host.includes("file") && host.includes("deliwenku.com")) {
            deliFile();
        } else if (host === "www.doc88.com") {
            doc88();
        } else if (host === "www.360doc.com") {
            doc360();
        } else if (host === "doc.mbalib.com") {
            mbalib();
        } else if (host === "www.dugen.com") {
            dugen();
        } else if (host === "c.gb688.cn") {
            gb688();
        } else if (host === "www.safewk.com") {
            safewk();
        } else if (host.includes("book118.com")) {
            book118();
        } else if (host === "www.renrendoc.com") {
            renrendoc();
        } else if (host.includes("yunzhan365.com")) {
            yunzhan365();
        } else if (host === "wenku.so.com") {
            wenku360();
        } else if (host === "jg.class.com.cn") {
            jg();
        } else if (host === "preview.imm.aliyuncs.com") {
            jgPreview();
        } else if (host === "www.wenkub.com") {
            wenkub();
        } else if (
            (host.includes("sklib") && path === "/manuscripts/")
            || host === "gwfw.sdlib.com") {
            sklib();
        } else if (host === "www.jinchutou.com") {
            jinchutou();
        } else if (host === "www.nrsis.org.cn") {
            nrsis();
        } else if (host === "xianxiao.ssap.com.cn") {
            xianxiao();
        } else {
            console.log("匹配到了无效网页");
        }
    }


    setTimeout(main, 1000);

})();