Wenku Doc Downloader

下载文档,导出纯图片PDF。有限地支持(1)豆丁网 (2)道客巴巴 (3)360个人图书馆(4)得力文库 (5)MBA智库(6)爱问文库(7)原创力文档(8)读根网(9)国标网(10)食典通(11)安全文库网(12)人人文库(13)云展网。在网页左侧中间有按钮区和小猴子图标,说明脚本生效了。【反馈请提供网址】。不支持手机端。你能预览多少页,就可以导出多少页的PDF。

当前为 2023-03-03 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Wenku Doc Downloader
// @namespace    http://tampermonkey.net/
// @version      1.7.7
// @description  下载文档,导出纯图片PDF。有限地支持(1)豆丁网 (2)道客巴巴 (3)360个人图书馆(4)得力文库 (5)MBA智库(6)爱问文库(7)原创力文档(8)读根网(9)国标网(10)食典通(11)安全文库网(12)人人文库(13)云展网。在网页左侧中间有按钮区和小猴子图标,说明脚本生效了。【反馈请提供网址】。不支持手机端。你能预览多少页,就可以导出多少页的PDF。
// @author       [email protected]
// @match        *://*.docin.com/p-*
// @match        *://ishare.iask.sina.com.cn/f/*
// @match        *://ishare.iask.com/f/*
// @match        *://swf.ishare.down.sina.com.cn/?path=*
// @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.yunzhan365.com/basic/*
// @match        *://book.yunzhan365.com/*index.html*
// @match        *://www.bing.com/search?q=Bing+AI&showconv=1*
// @require      https://cdn.staticfile.org/jspdf/2.5.1/jspdf.umd.min.js
// @require      https://cdn.staticfile.org/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
// @license      GPL-3.0-only
// @create       2021-11-22
// @note         1. 优化 Bing AI 对话保存功能
// @note         2. 暂不支持原创力 PPT 文档
// ==/UserScript==


(function () {
    'use strict';

    /**
     * 元素选择器
     * @param {string | HTMLElement} selector 选择器或元素
     * @returns {Array<HTMLElement>} 元素列表
     */
    function _$(selector) {
        if (selector instanceof HTMLElement) {
            return [selector];
        }
        let self = this?.querySelectorAll ? this : document;
        return [...self.querySelectorAll(selector)];
    }
    globalThis.wk$ = _$;

    let utils = {
        global: globalThis,
        update: "2023-03-03",

        /**
         * 函数装饰器:仅执行一次 func
         * @param {Function} func 
         * @returns {Promise<Function>}
         */
        once: async function(func) {
            return async function() {
                let used = false;
                if (!used) {
                    await func();
                    used = true;
                }
            }
        },

        /**
         * 将类似于dict的简单object转为get请求的queryString
         * @param {Object} dict 
         * @returns {string}
         */
        dictToQueryStr: function(dict) {
            let params = [];
            for (let prop in dict) {
                params.push(`${prop}=${dict[prop]}`);
            }
            return params.join("&");
        },

        /**
         * 返回一个包含计数器的迭代器, 其每次迭代值为 [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) {
            // 用于取得一次列表中所有迭代器的值
            function _getAllValus(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];
            }
            
            // 强制转为迭代器
            let iterators = iterables.map(
                iterable => iterable[Symbol.iterator]()
            );

            // 逐次迭代
            while (true) {
                let [done, values] = _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");
            }
        
            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
         */
        getAllStyles: 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");
        },

        /**
         * 测试数组是否包含另一数组
         * @param {Array} arr 
         * @param {Array} sub_arr 
         * @returns {boolean} 包含与否
         */
        isSubArrIn: function(arr, sub_arr) {
            for (let i = 0, len = arr.length; i < len; i++) {
                let matched = true;
        
                for (let [j, val] of sub_arr.entries()) {
                    if (val !== arr[i + j]) {
                        matched = false;
                        break;
                    }
                }
        
                if (matched) {
                    return true;
                }
            }
            return false;
        },
          
        /**
         * 使用过时的execCommand复制文字
         * @param {string} text
         */
        _oldCopy: function(text) {
            let input = document.createElement("input");
            input.value = text;
            document.body.appendChild(input);
            input.select();
            document.execCommand("copy");
            input.remove();
        },

        /**
         * 复制text到剪贴板
         * @param {string} text 
         * @returns 
         */
        copy: function(text) {
            // 输出到控制台和剪贴板
            console.log(text);
            
            if (!navigator.clipboard) {
                this._oldCopy(text);
                return;
            }
            navigator.clipboard.writeText(text)
            .catch(_ => this._oldCopy(text));
        },

        /**
         * 装饰器, 用于打印函数执行耗时
         * @param {Function} func 需要计时的函数
         * @returns {Promise<Function>} 装饰的func => func的返回值
         */
        recTime: async function(func) {
            async function inner() {
                let begin = Date.now();
                let res = await func();
                let cost = ((Date.now() - begin) / 1000).toFixed(1);
                console.log(`Function <${func.name}> costed ${cost} seconds.`);
                return res;
            }
            return inner;
        },

        /**
         * 创建并下载文件
         * @param {string} file_name 文件名
         * @param {ArrayBuffer | ArrayBufferView | Blob | string} content blob_part
         */
        saveAs: function(file_name, content) {
            let a = document.createElement("a");
            let blob = new Blob([content]);
            a.download = file_name;
            let url = URL.createObjectURL(blob);
            a.href = url;
            a.click();
            URL.revokeObjectURL(url);
        },

        /**
         * canvas转为PNG格式的blob
         * @param {HTMLCanvasElement} canvas 
         * @returns {Promise<Blob>} blob
         */
        canvasToBlob: async function(canvas) {
            return new Promise(res => canvas.toBlob(res));
        },

        /**
         * 显示/隐藏按钮区.
         * @param {Function} func
         */
        toggleBtnsSec: 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;
            let _delay = delay + (max_delay - delay) * Math.random();
            return new Promise(resolve => setTimeout(resolve, _delay));
        },

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

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

        /**
         * 在指定节点后面插入节点
         * @param {HTMLElement} new_element 
         * @param {HTMLElement} target_element 
         */
        insertAfter: function(new_element, target_element) {
            let parent = target_element.parentNode;
            if (parent.lastChild === target_element) {
                parent.appendChild(new_element);
            } else {
                parent.insertBefore(new_element, target_element.nextElementSibling);
            }
        },

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

        /**
         * 抛出set中的第一个元素
         * @param {Set} set 
         * @returns 一个元素
         */
        setPop: function(set) {
            for (let item of set) {
                set.delete(item);
                return item;
            }
        },

        /**
         * 增强按钮(默认为蓝色按钮:展开文档)的点击效果
         * @param {string} custom_btn 按钮变量名
         */
        enhanceBtnClick: function(custom_btn = null) {
            let aim_btn;
            // 如果不使用自定义按钮元素,则默认为使用蓝色展开文档按钮
            if (!custom_btn || custom_btn === "btn_1") {
                aim_btn = document.querySelector(".btn-1");
            } else {
                aim_btn = document.querySelector(`.${custom_btn.replace("_", "-")}`);
            }

            let old_color = aim_btn.style.color; // 保存旧的颜色
            let old_text = aim_btn.textContent; // 保存旧的文字内容
            // 变黑缩小
            aim_btn.style.color = "black";
            aim_btn.style.fontWeight = "normal";
            aim_btn.textContent = `->${old_text}<-`;
            // 复原加粗
            let changeColorBack = function() {
                aim_btn.style.color = old_color;
                aim_btn.style.fontWeight = "bold";
                aim_btn.textContent = old_text;
            };
            setTimeout(changeColorBack, 1250);
        },

        /**
         * 绑定事件到指定按钮,返回按钮引用
         * @param {Function} listener click监听器
         * @param {string} aim_btn 按钮的变量名
         * @param {string} new_text 按钮的新文本,为null则不替换
         * @returns 按钮元素的引用
         */
        setBtnListener: function(listener, aim_btn, new_text=null) {
            let btn = wk$(`.${aim_btn.replace("_", "-")}`)[0];
            // 如果需要,替换按钮内文本
            if (new_text) {
                btn.textContent = new_text;
            }
            // 绑定事件,添加到页面上
            btn.addEventListener("click", () => {
                this.enhanceBtnClick(aim_btn);
                listener();
            });
            return btn;
        },

        /**
         * 强制隐藏元素
         * @param {string} selector 
         */
        forceHide: function(selector) {
            let cls = "force-hide";
            document.querySelectorAll(selector).forEach((elem) => {
                elem.className += ` ${cls}`;
            });
            // 判断css样式是否已经存在
            let style;
            style = document.querySelector(`style.${cls}`);
            // 如果已经存在,则无须重复创建
            if (style) {
                return;
            }
            // 否则创建
            style = document.createElement("style");
            style.innerHTML = `style.${cls} {
            visibility: hidden !important;
        }`;
            document.head.appendChild(style);
        },

        /**
         * 当元素可见时,操作目标元素(异步)。最多为不可见元素等待5秒。
         * @param {HTMLElement} elem 一个元素
         * @param {Function} callback (elem) => {...} 元素操作函数
         */
        manipulateElem: async function(elem, callback) {
            let isVisiable = () => getComputedStyle(elem).display !== "none";

            let max = 5 * 5; // 最多等待5秒
            let i = 0;

            // 如果不可见就等待0.2秒/轮
            while (!isVisiable() && i <= max) {
                i++;
                await utils.sleep(200);
            }

            callback(elem);
        },

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

        /**
         * 隐藏按钮,打印页面,显示按钮
         */
        hideBtnThenPrint: function() {
            // 隐藏按钮,然后打印页面
            let btns = document.querySelectorAll(".btns_section, .hide_btn_wk");
            btns.forEach(elem => elem.style.display = "none");
            window.print();

            // 打印结束,显示按钮
            btns.forEach(elem => elem.style.display = "block");
        },

        /**
         * 切换按钮显示/隐藏状态
         * @param {string} aim_btn 按钮变量名
         * @returns 按钮元素的引用
         */
        toggleBtn: function(aim_btn) {
            let btn = document.querySelector(`.${aim_btn.replace("_", "-")}`);
            let display = getComputedStyle(btn).display;
            // return;
            if (display === "none") {
                btn.style.display = "block";
            } else {
                btn.style.display = "none";
            }
            return btn;
        },

        /**
         * 用input框跳转到对应页码
         * @param {Element} cur_page 当前页码
         * @param {string | Number} aim_page 目标页码
         * @param {string} event_type 键盘事件类型:"keyup" | "keypress" | "keydown"
         */
        toPageNo: function(cur_page, aim_page, event_type) {
            // 设置跳转页码为目标页码
            cur_page.value = (aim_page).toString();
            // 模拟回车事件来跳转
            let keyboard_event_enter = new KeyboardEvent(event_type, {
                bubbles: true,
                cancelable: true,
                keyCode: 13
            });
            cur_page.dispatchEvent(keyboard_event_enter);
        },

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

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

        // /**
        //  * 滚动到页面底部
        //  */
        // scrollToBottom: function() {
        //     window.scrollTo({
        //         top: document.body.scrollHeight,
        //         behavior: "smooth"
        //     });
        // },

        /**
         * 用try移除元素
         * @param {Element} element 要移除的元素
         */
        tryToRemoveElement: function(element) {
            try {
                element.remove();
            } catch (e) {
            }
        },

        /**
         * 用try移除若干元素
         * @param {Element[]} elements 要移除的元素列表
         */
        tryToRemoveElements: function(elements) {
            elements.forEach((elem) => {
                this.tryToRemoveElement(elem);
            });
        },

        /**
         * 用try移除 [元素列表1, 元素列表2, ...] 中的元素
         * @param {Array} elem_list_box 要移除的元素列表构成的列表
         */
        tryToRemoveSameElem: function(elem_list_box) {
            for (let elem_list of elem_list_box) {
                if (!elem_list) {
                    continue;
                }
                for (let elem of elem_list) {
                    try {
                        elem.remove();
                    } catch (e) {
                        console.log();
                    }
                }
            }
        },

        /**
         * 使文档在页面上居中
         * @param {string} selector 文档容器的css选择器
         * @param {string} default_offset 文档部分向右偏移的百分比(0-59)
         * @returns 偏移值是否合法
         */
        centerDoc: function(selector, default_offset) {
            let doc_main = document.querySelector(selector);
            let offset = window.prompt("请输入偏移百分位:", default_offset);
            // 如果输入的数字不在 0-59 内,提醒用户重新设置
            if (offset.length === 1 && offset.search(/[0-9]/) !== -1) {
                doc_main.style.marginLeft = offset + "%";
                return true;
            } else if (offset.length === 2 && offset.search(/[1-5][0-9]/) !== -1) {
                doc_main.style.marginLeft = offset + "%";
                return true
            } else {
                alert("请输入一个正整数,范围在0至59之间,用来使文档居中\n(不同文档偏移量不同,所以需要手动调整)");
                return false;
            }
        },

        /**
         * 调整按钮内文本
         * @param {string} aim_btn 按钮变量名
         * @param {string} new_text 新的文本,null则保留旧文本
         * @param {Boolean} recommend_btn 是否增加"(推荐)"到按钮文本
         * @param {Boolean} use_hint 是否提示"文档已经完全展开,可以导出"
         */
        modifyBtnText: function(aim_btn = "btn_2", new_text = null, recommend_btn = false, use_hint = true) {
            // 提示文档已经展开
            if (use_hint) {
                let hint = "文档已经完全展开,可以导出";
                alert(hint);
            }
            let btn = document.querySelector(`.${aim_btn.replace("_", "-")}`);
            // 要替换的文本
            if (new_text) {
                btn.textContent = new_text;
            }
            // 推荐按钮
            if (recommend_btn) {
                btn.textContent += "(推荐)";
            }
        },

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

            // 如果是空列表或内容不为html元素, 则抛出异常
            if (elements.length === 0
                || !(elements[0] instanceof HTMLElement)) {
                throw new Error("htmlToCanvases 未得到任何html元素");
            }

            let tasks = [];
            for (let elem of elements) {
                tasks.push(html2canvas(elem));
            }
            // 等待全部page转化完成
            return await Promise.all(tasks);
        },

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

            // 拿到canvas宽、高
            let
                model = canvases[0],
                width = model.width,
                height = model.height;
            // 打包为pdf
            this.canvasesToPDF(canvases, title, width, height);
            // });
        },

        /**
         * 加载CDN脚本
         * @param {string} url 
         */
        loadWebScript: async function(url) {
            let resp = await fetch(url);
            Function(await resp.text())();
        },

        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;
        },

        /**
         * b64编码字符串转Uint8Array
         * @param {string} sBase64 b64编码的字符串
         * @param {number} nBlockSize 字节数
         * @returns {Uint8Array} arr
         */
        base64DecToArr: function(sBase64, nBlockSize=1) {
            let
                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 |= this.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 图像类型,默认为png
         * @param {number} canvas 品质,默认为1(最小0)
         * @returns {Promise<Blob>}
         */
        canvasToBlob: function(canvas, type="image/png", quality=1) {
            return new Promise(resolve => canvas.toBlob(resolve, type, quality));
        },

        /**
         * 存储所有canvas图形为png到一个压缩包
         * @param {Iterable<HTMLCanvasElement>} canvases canvas元素列表
         * @param {string} title 文档标题
         */
        canvasesToZip: async function(canvases, title) {
            // canvas元素转为png图像
            // 所有png合并为一个zip压缩包
            let zip = new JSZip();
            let tasks = [];

            for (let canvas of canvases) {
                tasks.push(this.canvasToBlob(canvas));
            }
            let blobs = await Promise.all(tasks);
            blobs.forEach(
                (blob, i) => zip.file(`page-${i+1}.png`, blob, { binary: true })
            );
            
            // 导出zip
            let zip_blob = await zip.generateAsync({ type: "blob" });
            console.log(zip_blob);
            // saveAs(content, `${title}.zip`);
            this.saveAs(`${title}.zip`, zip_blob);
        },

        /**
         * 将canvas转为png,然后导出PDF
         * @param {Iterable<HTMLCanvasElement>} canvas_box canvas元素列表
         * @param {string} title 文档标题
         */
        canvasesToPDF: function(canvas_box, title, width = 0, height = 0) {
            // 如果没有手动指定canvas的长宽,则自动检测
            if (!width && !height) {
                // 先获取第一个canvas用于判断竖向还是横向,以及得到页面长宽
                let first_canvas = canvas_box[0];

                if (first_canvas.width && parseInt(first_canvas.width) && parseInt(first_canvas.height)) {
                    [width, height] = [first_canvas.width, first_canvas.height];
                } else {
                    let [width_str, height_str] = [first_canvas.style.width.replace(/(px)|(rem)|(em)/, ""), first_canvas.style.height.replace(/(px)|(rem)|(em)/, "")];
                    [width, height] = [parseInt(width_str), parseInt(height_str)];
                }
            }

            console.log(`canvas数据:宽: ${width}px,高: ${height}px`);
            // 如果文档第一页的宽比长更大,则landscape,否则portrait
            let orientation = width > height ? 'l' : 'p';
            // jsPDF的第三个参数为format,当自定义时,参数为数字数组。
            let pdf = new jspdf.jsPDF(orientation, 'px', [height, width]);

            // 保存每一页文档到每一页pdf
            let canvas_list = Array.from(canvas_box);
            let last_canvas = canvas_list.pop();
            canvas_list.forEach(canvas => {
                pdf.addImage(canvas, 'png', 0, 0, width, height);
                pdf.addPage();
            });
            // 添加尾页
            pdf.addImage(last_canvas, 'png', 0, 0, width, height);
            // 导出文件
            pdf.save(`${title}.pdf`);
        },

        /**
         * Image元素列表合并到一个PDF中
         * @param {NodeList} imgs Image元素列表
         * @param {string} title 文档名
         */
        imgsToPDF: function(imgs, title) {
            // 取得宽高
            let model = imgs[0];
            let width = model.offsetWidth;
            let height = model.offsetHeight;

            // 创建pdf
            let orientation = width > height ? 'l' : 'p';
            let pdf = new jspdf.jsPDF(orientation, 'px', [height, width]);

            // 添加图像到pdf
            imgs.forEach((img, index) => {
                pdf.addImage(img, 'PNG', 0, 0, width, height);
                // 如果当前不是文档最后一页,则需要添加下一个空白页
                if (index !== imgs.length - 1) {
                    pdf.addPage();
                }
            });

            // 导出文件
            pdf.save(`${title}.pdf`);
        },


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

        getUrlAsBlob: async function(url) {
            return (await fetch(url)).blob();
        },

        /**
         * 导出图片链接
         * @param {Iterable<string>} urls
         */
        saveImgUrls: function(urls) {
            this.saveAs(
                "urls.csv",
                Array.from(urls).join("\n")
            );
        },

        /**
         * 图片blobs合并并导出为单个PDF
         * @param {Array<Blob>} blobs 
         * @param {string} title 文档名称, 不含后缀, 默认为"文档"
         * @param {boolean} filter 是否过滤 type 不以 "image/" 开头的 blob; 默认为 true
         */
        imgBlobsToPDF: async function(blobs, title="文档", filter=true) {
            // 格式转换:img blob -> bmp
            let tasks = blobs;
            if (filter) {
                tasks = blobs.filter(
                    blob => blob.type.startsWith("image/")
                );
            }
            tasks = tasks.map(
                blob => createImageBitmap(blob)
            );
            let bmps =  await Promise.all(tasks);

            // bmp -> canvas
            let canvases = bmps.map(
                bmp => this.bmpToCanvas(bmp)
            );

            // 导出PDF
            this.canvasesToPDF(canvases, title);
        },

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

            let tasks, img_blobs, i = 3;
            // 根据请求成功数量判断是否循环
            do {
                i -= 1;
                // 发起请求
                tasks = [this.getUrlAsBlob(first)];  // 初始化时加入第一个
                // 然后加入剩余的
                for (let url of urls) {
                    tasks.push(this.getUrlAsBlob(url));
                }
                
                // 接收响应
                let blobs = await Promise.all(tasks);
                img_blobs = blobs.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(2);
                } else {
                    // 结束循环
                    break;
                }
            } while (true)
            
            this.imgBlobsToPDF(img_blobs, title, false);
        },

        /**
         * 返回子串个数
         * @param {string} str 
         * @param {string} sub 
         */
        countSubStr: function(str, sub) {
            let i = 0;
            let counter = 0;

            while (true) {
                i = str.indexOf(sub, i);
                if (i === -1) {
                    return counter;
                } else {
                    i++;
                    counter++;
                }
            }
        },

        /**
         * 创建5个按钮:展开文档、导出图片、导出PDF、未设定4、未设定5;除第1个外默认均为隐藏
         */
        createBtns: function() {
            // 创建大容器
            let box = document.createElement("div");
            box.className = "wk-box";
            document.body.appendChild(box);

            // 创建按钮组
            let section = document.createElement("section");
            section.className = "btns_section";
            section.innerHTML = `
            <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>
        `;
            box.appendChild(section);

            // 添加隐藏/展示按钮
            // 隐藏【🙈】,展开【🐵】
            let hide_btn = document.createElement("p");
            hide_btn.className = "hide_btn_wk";
            hide_btn.textContent = "🐵";
            hide_btn.onclick = () => {
                // 显示 -> 隐藏
                if (getComputedStyle(section).display === "block") {
                    section.style.display = "none";
                    hide_btn.style.left = "20px";
                    hide_btn.textContent = "🙈";
                    // 隐藏 -> 显示
                } else {
                    section.style.display = "block";
                    hide_btn.style.left = "155px";
                    hide_btn.textContent = "🐵";
                }
            };
            box.appendChild(hide_btn);

            // 设定样式
            let style = document.createElement("style");
            style.innerHTML = `
            .hide_btn_wk {
                position: fixed;
                left: 155px;
                top: 36%;
                user-select: none;
                font-size: large;
                z-index: 2000;
            }
            .btns_section{
                position: fixed;
                width: 154px;                
                left: 10px;
                top: 32%;
                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';
                z-index: 1000;
            }
            .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;
            }
            .btn-1:hover,.btn-2:hover,.btn-3:hover,.btn-4,.btn-5:hover{ opacity: .8;}
            .btn-1:active,.btn-2:active,.btn-3:active,.btn-4,.btn-5:active{ opacity: 1;}`;
            document.body.appendChild(style);
        },

        /**
         * 添加弹窗到 body, 通过 utils.toID("wk-popup") 激发
         */
        addPopup: function() {
            let container = document.createElement("div");
            container.className = "wk-popup-container";
            container.innerHTML = `
            <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>
            <style>.wk-popup-container{height:100vh;width:100vw;position:fixed;top:0;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:40px;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{align-items:center;background:rgba(0,0,0,.7);bottom:0;display:flex;justify-content:center;left:0;position:fixed;right:0;top:0}#wk-popup{opacity:0;transition:opacity .25s ease-in-out;visibility:hidden}#wk-popup:target{opacity:1;visibility:visible}#wk-popup:target .modal-body{opacity:1;transform:translateY(1)}#wk-popup .modal-body{max-width:500px;opacity:0;transform:translateY(-100px);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>;`;
            document.body.appendChild(container);
            },

        /**
         * 设置弹窗标题
         * @param {string} text 
         */
        setPopupHead: function(text) {
            wk$(".wk-popup-head")[0].textContent = text;
        },

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

        /**
         * 移除弹窗
         */
        removePopup: function() {
            try {
                wk$(".wk-popup-container")[0].remove();
            } catch(e) {
                console.log(e);
            }
        },

        /**
         * 滚动页面到id位置的元素处
         * @param {string} id 
         */
        toID: function(id) {
            let a = document.createElement("a");
            a.href = "#" + id;
            a.click();
        }
    };

    globalThis.wkutils = utils;


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


    /**
     * 确保引用外部依赖的函数都在调用前加载了依赖
     */
    for (let prop of Object.keys(utils)) {
        // 跳过非函数
        if (!(typeof utils[prop] === "function")) {
            continue;
        }
        
        // 绑定this到utils
        utils[prop] = utils[prop].bind(utils);

        // 为有外部依赖的函数做包装
        let obj, url;
        let name = prop.toLowerCase();
        if (name.includes("tozip")) {
            obj = "JSZip";
            url = "https://cdn.staticfile.org/jszip/3.7.1/jszip.min.js";
        } else if (name.includes("topdf")) {
            obj = "jspdf";
            url = "https://cdn.staticfile.org/jspdf/2.5.1/jspdf.umd.min.js";
        // } else if (name.includes("tocanvas")) {
        //     obj = "html2canvas";
        //     url = "https://cdn.staticfile.org/html2canvas/1.4.1/html2canvas.min.js";
        } else {
            continue;
        }
        utils[prop] = ensureWebScript(obj, url, utils[prop]);
    }


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

        for (let _ of utils.range(10)) {
            let elems = selectAll();
            if (elems.length > 0) {
                return elems;
            }
            await utils.sleep(500);
        }
        throw Error(`"${selector}" not found in 5 seconds`);
    }

    globalThis.wk$$ = _$$;
    console.log("wk: `wkutils` 已经挂载到全局");

    /**
     * 展开道客巴巴的文档
     */
    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.toPageNo(cur_page, page_max, "keypress");
            // 返回顶部
            await utils.sleep(1000);
            utils.toPageNo(cur_page, "1", "keypress");
        }
        // 文档展开后,显示按钮2、3
        else {
            // 隐藏按钮
            utils.toggleBtn("btn_1");
            // 显示按钮
            utils.toggleBtn("btn_2");
            utils.toggleBtn("btn_3");
            utils.toggleBtn("btn_4");
            utils.toggleBtn("btn_5");
        }
    }


    /**
     * 隐藏搜索框
     */
    function hideSearchBox() {
        let elem = wk$("#min-search-result")[0];
        let hide = elem => elem.style.display = "none";
        utils.manipulateElem(elem, hide);
    }


    /**
     * 隐藏复制弹窗
     */
    function hideCopyPopup() {
        let elem = wk$("#ym-window")[0];
        let hide = elem => elem.parentElement.style.display = "none";
        utils.manipulateElem(elem, hide);
    }


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


    /**
     * 初始化任务
     */
    function initService() {
        // 初始化
        console.log("正在执行初始化任务");
        // 1. 隐藏选中文字的提示框
        hideSelectPopup();
        // 2. 隐藏搜索框
        hideSearchBox();
        // 3. 移除vip复制弹窗
        hideCopyPopup();
        // 4. 查找复制文字可能的api名称
        let prop = getCopyAPIValue();

        globalThis.doc88JS._apis = Object
        .getOwnPropertyNames(prop)
        .filter(name => {
            if (!name.startsWith("_")) {
                return false;
            }
            if (prop[name] === "") {
                return true;
            }
        });
    }


    /**
     * 取得 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 
     */
    function copySelected() {
        // 尚未选中文字
        if (getComputedStyle(wk$("#left-menu")[0]).display === "none") {
            console.log("尚未选中文字");
            return;
        }
        
        // // 选中文字,搜索文字,弹出搜索框
        // let search = wk$("#lmenu_search")[0];
        // search.click();
        // // 取得input内容
        // let input = wk$(".min-text input")[0];
        // let text = input.value;
        // // 清空input
        // input.value = "";

        // 输出到控制台和剪贴板
        utils.copy(getSelectedText());
    }


    /**
     * 捕获 ctrl + c 并关闭弹窗
     * @param {KeyboardEvent} keydown 
     * @returns 
     */
    function catchCtrlC(keydown) {
        // 判断是否为 ctrl + c
        if (!(keydown.code === "KeyC" && keydown.ctrlKey === true)) {
            return;
        }

        // 判断触发间隔
        let now = Date.now();

        // 距离上次小于1秒
        if (now - doc88JS.last_copy_time < 1000 * 1) {
            doc88JS.last_copy_time = now;
            return;
        }

        // 大于1秒
        // 刷新最近一次触发时间
        doc88JS.last_copy_time = now;
        // 复制文字
        copySelected();
    }


    /**
     * 随机改变字体颜色、大小、粗细
     * @param {HTMLElement} elem 
     */
    function emphasizeText(elem) {
        let rand = Math.random;
        elem.style = `
        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;
    `;
    }


    /**
     * 浏览并加载所有页面
     */
    async function walkThrough$1() {
        // 文档容器
        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.waitUntil(async() => {
                    let page = wk$(`#page_${i}`)[0];
                    // page无法选中说明有弹窗
                    if (!page) {
                        // 关闭弹窗,等待,然后递归
                        wk$("#ym-window .DOC88Window_close")[0].click();
                        await utils.sleep(500);
                        walkThrough$1();
                        throw new Error("walkThrough 递归完成,终止函数");
                    }
                    // canvas尚未绘制时width=300
                    return page.width !== 300;
                });
                // 凸显页码
                emphasizeText(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.createBtns();

        // 绑定主函数
        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.setBtnListener(readAllDoc88, "btn_1");

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

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

        // btn_5: 复制选中文字
        utils.setBtnListener(() => {
            copySelected();
            utils.modifyBtnText("btn_5", "复制成功!", false, false);
        }, "btn_5", "复制选中文字");

        // 为 ctrl + c 添加响应
        document.addEventListener("keydown", catchCtrlC);

        // 执行一次初始化任务
        await utils.sleep(1000);
        initService();
    }

    // 绑定主函数
    function getCanvasList() {
        // 获取全部canvas元素,用于传递canvas元素列表给 btn_2 和 btn_3
        let parent_node_list = document.querySelectorAll(".hkswf-content");
        let node_list = [];
        for (let node of parent_node_list) {
            node_list.push(node.children[0]);
        }
        return node_list;
    }


    function prepare() {
        // 获取canvas元素列表
        let node_list = getCanvasList();
        // 获取文档标题
        let title;
        if (document.querySelector("h1 [title=doc]")) {
            title = document.querySelector("h1 [title=doc]").nextElementSibling.textContent;
        } else if (document.querySelector(".doc_title")) {
            title = document.querySelector(".doc_title").textContent;
        } else {
            title = "文档";
        }
        return [node_list, title];
    }


    /**
     * 下载全部图片链接,适用性:爱问共享资料、得力文库
     * @param {string} selector 图形元素的父级元素
     */
    function savePicUrls(selector) {
        let pages = document.querySelectorAll(selector);
        let pic_urls = [];

        for (let elem of pages) {
            let pic_obj = elem.children[0];
            let url = pic_obj.src;
            pic_urls.push(url);
        }
        let content = pic_urls.join("\n");
        // 启动下载
        utils.saveAs("urls.csv", content);
    }


    // 判断是否有canvas元素
    function detectCanvas() {
        let haveCanvas = getCanvasList().length === 0 ? false : true;

        // 隐藏按钮
        utils.toggleBtn("btn_1");
        // 显示按钮
        utils.toggleBtn("btn_2");

        // 如果没有canvas元素,则认为文档页面由外链图片构成
        if (!haveCanvas) {
            // btn_2: 导出图片链接
            utils.setBtnListener(() => {
                if (confirm("确定每页内容都加载完成了吗?")) {
                    savePicUrls("[id*=img_]");
                }
            }, "btn_2", "导出全部图片链接");
        } else {
            // 显示按钮3
            utils.toggleBtn("btn_3");
            // btn_2: 导出zip
            utils.setBtnListener(() => {
                if (confirm("确定每页内容都加载完成了吗?")) {
                    utils.canvasesToZip(...prepare());
                }
            }, "btn_2", "导出图片到zip");
            // btn_3: 导出PDF
            utils.setBtnListener(() => {
                if (confirm("确定每页内容都加载完成了吗?")) {
                    utils.canvasesToPDF(...prepare());
                }
            }, "btn_3", "导出图片到PDF");
        }
    }


    /**
     * 豆丁文档下载策略
     */
    function docin() {
        // 创建脚本启动按钮
        utils.createBtns();

        // 隐藏底部工具栏
        document.querySelector("#j_select").click(); // 选择指针
        let tool_bar = document.querySelector(".reader_tools_bar_wrap.tools_bar_small.clear");
        tool_bar.style.display = "none";

        // btn_1: 判断文档类型
        utils.setBtnListener(() => {
            utils.forceHide(".jz_watermark");
            detectCanvas();
        }, "btn_1", "判断文档类型");
    }

    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.createBtns();

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

    /**
     * 返回包含对于数量svg元素的html元素
     * @param {string} data
     * @returns {HTMLDivElement} article
     */
    function _createDiv(data) {
        let num = utils.countSubStr(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.setBtnListener(change, "btn_4", "切换显示模式");

        utils.toggleBtn("btn_2");
        utils.toggleBtn("btn_3");
        utils.toggleBtn("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.createBtns();
        utils.setBtnListener(utils.hideBtnThenPrint, "btn_1", "打印页面到PDF");
        utils.setBtnListener(setGapGUI, "btn_2", "重设页间距");
        utils.setBtnListener(handleSVGtext, "btn_3", "显示空白点我");

        utils.toggleBtn("btn_2");
        utils.toggleBtn("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, 10);
        let sep = head.join() + ",";
        // 切断,重组,格式转换
        // return String.prototype.split.call(data, head).slice(1).map(
        //     val => new Uint8Array((sep + val).split(","))
        // );
        // return utils.splitArray(data, head).slice(1).map(
        //     val => Uint8Array.from([...head, ...val])
        // );
        // 切断,重组,格式转换
        return data.join().split(sep).slice(1).map(
            val => new Uint8Array((sep + val).split(","))
        );
    }


    /**
     * 图像Uint8数组列表合并然后导出PDF
     * @param {string} fname
     * @param {Array<Uint8Array>} img_data_list 
     */
    async function imgDataArrsToPDF(fname, img_data_list) {
        let cover_blob = new Blob([img_data_list[0]]);
        let cover = await createImageBitmap(cover_blob);

        utils.canvasesToPDF(
            img_data_list,
            fname,
            cover.width,
            cover.height
        );
    }


    async function exportPDF$3() {
        let fname = utils.getUrlParam("fname");
        let path = utils.getUrlParam("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} 张图片`);
        await imgDataArrsToPDF(fname, img_data_list);
    }


    function showHints$1() {
        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: "",
            text: "",
            content_1: "",
            content_2: ""
        };

        // 显示提示
        showHints$1();

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

        // btn_1: 识别文档类型 -> 导出PDF
        exportPDF$3 = await utils.recTime(exportPDF$3);
        utils.setBtnListener(exportPDF$3, "btn_1", "下载并导出PDF");
    }

    // /**
    //  * 清理并打印得力文库的文档页
    //  */
    // function printPageDeliwenku() {
    //     // 移除页面上的无关元素
    //     let selector = ".hr-wrap, #readshop, .nav_uis, .bookdesc, #boxright, .QQ_S1, .QQ_S, #outer_page_more, .works-manage-box.shenshu, .works-intro, .mt10.related-pic-box, .mt10.works-comment, .foot_nav, .siteInner";
    //     let elem_list = document.querySelectorAll(selector);
    //     for (let elem of elem_list) {
    //         utils.tryToRemoveElement(elem);
    //     }
    //     // 修改页间距
    //     let outer_pages = document.getElementsByClassName("outer_page");
    //     for (let page of outer_pages) {
    //         page.style.marginBottom = "20px";
    //     }
    //     // 使文档居中
    //     alert("建议使用:\n偏移量: 3\n缩放: 112\n请上下滚动页面,确保每页内容都加载完成以避免空白页\n如果预览时有空白页或文末有绿色按钮,请取消打印重试");
    //     if (!utils.centerDoc("#boxleft", "3")) {
    //         return; // 如果输入非法,终止函数调用
    //     }
    //     // 打印文档
    //     utils.hideBtnThenPrint();
    // }


    // /**
    //  * 点击“继续阅读”,适用性:得力文库
    //  */
    // function readAllDeliwenku() {
    //     // 点击“同意并开始预览全文”
    //     let start_btn = document.getElementsByClassName("pre_button")[0];
    //     let display = start_btn.parentElement.parentElement.style.display;
    //     // 如果该按钮显示着,则点击,然后滚动至页面底部,最后终止函数
    //     if (!display) {
    //         start_btn.children[0].click();
    //         setTimeout(() => {
    //             scroll(0, document.body.scrollHeight);
    //         }, 200);
    //         return;
    //     }
    //     // 增强按钮点击效果
    //     utils.enhanceBtnClickReaction();

    //     let read_all_btn = document.getElementsByClassName("fc2e")[0];
    //     let display2 = read_all_btn.parentElement.parentElement.style.display
    //         // 继续阅读
    //     if (display2 !== "none") {
    //         // 获取input元素
    //         let cur_page = document.querySelector("#pageNumInput");
    //         let page_old = cur_page.value;
    //         let page_max = cur_page.parentElement.nextElementSibling.textContent.replace(" / ", "");
    //         // 跳转到尾页
    //         utils.jump2pageNo(cur_page, page_max, "keydown");
    //         // 跳转回来
    //         utils.jump2pageNo(cur_page, page_old, "keydown");

    //         // 切换按钮准备导出
    //     } else {
    //         // 推荐导出图片链接
    //         utils.modifyBtnText("btn_2", null, true);
    //         // 隐藏按钮
    //         utils.toggleBtnStatus("btn_1");
    //         // 显示按钮
    //         utils.toggleBtnStatus("btn_2");
    //         utils.toggleBtnStatus("btn_3");
    //         // btn_3 橙色按钮
    //         utils.setBtnEvent(printPageDeliwenku, [], "btn_3", "打印页面到PDF");
    //     }
    // }


    // /**
    //  * 得力文库文档下载策略
    //  */
    // function deliwenkuDeprecated() {
    //     // 创建脚本启动按钮1、2
    //     utils.createBtns();

    //     // btn_1: 展开文档
    //     utils.setBtnEvent(readAllDeliwenku, [], "btn_1");
    //     // btn_2: 导出图片链接
    //     utils.setBtnEvent(() => {
    //         if (confirm("确定每页内容都加载完成了吗?")) {
    //             utils.savePicUrls('.inner_page div');
    //         }
    //     }, [], "btn_2", "导出图片链接");

    //     // 尝试关闭页面弹窗
    //     try { document.querySelector("div[title=点击关闭]").click(); } catch (e) { console.log(0); }
    //     // 解除打印限制
    //     utils.allowPrint();
    // }


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


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

        globalThis.open(
            `${url.protocol}//${url.host}/?num=${num}&lmt=${lmt}&fname=${fname}&path=${base_path}`,
            "hostage"
        );
    }


    function deliwenku() {
        utils.createBtns();
        utils.setBtnListener(jumpToHostage, "btn_1", "到下载页面");
    }

    function showHints() {
        let info = globalThis["deliJS"];
        let 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">
                文档名称:${info.fname}<br>
                原始文档页数:${info.num}<br>
                最大免费页数:${info.lmt}<br>
            </div>
        </div>
    `;
        document.title = utils.getUrlParam("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() {
        let path = utils.getUrlParam("path");
        let base_url = genBaseURL(path);
        let fname = utils.getUrlParam("fname");
        let num = parseInt(utils.getUrlParam("num"));
        let lmt = parseInt(utils.getUrlParam("lmt"));

        lmt = lmt > 3? lmt: 20;
        lmt = lmt > num? num: lmt;

        globalThis["deliJS"] = {
            base_url,
            num,
            fname,
            lmt
        };
    }


    async function exportPDF$2() {
        let info = globalThis["deliJS"];
        await utils.imgUrlsToPDF(
            genUrls(info.base_url, info.num),
            info.fname,
            info.lmt,
            true  // 请求完成后清理控制台
        );
    }


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

        // 创建按钮区
        utils.createBtns();
        // btn_1: 导出PDF
        exportPDF$2 = await utils.recTime(exportPDF$2);
        utils.setBtnListener(exportPDF$2, "btn_1", "下载并导出PDF");
    }

    function readAll360Doc() {
        // 展开文档
        document.querySelector(".article_showall a").click();
        // 隐藏按钮
        utils.toggleBtn("btn_1");
        // 显示按钮
        utils.toggleBtn("btn_2");
        utils.toggleBtn("btn_3");
        utils.toggleBtn("btn_4");
    }


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

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

        // 保存纯文本文档
        let title = document.querySelector("#titiletext").textContent;
        utils.saveAs(`${title}.txt`, content.join("\n"));
    }


    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 = document.querySelectorAll(selector);
        let under_doc_1, under_doc_2;
        try {
            under_doc_1 = document.querySelector("#bgchange p.clearboth").nextElementSibling;
            under_doc_2 = document.querySelector("#bgchange").nextElementSibling.nextElementSibling;
        } catch (e) { console.log(); }
        // 执行移除
        for (let elem of elem_list) {
            utils.tryToRemoveElement(elem);
        }
        utils.tryToRemoveElement(under_doc_1);
        utils.tryToRemoveElement(under_doc_2);
        // 执行隐藏
        document.querySelector("a[title]").style.display = "none";

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


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


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


    /**
     * 重置图像链接和最大宽度
     * @param {Document} doc
     */
    function resetImg(doc=document) {
        doc.querySelectorAll("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.createBtns();
        // btn_1: 展开文档
        utils.setBtnListener(readAll360Doc, "btn_1");
        // btn_2: 导出纯文本
        utils.setBtnListener(saveText_360Doc, "btn_2", "导出纯文本");
        // btn_3: 打印页面到PDF
        utils.setBtnListener(printPage360Doc, "btn_3", "打印页面到PDF");
        // btn_3: 清理页面
        utils.setBtnListener(cleanPage, "btn_4", "清理页面(推荐)");
    }

    // /**
    //  * 查找出所有未被捕获的页码,并返回列表
    //  * @returns 未捕获页码列表
    //  */
    // function getMissedPages() {
    //     let all = []; // 全部页码
    //     for (let i = 0; i < window.mbaJS.max_page; i++) {
    //         all[i] = i + 1;
    //     }
    //     let missed = []; // 未捕获页码
    //     let possessed = Array.from(window.mbaJS.canvases_map.keys()); // 已捕获页面

    //     // 排除并录入未捕获页码
    //     for (let num of all) {
    //         if (!possessed.includes(`page${num}`)) {
    //             missed.push(num);
    //         }
    //     }
    //     return missed;
    // }


    // /**
    //  * 根据键中的id数字对map排序
    //  * @param {Map} elems_map 
    //  * @returns sorted_map
    //  */
    // function sortMapByID(elems_map) {
    //     // id形式:page2
    //     let elems_arr = Array.from(elems_map);
    //     elems_arr.sort((item1, item2) => {
    //         // 从key中取出id
    //         let id1 = parseInt(item1[0].replace("page", ""));
    //         let id2 = parseInt(item2[0].replace("page", ""));
    //         // 升序排序
    //         return id1 - id2;
    //     });
    //     // 返回排序好的map
    //     return new Map(elems_arr);
    // }


    // /**
    //  * 存储动态加载的canvas元素、textContent
    //  */
    // function storeElements_MBA() {
    //     let canvases_map = window.mbaJS.canvases_map;
    //     let texts_map = window.mbaJS.texts_map;
    //     let quality = window.mbaJS.quality;

    //     document.querySelectorAll(".page[data-loaded=true]").forEach(
    //         (elem) => {
    //             let capture = (elem) => {
    //                 // (1) 存储页面为canvas图形
    //                 let canvas, data_base64;
    //                 // 导出canvas数据防止丢失
    //                 try {
    //                     // 存储canvas
    //                     canvas = elem.querySelector("canvas[id*=page]");
    //                     if (window.mbaJS.only_text) {
    //                         data_base64 = null;
    //                     } else {
    //                         data_base64 = canvas.toDataURL("image/jpeg", quality);
    //                     }
    //                 } catch (e) {
    //                     // utils.sleep(500);
    //                     return;
    //                 }
    //                 // 增量录入map
    //                 let id = canvas.id; // id的形式:page2
    //                 if (!canvases_map.has(id)) {
    //                     canvases_map.set(id, data_base64);
    //                 }
    //                 // 确定canvas长宽
    //                 if (!window.mbaJS.only_text && !window.mbaJS.width) {
    //                     window.mbaJS.width = parseInt(canvas.width);
    //                     window.mbaJS.height = parseInt(canvas.height);
    //                 }

    //                 // (2) 存储text
    //                 let text = elem.textContent;
    //                 if (!texts_map.has(id)) {
    //                     texts_map.set(id, text);
    //                 }
    //             };
    //             setTimeout(capture, 500, elem);
    //         });
    //     if (canvases_map.size === window.mbaJS.max_page) {
    //         // 根据id排序
    //         window.mbaJS.canvases_map = sortMapByID(window.mbaJS.canvases_map);
    //         window.mbaJS.texts_map = sortMapByID(window.mbaJS.texts_map);
    //         window.mbaJS.finished = true;
    //         window.onscroll = null;
    //     }
    // }


    // /**
    //  * 将canvas转为jpeg,然后导出PDF
    //  * @param {Array} base64_list canvas元素列表
    //  * @param {String} title 文档标题
    //  */
    // function saveCanvasesToPDF_MBA(base64_list, title) {
    //     let width = window.mbaJS.width;
    //     let height = window.mbaJS.height;

    //     console.log(`canvas数据:宽: ${width}px,高: ${height}px`);
    //     // 如果文档第一页的宽比长更大,则landscape,否则portrait
    //     let orientation = width > height ? 'l' : 'p';
    //     let pdf = new jspdf.jsPDF(orientation, 'px', [height, width]);

    //     // 保存每一页文档到每一页pdf
    //     let i = 0;
    //     for (let base64 of base64_list) {
    //         i += 1;
    //         pdf.addImage(base64, 'JPEG', 0, 0, width, height);
    //         // 如果当前不是文档最后一页,则需要添加下一个空白页
    //         if (i < window.mbaJS.max_page) {
    //             pdf.addPage();
    //         }
    //     }
    //     // 导出文件
    //     pdf.save(`${title}.pdf`);
    // }

    // /**
    //  * 判断文档页是否收集完毕,当不行时给出提示
    //  * @returns boolean
    //  */
    // function ready2use() {
    //     removeAds(); // 顺便清理广告
    //     // 如果是首次点击按钮,给出提示
    //     if (window.mbaJS.first_hint) {
    //         let hint = [
    //             "如果浏览速度过快,比如:",
    //             "当前页面还没完全加载好就滚动页面去看下一页",
    //             "那就极有可能导致导出的PDF有空白页或文本有缺漏",
    //             "由防范技术的干扰,该功能目前很不好用,见谅"
    //         ].join("\n");
    //         alert(hint);
    //         window.mbaJS.first_hint = false;
    //     }
    //     // 如果文档页没有收集完,给出提示
    //     if (!window.mbaJS.finished) {
    //         let hint = [
    //             "仍有内容未加载完,无法使用该功能",
    //             "建议从头到尾慢速地再浏览一遍",
    //             "以下是没有加载完成页面的页码:",
    //             getMissedPages().join(",")
    //         ]
    //         alert(hint.join("\n"));
    //         return false;
    //     }
    //     return true;
    // }


    // /**
    //  * 用捕获好的canvas转jpg,生成PDF
    //  * @returns 
    //  */
    // function canvas2PDF_mba() {
    //     if (!ready2use()) {
    //         return;
    //     }
    //     let canvases = window.mbaJS.canvases_map.values();
    //     // 导出PDF
    //     let title = document.title.split("-")[0].trim();
    //     saveCanvasesToPDF_MBA(canvases, title);
    // }


    // /**
    //  * 拼合捕获好的文本,保存到txt文件
    //  * @returns 
    //  */
    // function saveText_mba() {
    //     if (!ready2use()) {
    //         return;
    //     }
    //     let content = Array.from(window.mbaJS.texts_map.values());
    //     let title = document.title.split("-")[0].trim();
    //     utils.saveAs(`${title}.txt`, content.join("\n"));
    // }


    // /**
    //  * 移除广告
    //  */
    // function removeAds() {
    //     document.querySelectorAll(".doc-ad").forEach((ad_elem) => {
    //         utils.tryToRemoveElement(ad_elem);
    //     });
    // }


    // function mbalib_() {
    //     // 移除广告和左侧工具栏
    //     removeAds();
    //     let tool_bar = document.querySelector(".tool-bar");
    //     utils.tryToRemoveElement(tool_bar);

    //     // 创建按钮
    //     utils.createBtns();
    //     // 隐藏按钮
    //     utils.toggleBtnStatus("btn_1");
    //     // 显示按钮
    //     utils.toggleBtnStatus("btn_2");
    //     utils.toggleBtnStatus("btn_3");
    //     utils.toggleBtnStatus("btn_4");

    //     // 取得页数
    //     let max_page = parseInt(document.querySelector("#numPages").textContent.replace("/ ", ""));

    //     // 为导出内容提供全局变量,便于动态收集文档页元素的存取
    //     window.mbaJS = {
    //         max_page: max_page,
    //         texts_map: new Map(), // id: text
    //         canvases_map: new Map(), // id: canvas_data_base64
    //         quality: 1, // canvas转jpg的质量
    //         width: null, // canvas宽度(px)
    //         height: null,
    //         finished: false, // 是否收集完了全部文档页元素
    //         first_hint: true,
    //         scroll_count: 0, // 用于统计累计触发scroll的次数,
    //         only_text: false // 是否仅捕获文本
    //     };
    //     // 跟随浏览,动态收集页面元素
    //     window.onscroll = () => {
    //         storeElements_MBA();
    //     };
    //     // 跟随浏览,动态收集页面元素
    //     utils.scrollFunc(storeElements_MBA, window.mbaJS, 20, 50, "mba元素: 收集");
    //     // 绑定事件
    //     utils.setBtnListener(saveText_mba, [], "btn_2", "导出纯文本(不稳定)");
    //     utils.setBtnListener(canvas2PDF_mba, [], "btn_3", "导出PDF(不稳定)");

    //     // 根据页数决定按钮功能:<40页,导出文本+导出pdf,>40页:导出文本
    //     let btn_text, aim_btn, hint;
    //     if (max_page > 40) {
    //         btn_text = "失效说明";
    //         aim_btn = "btn_3";
    //         hint = [
    //             "页数超过40,脚本无效",
    //             "只能使用导出文本功能",
    //             "而此脚本会使页面内容加载明显变慢,建议禁用"
    //         ];
    //         utils.setBtnListener(
    //             () => {
    //                 utils.toggleBtnsSec();
    //                 window.onscroll = null;
    //             },
    //             [],
    //             "btn_4",
    //             "临时禁用脚本"
    //         );
    //     } else {
    //         btn_text = "空白页说明";
    //         aim_btn = "btn_4";
    //         hint = [
    //             "导致空白页的原因如下",
    //             "加载该页的时间超过2秒 / 明显等待",
    //             "而此脚本会使页面内容加载明显变慢,如果影响严重请禁用"
    //         ];
    //     }

    //     utils.setBtnListener(() => {
    //         alert(hint.join("\n"));
    //     }, [], aim_btn, btn_text);
    // }


    // function mbalib() {
    //     setTimeout(mbalib_, 2000);
    // }

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


    function mbalib() {
        utils.createBtns();
        utils.setBtnListener(getPDF, "btn_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$1(page_num) {
        // 先尝试官方接口,不行再用模拟的
        try {
            Viewer._GotoPage(page_num);
        } catch(e) {
            console.error(e);
            utils.toPageNo(wk$("#pageNumInput")[0], page_num, "keydown");
        }
    }


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

        // 逐页加载
        let lmt = window.dugenJS.lmt;
        for (let i of utils.range(1, lmt + 1)) {
            toPage$1(i);
            await utils.waitUntil(
                () => 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$1() {
        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$1();
        if (!ok) {
            return;
        }
        utils.saveAs("urls.csv", urls.join("\n"));
    }


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


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

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

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

    /**
     * 取得文档类型
     * @returns {String} 文档类型str
     */
    function getDocType() {
        let type_elem = document.querySelector(".title .icon.icon-format");
        // ["icon", "icon-format", "icon-format-doc"]
        let cls_str = type_elem.classList[2];
        // "icon-format-doc"
        let type = cls_str.split("-")[2];
        return type;
    }


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


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


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


    /**
     * 取得最大页码
     * @returns {Number} 最大页码
     */
    function getPageCounts$1() {
        // let page_counts_str = document.querySelector(".intro-list").textContent;
        // let page_counts = parseInt(page_counts_str.match(/(?<=约 )[0-9]{1,3}(?=页)/)[0]);
        return parseInt(
            wk$(".counts")[0].textContent.split("/")[1].trim()
        );
    }


    /**
     * 取得未加载页面的页码
     * @param {Set} loaded 已加载的页码集合
     * @returns {Array} not_loaded 未加载页码列表
     */
    function getNotLoaded(loaded) {
        let not_loaded = [];
        let page_counts = window.book118JS.page_counts;
        for (let i = 1; i <= page_counts; i++) {
            if (!loaded.has(i)) {
                not_loaded.push(i);
            }
        }
        return not_loaded;
    }


    /**
     * 取得全部文档页的url
     * @returns [<是否全部加载>, <未加载页码列表>|<urls列表>]
     */
    function getUrls() {
        let loaded = new Set(); // 存储已加载页面的页码
        let urls = []; // 存储已加载页面的图形src
        // 收集已加载页面的url
        document.querySelectorAll("div[data-id]").forEach((div) => {
            let src = div.querySelector("img").src;
            if (src) {
                // "1": "https://view-cache.book118.com/..."
                loaded.add(parseInt(div.getAttribute("data-id")));
                urls.push(src);
            }
        });
        // 如果所有页面加载完毕
        if (loaded.size === window.book118JS.page_counts) {
            return [true, urls];
        }
        // 否则收集未加载页面的url
        return [false, getNotLoaded(loaded)];
    }


    /**
     * 展开全文
     */
    function readAll() {
        window.preview.jump(999);
        utils.toggleBtn("btn_1");
    }


    /**
     * btn_2: 导出图片链接
     */
    function wantUrls() {
        let [flag, res] = getUrls();
        // 页面都加载完毕,下载urls
        if (flag) {
            utils.saveAs("urls.csv", res.join("\n"));
            return;
        }
        // 没有加载完,提示出未加载好的页码
        let hints = [
            "仍有页面没有加载",
            "请浏览并加载如下页面:",
            res.join(",")
        ];
        alert(hints.join("\n"));
    }


    /**
     * 打开PPT预览页面
     */
    function openPPTpage() {
        alert("原创力文档可能改版了,该功能可能无效");
        window.preview.getSrc();
        let openPPT = () => {
            let ppt_src = document.querySelector("iframe.preview-iframe").src;
            utils.openURL(ppt_src);
            window.preview.close();
        };
        setTimeout(openPPT, 1000);
    }


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

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

        // 处理非PPT文档
        // 创建按钮组
        utils.createBtns();
        // 绑定监听器到按钮
        // 按钮1:展开文档
        utils.setBtnListener(readAll, "btn_1");
        // 按钮2:导出图片链接
        utils.setBtnListener(wantUrls, "btn_2", "导出图片链接");
        utils.toggleBtn("btn_2");
        // 按钮3:自动导出链接
        utils.setBtnListener(toAPIPage, "btn_3", "自动导出链接(荐)");
        utils.toggleBtn("btn_3");
    }


    /**
     * 取得PPT文档最大页码
     * @returns PPT文档最大页码int
     */
    async function getPageCountsPPT() {
        await utils.waitUntil(
            () => !!wk$("#PageCount")[0].textContent
        );
        return parseInt(
            wk$("#PageCount")[0].textContent
        );
    }


    /**
     * 取得当前的页码
     * @returns {Number} this_page
     */
    function getThisPage() {
        let this_page = document.querySelector("#PageIndex").textContent;
        this_page = parseInt(this_page);
        return this_page;
    }


    /**
     * 点击下一动画直到变成下一页,再切回上一页
     * @param {Number} next_page 下一页的页码
     */
    async function __nextFrameUntillNextPage(next_page) {
        // 如果已经抵达下一页,则返回上一页
        let this_page = getThisPage();

        // 最后一页直接退出
        if (next_page > window.book118JS.page_counts) {
            return;
        }
        // 不是最后一页,但完成了任务
        else if (this_page === next_page) {
            document.querySelector(".btmLeft").click();
            await utils.sleep(500);
            return;
        }
        // 否则递归的点击下一动画
        document.querySelector(".btmRight").click();
        await utils.sleep(500);
        await __nextFrameUntillNextPage(next_page);
    }


    /**
     * 确保当前页面是最后一帧动画
     */
    async function ensurePageLoaded() {
        // 取得当前页码和下一页页码
        let this_page = getThisPage();
        let next_page = this_page + 1;
        // 开始点击下一页按钮,直到变成下一页,再点击上一页按钮来返回
        await __nextFrameUntillNextPage(next_page);
    }


    /**
     * (异步)转换当前视图为canvas,添加到book118JS.canvases中。在递归终止时显示btn_2。
     */
    async function docView2Canvas() {
        await ensurePageLoaded();
        // 取得页码
        let cur_page = getThisPage();
        // 取得视图元素,计数从0开始
        let doc_view = document.querySelector(`#view${cur_page-1}`);
        // 转化为canvas
        let tasks = window.book118JS.tasks;
        tasks.push((async() => {
            let canvas = await html2canvas(doc_view);
            console.log(`page ${cur_page} finished`);
            return canvas;
        })());

        // 如果到最后一页
        if (cur_page === window.book118JS.page_counts) {
            // 终止递归,并且显示导出PDF按钮
            utils.toggleBtn("btn_2");
            return;
        }
        // 否则下一次递归(继续捕获下一页)
        document.querySelector(".pgRight").click();
        await utils.sleep(500);
        await docView2Canvas();
    }


    /**
     * 将捕获的canvases合并并导出为pdf
     * @returns 
     */
    async function canvases2pdf() {
        // 已经捕获的页面数量
        let stored_amount = window.book118JS.tasks.length;
        // 总页面数量
        let page_counts = window.book118JS.page_counts;
        // 校验数量
        let diff = page_counts - stored_amount;
        if (diff > 0) {
            alert(`缺失了 ${diff} 页,可以过一会再点击该按钮试试。`);
            if (!confirm("是否仍要导出PDF?")) {
                // 不坚持导出PDF的情况
                return;
            }
        }
        // 导出PDF
        let canvases = await Promise.all(window.book118JS.tasks);
        window.book118JS.canvases = canvases;
        // 取得宽高
        let model = canvases[0];
        let width = model.width;
        let height = model.height;
        // 取得标题然后导出pdf
        utils.canvasesToPDF(canvases, "原创力PPT文档", width, height);
    }


    /**
     * 原创力文档(PPT)下载策略
     */
    async function book118_PPT() {
        // 创建全局对象
        window.book118JS = {
            page_counts: await getPageCountsPPT(),
            canvases: [],  // 存储每页文档转化的canvas
            tasks: []
        };

        // 创建按钮区
        utils.createBtns();
        // 绑定监听器到按钮1
        utils.setBtnListener(() => {
            let hints = [
                "正在为文档“截图”,请耐心等待过程完成,不要操作",
                "“截图”可能会有额外一层黑边,原因未知,暂无法处理,烦请谅解"
            ];
            alert(hints.join("\n"));
            // 隐藏按钮1
            utils.toggleBtn("btn_1");
            // 开始捕获页面(异步)
            docView2Canvas(window.book118JS.page_counts);
        }, "btn_1", "捕获页面");
        // 为按钮2绑定监听器
        utils.setBtnListener(
            await utils.recTime(canvases2pdf),
            "btn_2",
            "导出PDF"
        );
    }


    /**
     * 取得当前页面的excel,返回csv string
     * @returns {String} csv
     */
    function excel2CSV() {
        let table = [];
        let rows = document.querySelectorAll("tr[id]");

        // 遍历行
        for (let row of rows) {
            let csv_row = [];
            // 遍历列(单元格)
            for (let cell of row.querySelectorAll("td[class*=fi], td.tdrl")) {
                // 判断单元格是否存储图片
                let img = cell.querySelector("img");
                if (img) {
                    // 如果是图片,保存图片链接
                    csv_row.push(img.src);
                } else {
                    // 否则保存单元格文本
                    csv_row.push(cell.textContent);
                }
            }
            table.push(csv_row.join(","));
        }

        let csv = table.join("\n");
        csv = csv.replace(/\n{2,}/g, "\n");
        return csv;
    }


    /**
     * 下载当前表格内容,保存为csv(utf-8编码)
     */
    function wantEXCEL() {
        let file_name = "原创力表格_UTF-8.csv";
        utils.saveAs(file_name, excel2CSV());
    }


    /**
     * 在Excel预览页面给出操作提示
     */
    function help() {
        let hints = [
            "【导出表格到CSV】只能导出当前sheet,",
            "如果有多张sheet请在每个sheet上用按钮分别导出CSV。",
            "CSV是一种简单的表格格式,可以被Excel打开,",
            "并转为 xls 或 xlsx 格式存储,",
            "但CSV本身不能存储图片,所以用图片链接代替,请自行下载图片",
            "",
            "本功能导出的CSV文件无法直接用Excel打开,因为中文会乱码。",
            "有两个办法:",
            "1. 打开Excel,选择【数据】,选择【从文本/CSV】,",
            "  选择文件,【文件原始格式】选择【65001: Unicode(UTF-8)】,选择【加载】。",
            "2. 用【记事本】打开CSV文件,【文件】->【另存为】->",
            "  【编码】选择【ANSI】->【保存】。现在可以用Excel直接打开它了。"
        ];
        alert(hints.join("\n"));
    }


    /**
     * 原创力文档(EXCEL)下载策略
     */
    function book118_EXCEL() {
        // 创建按钮区
        utils.createBtns();
        // 绑定监听器到按钮
        utils.setBtnListener(wantEXCEL, "btn_1", "导出表格到CSV");
        utils.setBtnListener(help, "btn_2", "使用说明");
        // 显示按钮
        utils.toggleBtn("btn_2");
    }


    /**
     * 打开Excel预览页面
     */
    function openEXCELpage() {
        openPPTpage();
    }


    /**
     * 跳转到图片接口页面
     * @returns 
     */
    function toAPIPage() {
        let type = window?.base?.detail?.preview?.channel;
        if (type !== "pic") {
            alert("当前文档类型不适用");
            return;
        }

        let base = window.base;
        let query = utils.dictToQueryStr({
            project_id: 1,
            aid: base.detail.preview.office.aid,
            t: base.detail.senddate,
            view_token: base.detail.preview.pic.view_token,
            filetype: getDocType(),
            callback: "jQuery123_456",
            max: base.detail.preview.pic.preview_page,
        });

        open(`https://openapi.book118.com/?${query}`);
    }


    /**
     * 原创力文档下载策略
     */
    function book118() {
        let host = window.location.hostname;
        if (host === 'max.book118.com') {
            if (isEXCEL()) {
                utils.createBtns();
                utils.setBtnListener(openEXCELpage, "btn_1", "导出EXCEL");
            } else if (isPPT()) {
                utils.createBtns();
                utils.setBtnListener(openPPTpage, "btn_1", "导出PPT");
            } else {
                book118_CommonDoc();
            }
        } else if (host === "view-cache.book118.com") {
            book118_PPT();
        } else if (host.match(/view[0-9]{1,3}.book118.com/)) {
            book118_EXCEL();
        } else {
            console.log(`wk: Unknown host: ${host}`);
        }
    }

    async function get6urls(page) {
        let query = window.book118JS.query;
        let resp = await fetch(
            `${location.origin}/getPreview.html?&${query}&_=${Date.now()}&page=${page}`
        );
        
        let text = await resp.text();
        let json = text;
        if (text.startsWith("jQuery")) {
            json = text.replace(/.+?[(]{/, "{").slice(0, -2);
        }
        let info = JSON.parse(json);
        return Object.entries(info.data);
    }


    async function getAllUrls(max) {
        window.book118JS.requested = 0;
        let tasks = [];
        for (let i of wkutils.range(1, max, 6)) {
            tasks.push(get6urls(i));
            // 更新进度
            window.book118JS.requested += 1;
            // 等待间隔
            await wkutils.sleep(2000);
        }
        let url_bundles = await Promise.all(tasks);
        return url_bundles.flat(1).map(
            item => item[1]
        );
    }


    async function reget(urls) {
        let i = urls.findIndex(value => value === "");
        if (i !== -1) {
            let new_urls = await get6urls(i);
            new_urls.forEach(item => {
                if (item[1] !== "") {
                    urls[item[0]] = item[1];
                }
            });
            return true;
        }
        return false; 
    }


    async function exportUrls(max) {
        let urls = window.book118JS.urls;
        if (urls.length < 1) {
            urls = await getAllUrls(max);
            let i = 0;
            while (await reget(urls)) {
                console.log(`reloading: ${++i}`);
                await wkutils.sleep(1000);
            }    
            urls = urls.map(url => "https:" + url);
            window.book118JS.urls = urls;
        }
        utils.saveImgUrls(urls);
        console.log(200);
    }


    function makeQuery(params) {
        let keys = ["project_id", "aid", "t", "view_token", "filetype", "callback"];
        return keys.map(
            key => `${key}=${params.get(key)}`
        ).join("&");
    }


    async function exportUrlsGUI() {
        utils.addPopup();
        utils.setPopupHead("进度条");
        utils.setPopupBody("进度条初始化中...");
        
        let showProgress = () => {
            if (location.hash === "#wk-popup") {
                utils.toID("?");
            } else {
                utils.toID("wk-popup");
            }
        };
        utils.setBtnListener(showProgress, "btn_2", "进度条");
        utils.toggleBtn("btn_2");
        utils.toID("wk-popup");

        await exportUrls(window.book118JS.max);
        utils.removePopup();
        utils.toggleBtn("btn_2");
    }


    function updateProgress() {
        let all = parseInt(window.book118JS.max / 6) + 1;
        utils.setPopupBody(`已经请求:${window.book118JS.requested}/${all}`);
    }



    /**
     * 原创力文档图片接口策略
     */
    async function book118api() {
        let url = new URL(location.href);
        let query = makeQuery(url.searchParams);
        let _rqst = 0;

        window.book118JS = {
            query,
            urls: [],
            max: parseInt(url.searchParams.get("max")),
            get requested() { return _rqst },
            set requested(val) { 
                _rqst = val;
                updateProgress();
            },
        };

        utils.createBtns();
        utils.setBtnListener(
            await utils.recTime(exportUrlsGUI),
            "btn_1",
            "导出链接"
        );
    }

    // 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纸背景
        let [page_w, page_h] = [1190, 1680];
        let bg = document.createElement("canvas");
        bg.width = page_w;  // 注意canvas作为取景框的大小
        bg.height = page_h;  // 如果不设置等于一个很小的取景框
        
        let bg_ctx = bg.getContext("2d");
        bg_ctx.fillStyle = "white";
        bg_ctx.fillRect(0, 0, page_w, page_h);

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


    /**
     * 页面批量请求、裁剪重绘, 合成PDF并下载
     */
    async function turnPagesToPDF() {
        // 渲染每页
        let tasks = [];
        wk$("#viewer .page").forEach((page_div, i) => {
            tasks.push(
                getAndDrawPage(i, page_div)
            );
        });
        
        // 等待每页渲染完成后,排序
        let results = await Promise.all(tasks);
        results.sort((prev, next) => prev[0] - next[0]);
        
        // 合并为PDF并导出
        utils.canvasesToPDF(
            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.createBtns();
        // 绑定监听器
        // 按钮1:导出PDF
        turnPagesToPDF = await utils.recTime(turnPagesToPDF);
        utils.setBtnListener(hintThenDownload$1, "btn_1", "导出PDF");
    }

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


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


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


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


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


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

    /**
     * 跳转到页码
     * @param {string | number} num 
     */
    function toPage(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 toPageGUI() {
        let num = prompt("请输入要跳转的页码")?.trim();
        if (/^[0-9]+$/.test(num)) {
            toPage(num);
        } else {
            console.log(`输入值 [${num}] 不是合法整数`);
        }
    }


    /**
     * 记录prop不存在value到控制台
     * @param {string} prop 
     * @param {any} value 
     */
    function logEmpty(prop, value) {
        console.log(`wk: info's [${prop}] is an empty value: [${value}]`);
    }


    /**
     * 验证info对象是否成功采集各项数据,失败时输出到控制台
     * @param {Object} info 
     */
    function isIntact(info) {
        for (let prop in info) {
            let value = info[prop];

            if (value === undefined || value === "" || value === NaN) {
                logEmpty(prop, value);
                return false;
            }

            if (typeof value === "object") {
                if (!isIntact(value)) {
                    return false;
                }
            }
        }
        return true;
    }


    /**
     * 更新 WebPreview.Base.data 中的时间戳属性
     */
    function updateStamp() {
        let data = window?.WebPreview?.Base?.data;
        if (data) {
            let now = Date.now();
            data.jsoncallback = 'callback' + now;
            data.callback = 'callback' + now;
            data._ = now;
        }
    }


    /**
     * 收集文档信息,用于跨页面传递
     * @returns 
     */
    function collectInfo() {
        let 
            win = window,
            params = win.previewParams,
            view = win.WebPreview,
            ver = view?.version,
            base = view?.Base,
            data = view?.Data,
            pages = parseInt(wk$('meta[property="og:document:page"]')[0].content),
            preview = data?.preview_page,  // 默认值可能不准,需要从加密图像url链接的请求中获取更新值
            encrypted_at = base?.page?.start,
            query_str = utils.dictToQueryStr(base?.data);

        // WebPreview 为 true 说明>=6页
        // general.encrypted 为 true 说明>=21页
        let info = {
            direct: {
                base_url: params?.pre,
                fake_type: params?.ShowType === 2 ? ".svg" : ".gif"
            },

            encrypted: {
                query_str,
                step: base?.page?.zone,
                api: base?.host + base?.requestUrl
            },

            general: {
                WebPreview: !!view,
                renrendoc_ver: ver,
                ver_followed: ver ? ver === "2022.10.25_2" : "unknown",
                pages,
                encrypted_at,
                preview,
                encrypted:
                    preview && encrypted_at ? (
                        preview < encrypted_at ? false : true       // 预览页码小于加密起始页码,肯定不加密
                    ) : (                                  // 不存在某个页码,说明页数少,不加密
                        pages && encrypted_at ? (
                            pages < encrypted_at ? false : true     // 总页码小于加密起始页码,肯定不加密
                        ) : false                                   // 不存在某个页码,说明页数少,不加密
                    )
            }
        };
        isIntact(info);
        window.rrdocJS.info = info;
        return info;
    }


    /**
     * 为请求失败抛出异常
     * @param {Response} resp 
     */
    function raiseForStatus(resp) {
        let code = resp.status;
        if ("45".includes(`${code}`[0])) {
            throw new Error(`request failed: ${code}`);
        }
    }

    /**
     * 获取加密图片的链接列表的JSON响应
     * @param {Object} info 
     * @param {number} begin 请求的起始页码,一般是 5n + 1 且 n >= 21
     * @returns {Promise<Object>}
     */
    async function _getEncryptedImgUrlsJson(info, begin) {
        // 配置请求
        let url = new URL(
            `${info.encrypted.api}?${info.encrypted.query_str}`
        );
        url.searchParams.set("start", begin);
        
        // 请求图像链接列表
        let resp = await fetch(url);
        raiseForStatus(resp);
        let text = await resp.text();
        let json = text.replace(/callback[0-9]+[(]/, "").slice(0, -1);
        return JSON.parse(json);
    }


    /**
     * 通过请求一次加密图片链接列表来更新可预览页数
     */
    async function updatePreviewPageNum(info) {
        let begin = info.general.encrypted_at;
        let data = await _getEncryptedImgUrlsJson(info, begin);
        console.log(data);

        let prop = window?.WebPreview?.Data;
        if (!prop) {
            logEmpty("window?.WebPreview?.Data", prop);
            return;
        }
        let preview = data?.data?.read_count;
        if (preview) {
            prop.preview_page = parseInt(preview);
        }
    }


    /**
     * 获取加密图片的链接列表(length=5)
     * @param {Object} info 
     * @param {number} begin 请求的起始页码,一般是 5n + 1 且 n >= 21
     * @returns {Promise<string>}
     */
    async function getEncryptedImgUrls(info, begin) {
        // 请求JSON
        let data = await _getEncryptedImgUrlsJson(info, begin);
        
        // 提取列表
        if (data.data && data.data.preview_list) {
            return data.data.preview_list.map(page => "https:" + page.url);
        }
        return [];
    }


    /**
     * 获取全部加密图片链接构成的列表
     * @param {Object} info 
     * @returns {Promise<Array<string>>}
     */
    async function getAllEncryptedImgUrls(info) {
        info = await getLatestInfo(info);
        
        let
            gap = window.rrdocJS.gap,
            max_gap = window.rrdocJS.max_gap,
            begin = info.general.encrypted_at,
            stop = info.general.preview,
            step = info.encrypted.step,
            tasks = [];
        await utils.sleep(gap, max_gap);
        
        // 总进度条
        window.rrdocJS.all = stop + 1 - begin;

        for (let i of utils.range(begin, stop + 1, step)) {
            tasks.push(getEncryptedImgUrls(info, i));
            // 当前进度
            window.rrdocJS.requested += step;
            // 等待间隔,2-2.5秒
            await utils.sleep(gap, max_gap);
        }
        return (await Promise.all(tasks)).flat();
    }


    function _notNull(value, default_value) {
        return value !== null ? value : default_value;
    }


    /**
     * 获取全部直接请求的图片
     * @param {Object} info 
     * @param {number} end 可以手动指定终止页码, 默认为null
     * @param {string} base 可以手动指定基准url, 默认为null
     * @param {string} type 可以手动指定图像类型, 默认为null
     * @returns
     */
    function* genDirectImgUrls(info, end=null, base=null, type=null) {
        let pages = info.general.pages;
        end = pages > 20 ? 20 : pages;
        base = _notNull(base, info.direct.base_url);
        type = _notNull(type, info.direct.fake_type);

        for (let i of utils.range(1, end + 1)) {
            yield `${base}${i}${type}`;
        }
    }


    /**
     * 提取首个图片链接的 [base_url, type]
     * @returns {Array<string>}
     */
    function getBaseUrlOnPage() {
        let img = wk$(".page > img")[0];
        let origin = img.getAttribute("data-original");
        let url = origin ? origin : img.src;
        let type = url.slice(-8).split("1.")[1];
        return [url.replace("1." + type, ""), type];
    }

    /**
     * 以旧info查询并返回新的
     * @param {Object} info 
     * @returns {Promise<Object>}
     */
    async function getLatestInfo(info) {
        updateStamp();
        info = collectInfo();
        await updatePreviewPageNum(info);
        return collectInfo();
    }


    /**
     * 初始化进度条弹窗
     */
    function initProgressPopup() {
        let all = -1;
        Object.defineProperty(
            window.rrdocJS,
            "all",
            {
                get: () => all,
                set: val => all = val
            }
        );

        utils.addPopup();
        utils.setPopupHead("下载进度条");
        utils.setPopupBody("进度条初始化中...");

        let sent = 0;
        Object.defineProperty(
            window.rrdocJS,
            "requested",
            {
                get: () => sent,
                set: val => {
                    sent = val;
                    // 计算进度
                    let
                        rate = (val / all * 100),
                        show_val = val <= all ? val : all,
                        remain
                            = 0.5 * (
                                window.rrdocJS.gap + window.rrdocJS.max_gap
                            ) * 0.001 * (all - val)
                            / window.rrdocJS.info.encrypted.step;
                    rate = (rate <= 100 ? rate : 100).toFixed(2);
                    remain = (remain >= 0 ? remain : 0).toFixed(1);
                    // 更新进度条
                    utils.setPopupBody(
                        `当前进度: ${rate}%,${show_val}/${all}(+20)页,预计剩余${remain}秒`
                    );
                }
            }
        );
        // 按钮3:显示进度条
        utils.setBtnListener(
            () => utils.toID("wk-popup"),
            "btn_3",
            "显示进度条"
        );
        utils.toggleBtn("btn_3");
        // 按钮4:为什么下载慢
        utils.setBtnListener(
            () => {
                let delay = (window.rrdocJS.gap / 1000).toFixed(0);
                alert(`因网站限制,请求间隔必须大于 ${delay} 秒,请理解`);
            },
            "btn_4",
            "为什么下载慢"
        );
        utils.toggleBtn("btn_4");
        // 显示弹窗
        utils.toID("wk-popup");
        // 隐藏跳转页码以降低触发网络请求的可能性
        utils.toggleBtn("btn_2");
    }


    /**
     * 销毁进度条弹窗
     */
    function destoryProgressPopup() {
        let {all, requested} = window.rrdocJS;
        Object.defineProperties(window.rrdocJS, {
            "all": {
                value: all
            },
            "requested": {
                value: requested
            }
        });
        utils.removePopup();
        // 隐藏按钮3
        utils.toggleBtn("btn_3");
        // 显示跳转页码
        utils.toggleBtn("btn_2");
    }


    function showDocType() {
        let type = window.rrdocJS.doc_type;
        alert(`当前文档类型:${type}\n1类:最多导出5页的链接\n2类:最多导出20页的链接\n3类:不确定`);
    }


    async function judgeFileType() {
        // 创建按钮区
        utils.createBtns();

        let info = collectInfo();
        let pages = info.general.pages;
        let handler, doc_type;
        
        // 判断页数范围
        // 小于等于5页
        if (pages < 6) {
            doc_type = 1;
            handler = () => {
                let [base, type] = getBaseUrlOnPage();
                utils.saveImgUrls(
                    genDirectImgUrls(info, null, base, type)
                );
            };
        }
        
        // 没有加密图片
        else if (!info.general.encrypted) {
            doc_type = 2;
            handler = () => utils.saveImgUrls(
                genDirectImgUrls(info)
            );
        }

        // 有加密图片
        else if (info.general.encrypted) {
            doc_type = 3;
            handler = async() => {
                utils.toggleBtn("btn_1");
                initProgressPopup();
                let urls = [
                    ...genDirectImgUrls(info),
                    ...(await getAllEncryptedImgUrls(info))
                ];
                utils.saveImgUrls(urls);
                destoryProgressPopup();
            };
            // 按钮2:转到页码
            utils.setBtnListener(toPageGUI, "btn_2", "转到页码");
            utils.toggleBtn("btn_2");

        // 未知情况
        } else {
            doc_type = -1;
            console.log(info);
            alert("未能处理该文档,请加 QQ 群反馈");
            return;
        }
        window.rrdocJS.doc_type = doc_type;

        // 按钮1:导出图片链接
        utils.setBtnListener(handler, "btn_1", "导出图片链接");
        // 按钮5:为什么不全
        utils.setBtnListener(showDocType, "btn_5", "显示文档类型");
        utils.toggleBtn("btn_5");
    }


    /**
     * 人人文档下载策略
     */
    async function renrendoc() {
        window.rrdocJS = {
            info: {},
            doc_type: null,
            gap: 2000,  // 请求间隔, ms
            max_gap: 2500,  // 请求间隔的波动上限, ms
            all: -1,  // 一共需要请求的页数        
            requested: 0  // 已经请求的页数
        };

        await utils.sleep(500);
        judgeFileType();  // 判断完类型会显示按钮1
    }

    /**
     * 取得全部图片连接
     * @returns {Array<string>}
     */
    function getImgUrls() {
        // '../files/large/'
        let pre_path = htmlConfig.bookConfig.largePath;
        if (pre_path instanceof Array) {
            pre_path = pre_path[0];
        }
        let base = location.href;

        let urls = globalThis.htmlConfig.fliphtml5_pages
        .map(obj => {
            // "../files/large/d8b6c26f987104455efb3ec5addca7c9.jpg"
            let path = pre_path + obj.n[0].split("?")[0];
            let url = new URL(path, base);
            // https://book.yunzhan365.com/mctl/itid/files/large/d8b6c26f987104455efb3ec5addca7c9.jpg
            return url.href;
        });
        globalThis.img_urls = urls;
        return urls;
    }


    /**
     * 导出图片到PDF
     */
    async function exportPDF() {
        let urls = getImgUrls();
        let title = htmlConfig.meta.title;

        alert("正在下载图片,请稍等,时长取决于图片数量");
        await utils.imgUrlsToPDF(urls, title);
    }


    /**
     * 移除多余空按钮
     */
    function removeSpareBtns() {
        utils.tryToRemoveElements(
            wk$(".btns_section [class*=btn-]").slice(1)
        );
    }


    /**
     * 云展网文档下载策略
     */
    async function yunzhan365() {
        // 根据网址分别处理
        if (location.pathname.startsWith("/basic")) {
            // utils.setBtnListener(toIndex, "btn_1", "转到全屏页");
            return;
        }

        // 创建脚本启动按钮
        utils.createBtns();
        utils.setBtnListener(exportPDF, "btn_1", "导出PDF");
        removeSpareBtns();
    }

    /**
     * 截取当前对话,返回canvas
     * @param {Function} once 一次性任务
     * @returns {Promise<HTMLCanvasElement>}
     */
    async function captureChat(once) {
        // 执行一次性任务
        await once();

        return (
            await utils.elementsToCanvases([
                window.chat
            ])
        )[0];
    }


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


    /**
     * 导出图片到文件和剪贴板
     * @param {HTMLCanvasElement} canvas
     */
    async function exportCanvas(canvas) {
        let blob = await utils.canvasToBlob(canvas);
        utils.saveAs("BingAI对话.png", blob);
        copyToClipboard(blob);
    }


    /**
     * 截图并导出对话
     */
    async function exportChatAsImage() {
        // 定义一次性任务
        let once = await utils.once(() => {
            // 移除video元素
            utils.tryToRemoveElements(
                document.querySelectorAll("video")
            );

            // 移除对话外元素
            [...document.body.children].slice(2).forEach(elem => elem.remove());
        });

        await exportCanvas(
            await captureChat(once)
        );
    }


    /**
     * 初始化工作
     */
    async function init() {
        let 
            box1 = (await wk$$(".cib-serp-main"))[0],
            box2 = (await wk$$.call(box1.shadowRoot, "#cib-conversation-main"))[0],
            chat = (await wk$$.call(box2.shadowRoot, "#cib-chat-main"))[0];
        window.chat = chat;

        let 
            outer_bar = (await wk$$.call(box1.shadowRoot, "#cib-action-bar-main"))[0],
            bar = (await wk$$.call(outer_bar.shadowRoot, ".root"))[0],
            ori_btn = (await wk$$.call(bar, ".outside-left-container"))[0];

        if (!(ori_btn instanceof HTMLElement)) {
            throw Error(`${ori_btn} 的类型不是 HTMLElement!`);
        }

        // 复制按钮
        let button = ori_btn.cloneNode(false);
        window.button = button;
        
        // 更换按钮图标
        button.innerHTML = `
        <div class="button-compose-wrapper">
        <button class="button-compose" type="button" aria-label="新主题" collapsed="">
        <div class="button-compose-content">
        <div size="32" style="--icon-size:32px;" class="button-compose-icon">
        <svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" preserveAspectRatio="xMidYMid meet">
        <metadata>
        Created by potrace 1.15, written by Peter Selinger 2001-2017, edited by Allen Lv 2023
        </metadata>
        <g transform="translate(4.7, 4.7) scale(0.02, 0.02)">
        <g transform="translate(0.000000,1280.000000) scale(0.100000,-0.100000)" fill="#ffffff" stroke="none">
        <path d="M3998 12784 c-62 -33 -58 154 -58 -2439 l0 -2365 -800 0 -800 0 -31
        -28 c-39 -35 -43 -89 -8 -136 66 -90 3316 -4165 3340 -4188 22 -21 39 -28 69
        -28 29 0 47 7 70 28 16 15 779 968 1695 2117 1163 1460 1667 2099 1671 2121 7
        36 -8 70 -40 96 -19 16 -78 18 -823 18 l-803 0 0 2375 0 2375 -29 32 -29 33
        -1699 2 c-1488 2 -1701 0 -1725 -13z"></path>
        <path d="M1687 4649 l-288 -8 -32 -27 c-17 -15 -332 -286 -699 -602 l-668
        -575 0 -1719 0 -1718 5630 0 5630 0 0 1718 -1 1717 -700 603 -700 603 -252 5
        c-139 3 -267 7 -284 9 l-33 4 0 -800 0 -799 -3645 0 c-2423 0 -3645 -3 -3645
        -10 0 -5 -3 -10 -7 -10 -5 0 -7 364 -5 810 1 445 -1 809 -5 808 -5 0 -137 -5
        -296 -9z"></path></g></g></svg></div>
        <div class="button-compose-text"><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">保存对话</font></font></div></div>
        </button>
        <div class="button-compose-hint"><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">保存对话</font></font></div>
        </div>`;
        
        // 为点击绑定回调
        button
        .querySelector(".button-compose-content")
        .addEventListener("click", exportChatAsImage);

        // 添加到DOM
        bar.appendChild(button);
    }


    /**
     * BingAI对话辅助工具
     */
    async function bingAIChat() {
        init();
    }

    /**
     * 主函数:识别网站,执行对应文档下载策略
     */
    function main() {
        // 显示当前位置
        let host = location.host;
        console.log(`当前 host: ${host}`);

        if (host.includes("docin.com")) {
            docin();
        } else if (host === "swf.ishare.down.sina.com.cn") {
            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 === "openapi.book118.com") {
            book118api();
        } else if (host.includes("book118.com")) {
            book118();
        } else if (host === "www.renrendoc.com") {
            renrendoc();
        } else if (host.includes("yunzhan365.com")) {
            yunzhan365();
        } else if (host === "www.bing.com") {
            bingAIChat();
        } else {
            console.log("匹配到了无效网页");
        }
    }


    let options = {
        show_buttons: true
    };
    
    if (options.activation_test) {
        alert(`Wenku Doc Downloader 已经生效!\n当前网址:\n${window.location.host}`);
    }
    
    // 根据配置选择:是否默认显示
    if (!options.show_buttons) {
        utils.toggleBtnsSec();
    }
    
    utils.manipulateElem(document.body, main);

})();