csdn2md - 批量下载CSDN文章为Markdown

下载CSDN文章为Markdown格式,支持专栏批量下载。CSDN排版经过精心调教,最大程度支持CSDN的全部Markdown语法:KaTeX内联公式、KaTeX公式块、图片、内联代码、代码块、Bilibili视频控件、有序/无序/任务/自定义列表、目录、注脚、加粗斜体删除线下滑线高亮、内容居左/中/右、引用块、链接、快捷键(kbd)、表格、上下标、甘特图、UML图、FlowChart流程图

// ==UserScript==
// @name         csdn2md - 批量下载CSDN文章为Markdown
// @namespace    http://tampermonkey.net/
// @version      3.3.1
// @description  下载CSDN文章为Markdown格式,支持专栏批量下载。CSDN排版经过精心调教,最大程度支持CSDN的全部Markdown语法:KaTeX内联公式、KaTeX公式块、图片、内联代码、代码块、Bilibili视频控件、有序/无序/任务/自定义列表、目录、注脚、加粗斜体删除线下滑线高亮、内容居左/中/右、引用块、链接、快捷键(kbd)、表格、上下标、甘特图、UML图、FlowChart流程图
// @author       ShizuriYuki
// @match        https://*.csdn.net/*
// @icon         https://g.csdnimg.cn/static/logo/favicon32.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @run-at       document-idle
// @license      PolyForm Strict License 1.0.0  https://polyformproject.org/licenses/strict/1.0.0/
// @supportURL   https://github.com/Qalxry/csdn2md
// @require      https://cdn.jsdmirror.com/gh/Qalxry/csdn2md/plugins/jszip.min.js#sha256-yeSlK6wYruTz+Q0F+8pgP1sPW/HOjEXmC7TtOiyy7YY=
// @require      https://cdn.jsdmirror.com/gh/Qalxry/csdn2md/plugins/fflate.min.js#sha256-w7NPLp9edNTX1k4BysegwBlUxsQGQU1CGFx7U9aHXd8=
// @require      https://cdn.jsdmirror.com/gh/Qalxry/csdn2md/plugins/streamSaver.min.js#sha256-VxQm++CYEdHipBjKWh4QQHHOYZmyo8F/7dJQxG11xFM=
// ==/UserScript==

(function () {
    "use strict";

    // 需要加载的库及其备用源
    const libsToLoad = {
        JSZip: {
            isLoaded: () => typeof JSZip !== "undefined",
            urls: [
                "https://cdn.jsdelivr.net/gh/Qalxry/csdn2md/plugins/jszip.min.js#sha256-yeSlK6wYruTz+Q0F+8pgP1sPW/HOjEXmC7TtOiyy7YY=",
                "https://cdn.jsdmirror.com/gh/Qalxry/csdn2md/plugins/jszip.min.js#sha256-yeSlK6wYruTz+Q0F+8pgP1sPW/HOjEXmC7TtOiyy7YY=",
                "https://cdnjs.webstatic.cn/ajax/libs/jszip/3.7.1/jszip.min.js#sha256-yeSlK6wYruTz+Q0F+8pgP1sPW/HOjEXmC7TtOiyy7YY=",
                "https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/jszip/3.7.1/jszip.min.js#sha256-yeSlK6wYruTz+Q0F+8pgP1sPW/HOjEXmC7TtOiyy7YY=",
                "https://use.sevencdn.com/ajax/libs/jszip/3.7.1/jszip.min.js#sha256-yeSlK6wYruTz+Q0F+8pgP1sPW/HOjEXmC7TtOiyy7YY=",
                "https://cdn.jsdmirror.com/ajax/libs/jszip/3.7.1/jszip.min.js#sha256-yeSlK6wYruTz+Q0F+8pgP1sPW/HOjEXmC7TtOiyy7YY=",
                "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js#sha256-yeSlK6wYruTz+Q0F+8pgP1sPW/HOjEXmC7TtOiyy7YY=",
            ],
        },
        fflate: {
            isLoaded: () => typeof fflate !== "undefined",
            urls: [
                "https://cdn.jsdelivr.net/gh/Qalxry/csdn2md/plugins/fflate.min.js#sha256-w7NPLp9edNTX1k4BysegwBlUxsQGQU1CGFx7U9aHXd8=",
                "https://cdn.jsdmirror.com/gh/Qalxry/csdn2md/plugins/fflate.min.js#sha256-w7NPLp9edNTX1k4BysegwBlUxsQGQU1CGFx7U9aHXd8=",
                "https://npm.webcache.cn/[email protected]/umd/index.js#sha256-w7NPLp9edNTX1k4BysegwBlUxsQGQU1CGFx7U9aHXd8=",
                "https://use.sevencdn.com/npm/[email protected]/umd/index.js#sha256-w7NPLp9edNTX1k4BysegwBlUxsQGQU1CGFx7U9aHXd8=",
                "https://cdn.jsdmirror.com/npm/[email protected]/umd/index.js#sha256-w7NPLp9edNTX1k4BysegwBlUxsQGQU1CGFx7U9aHXd8=",
                "https://unpkg.com/[email protected]/umd/index.js#sha256-w7NPLp9edNTX1k4BysegwBlUxsQGQU1CGFx7U9aHXd8=",
                "https://cdn.jsdelivr.net/npm/[email protected]/umd/index.js#sha256-w7NPLp9edNTX1k4BysegwBlUxsQGQU1CGFx7U9aHXd8=",
            ],
        },
        streamSaver: {
            isLoaded: () => typeof streamSaver !== "undefined",
            urls: [
                "https://cdn.jsdelivr.net/gh/Qalxry/csdn2md/plugins/streamSaver.min.js#sha256-VxQm++CYEdHipBjKWh4QQHHOYZmyo8F/7dJQxG11xFM=",
                "https://cdn.jsdmirror.com/gh/Qalxry/csdn2md/plugins/streamSaver.min.js#sha256-VxQm++CYEdHipBjKWh4QQHHOYZmyo8F/7dJQxG11xFM=",
                "https://use.sevencdn.com/npm/[email protected]/StreamSaver.min.js",
                "https://cdn.jsdelivr.net/npm/[email protected]/StreamSaver.min.js",
                "https://cdn.jsdmirror.com/npm/[email protected]/StreamSaver.min.js",
            ],
        },
    };

    // 动态插入脚本
    function loadScript(src) {
        return new Promise((resolve, reject) => {
            const s = document.createElement("script");
            let hash = src.match(/#(.*)$/)?.[1];
            s.src = src;
            if (hash) {
                s.setAttribute("integrity", hash);
                s.setAttribute("crossorigin", "anonymous");
            }
            s.onload = () => {
                resolve();
            };
            s.onerror = () => reject(new Error(`Failed to load ${src.slice(0, 100)}`));
            document.head.appendChild(s);
        });
    }

    // 如果全局对象不存在,就按顺序尝试加载备用源
    (async () => {
        for (const [libName, libData] of Object.entries(libsToLoad)) {
            if (!libData.isLoaded()) {
                console.warn(`${libName} not found, loading from additional sources...`);
                for (const url of libData.urls) {
                    try {
                        await loadScript(url);
                        // 检查是否加载成功
                        if (!libData.isLoaded()) {
                            throw new Error(`not loaded after script injection`);
                        }
                        console.info(`${libName} loaded successfully from ${url}`);
                        break;
                    } catch (e) {
                        console.error(`Failed to load ${libName} from ${url}:`, e);
                    }
                }
            } else {
                console.info(`${libName} is already loaded.`);
            }
        }
    })();

    /**
     * 模块: 工具函数
     * 提供各种辅助功能的工具函数集合
     */
    const Utils = {
        /**
         * 清除字符串中的特殊字符
         * @param {string} str - 输入字符串
         * @returns {string} 清理后的字符串
         */
        clearSpecialChars(str) {
            return str
                .replaceAll(/[\s]{2,}/g, "")
                .replaceAll(
                    /[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF\u00AD\u034F\u061C\u180E\u2800\u3164\uFFA0\uFFF9-\uFFFB]/g,
                    ""
                )
                .replaceAll("⎧", "")
                .replaceAll("⎨", "{")
                .replaceAll("⎩", "")
                .replaceAll("⎫", "")
                .replaceAll("⎬", "}")
                .replaceAll("⎭", "")
                .replaceAll("⎡", "[")
                .replaceAll("⎢", "")
                .replaceAll("⎣", "")
                .replaceAll("⎤", "]")
                .replaceAll("⎥", "")
                .replaceAll("⎦", "");
        },

        /**
         * 根据长度特征清除字符串中开头的杂乱字符
         * @param {string} str - 输入字符串
         * @returns {string} 清理后的字符串
         */
        clearKatexMathML(str) {
            const strSplit = str.split(/(?=.*\n)(?=.* )[\s\n]{10,}/);
            let maxLen = 0;
            let maxStr = "";
            for (const item of strSplit) {
                if (item.length > maxLen) {
                    maxLen = item.length;
                    maxStr = item;
                }
            }
            return maxStr;
        },

        /**
         * 清理URL中的参数和锚点
         * @param {string} url - 输入URL
         * @returns {string} 清理后的URL
         */
        clearUrl(url) {
            return url.replaceAll(/[?#@!$&'()*+,;=].*$/g, "");
        },

        /**
         * 将文件名转换为安全的文件名
         * @param {string} filename - 原始文件名
         * @returns {string} 安全的文件名
         */
        safeFilename(filename) {
            return filename.replaceAll(/[\\/:*?"<>|]/g, "_");
        },

        /**
         * 压缩HTML内容,移除多余的空白和换行符
         * @param {string} html - 输入的HTML字符串
         * @returns {string} 压缩后的HTML字符串
         */
        shrinkHtml(html) {
            return html
                .replaceAll(/>\s+</g, "><") // 去除标签之间的空白
                .replaceAll(/\s{2,}/g, " ") // 多个空格压缩成一个
                .replaceAll(/^\s+|\s+$/g, ""); // 去除首尾空白
        },

        /**
         * 将SVG图片转换为Base64编码的字符串
         * @param {string} svgText - SVG图片的文本内容
         * @returns {string} Base64编码的字符串
         */
        svgToBase64(svgText) {
            const uint8Array = new TextEncoder().encode(svgText);
            const binaryString = uint8Array.reduce((data, byte) => data + String.fromCharCode(byte), "");
            return btoa(binaryString);
        },

        formatSeconds(seconds) {
            const hrs = Math.floor(seconds / 3600);
            const mins = Math.floor((seconds % 3600) / 60);
            const secs = Math.floor(seconds % 60);
            const pad = (num) => num.toString().padStart(2, "0");
            return `${pad(hrs)}:${pad(mins)}:${pad(secs)}`;
        },

        async parallelPool(array, iteratorFn, poolLimit = 10) {
            const ret = []; // 存储所有任务
            const executing = []; // 存储正在执行的任务
            let index = 0;
            for (const item of array) {
                const currentIndex = index++;
                const p = new Promise(async (resolve, reject) => {
                    try {
                        await iteratorFn(item, currentIndex);
                        resolve();
                    } catch (error) {
                        reject(error);
                    }
                });
                ret.push(p);

                if (poolLimit <= array.length) {
                    const e = p.finally(() => executing.splice(executing.indexOf(e), 1));
                    executing.push(e);

                    if (executing.length >= poolLimit) {
                        await Promise.race(executing);
                    }
                }
            }

            return Promise.all(ret);
        },

        /**
         * 计算字符串的简单哈希值
         * @param {string} str - 输入字符串
         * @param {number} length - 返回的16进制字符串长度,默认为8
         * @returns {string} 指定长度的16进制哈希字符串
         */
        simpleHash(str, length = 8) {
            let hash = 0;
            if (str.length === 0) return "0".repeat(length);

            for (let i = 0; i < str.length; i++) {
                const char = str.charCodeAt(i);
                hash = (hash << 5) - hash + char;
                hash = hash & hash; // 转换为32位整数
            }

            // 转换为16进制并确保为正数
            let hexHash = Math.abs(hash).toString(16);

            // 如果长度不够,重复哈希直到达到要求长度
            while (hexHash.length < length) {
                hash = (hash << 5) - hash + hash;
                hash = hash & hash;
                hexHash += Math.abs(hash).toString(16);
            }

            // 截取到指定长度
            return hexHash.substring(0, length);
        },

        formatFileSize(bytes) {
            if (bytes === 0) return "0 Bytes";
            const k = 1024;
            const sizes = ["Bytes", "KB", "MB", "GB"];
            const i = Math.floor(Math.log(bytes) / Math.log(k));
            return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
        },
    };

    /**
     * 模块: 锁管理
     * 处理异步操作锁
     */
    class ReentrantAsyncLock {
        /**
         * 创建一个可重入异步锁
         * @param {boolean} enableReentrant - 是否启用重入功能
         */
        constructor(enableReentrant = true) {
            this.queue = [];
            this.locked = false;
            this.owner = null; // 记录锁的持有者,用于重入
            this.enableReentrant = enableReentrant;
        }

        /**
         * 获取锁
         * @param {any} ownerId - 锁持有者的标识
         * @returns {Promise<void>}
         */
        async acquire(ownerId = null) {
            if (this.locked) {
                // 如果允许重入,且当前持有者是ownerId,则直接返回
                if (this.enableReentrant && this.owner === ownerId) {
                    return;
                }
                // 否则加入队列等待
                await new Promise((resolve) => this.queue.push(resolve));
            }
            this.locked = true;
            this.owner = ownerId;
        }

        /**
         * 释放锁
         * @param {any} ownerId - 锁持有者的标识
         */
        release(ownerId) {
            if (this.enableReentrant && this.owner !== ownerId) {
                throw new Error("Cannot release a lock you do not own");
            }
            this.locked = false;
            this.owner = null;
            if (this.queue.length > 0) {
                const resolve = this.queue.shift();
                resolve();
                this.locked = true;
                this.owner = ownerId; // 继续持有锁
            }
        }
    }
    /**
     * 模块: UI管理
     * 处理界面相关的功能,支持多种输入类型和分组
     */
    class UIManager {
        /**
         * 创建UI管理器
         **/
        constructor() {
            /** @type {ConfigManager} */
            this.configManager = new ConfigManager(); // 配置管理
            /** @type {ArticleDownloader} */
            this.downloadManager = new ArticleDownloader(); // 下载管理器
            this.downloadManager.setUIManager(this); // 设置UI与下载管理器的双向引用
            this.isDragging = 0;
            this.offsetX = 0;
            this.offsetY = 0;
            this.container = null;
            this.contentBox = null;
            this.mainButton = null;
            this.floatWindow = null;
            this.downloadButton = null;
            this.gotoRepoButton = null;
            this.isOpen = false;
            this.repo_url = "https://github.com/Qalxry/csdn2md";

            // 初始化
            this.initStyles();
            this.initUI();
            this.setupEventListeners();
            this.dialogQueue = [];
            this.isDialogActive = false;
            this.updateAllOptions();
        }

        /**
         * 初始化UI样式
         */
        initStyles() {
            GM_addStyle(`
            :root {
                --tm_ui-linear-gradient: linear-gradient(135deg, #12c2e9 0%, #c471ed 50%, #f64f59 100%);
            }

            .tm_floating-container {
                position: fixed;
                bottom: 20px;
                right: 20px;
                z-index: 9999;
                transform-origin: bottom right;
                font-size: 13px;
            }

            .tm_main-button {
                width: 50px;
                height: 50px;
                border-radius: 50%;
                background: var(--tm_ui-linear-gradient);
                box-shadow: 0 0 20px rgba(0,0,0,0.2);
                border: none;
                color: white;
                font-size: 20px;
                cursor: pointer;
                transition: all 0.3s ease;
                display: flex;
                align-items: center;
                justify-content: center;
            }

            .tm_content-box {
                background: linear-gradient(45deg, #ffffff, #f8f9fa);
                border-radius: 13px;
                padding: 13px;
                width: 360px;
                box-shadow: 0 7px 20px rgba(0,0,0,0.15);
                margin-bottom: 13px;
                opacity: 0;
                transform: scale(0);
                transform-origin: bottom right;
                transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
                position: absolute;
                bottom: 100%;
                right: 0;
            }

            .tm_content-box.open {
                opacity: 1;
                transform: scale(1);
            }

            .tm_complex-content {
                display: flex;
                flex-direction: column;
                gap: 10px;
            }

            #myFloatWindow {
                width: 100%;
                position: relative;
            }

            .tm_ui-options-container {
                max-height: 480px;
                overflow-y: auto;
                padding-right: 5px;
                margin: 10px 0;
                scrollbar-width: thin;
                scrollbar-color: rgba(0,0,0,0.3) transparent;
                position: relative;
            }

            .tm_ui-options-disabled-overlay {
                position: absolute;
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                background: rgba(240,240,240,0.7);
                z-index: 10;
                display: none;
                border-radius: 8px;
            }

            .tm_ui-options-container::-webkit-scrollbar {
                width: 4px;
            }

            .tm_ui-options-container::-webkit-scrollbar-thumb {
                background-color: rgba(0,0,0,0.3);
                border-radius: 2px;
            }

            .tm_ui-option-group {
                border: 1px solid rgba(0,0,0,0.08);
                border-radius: 6px;
                padding: 0;
                margin-bottom: 7px;
                background: #ffffff;
                box-shadow: 0 1px 2px rgba(0,0,0,0.05);
                overflow: hidden;
                transition: all 0.3s ease;
            }

            .tm_ui-option-group-header {
                padding: 7px 8px;
                cursor: pointer;
                background: rgba(0,0,0,0.02);
                border-bottom: 1px solid rgba(0,0,0,0.08);
                display: flex;
                align-items: center;
                justify-content: space-between;
                user-select: none;
                transition: background 0.2s ease;
            }
            .tm_ui-option-group-header:hover {
                background: rgba(0, 123, 255, 0.05);
            }

            .tm_ui-option-group-title {
                font-weight: 800;
                color: #444;
                flex: 1;
                font-size: 12px;
            }

            .tm_ui-option-group-content {
                padding: 8px;
                overflow: hidden;
                max-height: 0px; /* 设置足够大的展开高度 */
                transition: max-height 0.3s cubic-bezier(0.33, 1, 0.68, 1), padding 0.3s ease, opacity 0.3s ease;
                opacity: 1;
            }

            .tm_ui-option-group-collapsed .tm_ui-option-group-content {
                max-height: 0px;
                padding-top: 0;
                padding-bottom: 0;
                opacity: 0;
            }

            .tm_ui-option-item {
                margin-bottom: 8px;
                display: flex;
                align-items: center;
                flex-wrap: wrap;
            }

            .tm_ui-option-item:last-child {
                margin-bottom: 0;
            }

            .tm_ui-option-label {
                margin-left: 5px;
                flex: 1;
                min-width: 80px;
                font-size: 12px;
            }

            .tm_ui-input-container {
                display: flex;
                align-items: center;
                flex: 1;
                max-width: 100px;
            }

            .tm_ui-tooltip {
                position: relative;
                display: inline-flex;
                margin-left: 6px;
                cursor: help;
            }

            .tm_ui-tooltip-icon {
                display: inline-flex;
                align-items: center;
                justify-content: center;
                width: 12px;
                height: 12px;
                border-radius: 50%;
                background-color: rgba(0,0,0,0.2);
                color: white;
                font-size: 9px;
                font-weight: bold;
                user-select: none; /* 设置不可选择文本 */
            }

            .tm_ui-tooltip-text {
                visibility: hidden;
                background-color: rgba(0,0,0,0.7);
                color: #fff;
                text-align: left;
                border-radius: 4px;
                padding: 5px 7px;
                position: absolute;
                z-index: 10000;
                font-size: 11px;
                opacity: 0;
                transition: opacity 0.3s;
                line-height: 1.4;
                pointer-events: none;
                box-shadow: 0 2px 8px rgba(0,0,0,0.2);
                top: -5px;
                right: 110%;
                white-space: nowrap;
                overflow-wrap: break-word;
            }

            .tm_ui-tooltip:hover .tm_ui-tooltip-text {
                visibility: visible;
                opacity: 1;
            }

            .tm_ui-switch {
                --tm_ui-width: 28px;
                --tm_ui-height: 14px;
                --tm_ui-padding: 2px;
                --tm_ui-duration: 0.2s;
                --tm_ui-color-on:rgb(76, 97, 175);
                --tm_ui-color-off: #e0e0e0;
                --tm_ui-color-knob: #ffffff;
                --tm_ui-shadow: 0 2px 5px rgba(0,0,0,0.2);
                --tm_ui-knob-size: calc(var(--tm_ui-height) - var(--tm_ui-padding) * 2);
                display: inline-block;
                position: relative;
                width: var(--tm_ui-width);
                height: var(--tm_ui-height);
            }
            .tm_ui-switch input {
                opacity: 0;
                width: 0;
                height: 0;
            }
            .tm_ui-switch-slider {
                position: absolute;
                cursor: pointer;
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                background-color: var(--tm_ui-color-off);
                transition: background-color var(--tm_ui-duration) ease;
                border-radius: var(--tm_ui-height);
                box-shadow: var(--tm_ui-shadow) inset;
            }
            .tm_ui-switch-slider:before {
                position: absolute;
                content: "";
                height: var(--tm_ui-knob-size);
                width: var(--tm_ui-knob-size);
                left: var(--tm_ui-padding);
                bottom: var(--tm_ui-padding);
                background-color: var(--tm_ui-color-knob);
                transition: transform var(--tm_ui-duration) ease;
                border-radius: 50%;
                box-shadow: var(--tm_ui-shadow);
            }
            .tm_ui-switch input:checked + .tm_ui-switch-slider {
                background-color: var(--tm_ui-color-on);
            }
            .tm_ui-switch input:checked + .tm_ui-switch-slider:before {
                transform: translateX(calc(var(--tm_ui-width) - var(--tm_ui-height)));
            }
            .tm_ui-switch input:disabled + .tm_ui-switch-slider {
                opacity: 0.6;
                cursor: not-allowed;
            }

            .tm_ui-range-container {
                display: flex;
                align-items: center;
                width: 100%;
                max-width: 120px;
                gap: 5px;
            }

            .tm_ui-range-input {
                flex: 1;
                width: 90px;
            }

            .tm_ui-range-value {
                width: 25px;
                text-align: center;
                border: 1px solid #ccc;
                border-radius: 2px;
                padding: 1px;
                font-size: 10px;
            }

            .tm_ui-select {
                padding: 3px;
                border-radius: 3px;
                border: 1px solid #ccc;
                background-color: white;
                width: 100%;
                font-size: 11px;
            }

            .tm_ui-input-number, .tm_ui-input-text {
                padding: 3px;
                border-radius: 3px;
                border: 1px solid #ccc;
                width: 100%;
                font-size: 11px;
            }

            .tm_ui-buttons-container {
                display: flex;
                gap: 7px;
                justify-content: center;
                margin-top: 3px;
            }

            #myDownloadButton, #myResetButton {
                text-align: center;
                padding: 5px 8px;
                background: var(--tm_ui-linear-gradient);
                color: white;
                cursor: pointer;
                transition: all 0.3s ease;
                border-radius: 3px;
                border: none;
                font-size: 11px;
            }

            #myDownloadButton {
                width: 100%;
                margin-bottom: 3px;
                padding: 8px;
            }
            
            #myResetButton {
                flex: 1;
            }
            
            #myGotoRepoButton {
                flex: 3;
            }

            .collapse-icon {
                width: 12px;
                height: 12px;
                transition: transform 0.3s ease;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            
            .tm_ui-option-group-collapsed .collapse-icon {
                transform: rotate(180deg);
            }

            #myDownloadButton:hover, #myResetButton:hover, #myGotoRepoButton:hover {
                transform: scale(1.02);
                box-shadow: 0 1px 5px rgba(0,0,0,0.15);
            }

            #myDownloadButton:disabled, #myResetButton:disabled {
                background: gray;
                color: #aaa;
                cursor: not-allowed;
                transform: none;
                box-shadow: none;
            }

            #myGotoRepoButton {
                background: #000000;
                color: #ffffff;
                text-align: center;
                padding: 5px 8px;
                cursor: pointer;
                transition: all 0.3s ease;
                border-radius: 3px;
                border: none;
                font-size: 11px;
            }
            #myGotoRepoButton:active {
                transform: scale(1);
            }
            #myGotoRepoButton:hover .goto-repo-btn-icon {
                fill: #ffff00;
                transform: scale(1.02);
                rotate: 360deg;
                filter: drop-shadow(0 0 5px rgba(255, 208, 0, 0.8))
                        drop-shadow(0 0 10px rgba(255, 208, 0, 0.6));
            }
            #myGotoRepoButton:hover .goto-repo-btn-text {
                filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.2))
                        drop-shadow(0 0 10px rgba(255, 255, 255, 0.4));
            }
            #myGotoRepoButton .goto-repo-btn-text {
                transition: all 1s ease;
            }
            #myGotoRepoButton .goto-repo-btn-icon {
                display: inline-block;
                width: 12px;
                height: 12px;
                transition: all 1s ease;
            }
        `);
        }

        /**
         * 初始化UI元素
         */
        initUI() {
            // 创建悬浮容器
            this.container = document.createElement("div");
            this.container.className = "tm_floating-container";
            this.container.id = "draggable";

            // 创建主按钮
            this.mainButton = document.createElement("button");
            this.mainButton.className = "tm_main-button";
            this.mainButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#FFFFFF"><path d="M480-337q-8 0-15-2.5t-13-8.5L308-492q-12-12-11.5-28t11.5-28q12-12 28.5-12.5T365-549l75 75v-286q0-17 11.5-28.5T480-800q17 0 28.5 11.5T520-760v286l75-75q12-12 28.5-11.5T652-548q11 12 11.5 28T652-492L508-348q-6 6-13 8.5t-15 2.5ZM240-160q-33 0-56.5-23.5T160-240v-80q0-17 11.5-28.5T200-360q17 0 28.5 11.5T240-320v80h480v-80q0-17 11.5-28.5T760-360q17 0 28.5 11.5T800-320v80q0 33-23.5 56.5T720-160H240Z"/></svg>`;

            // 创建内容区域
            this.contentBox = document.createElement("div");
            this.contentBox.className = "tm_content-box";

            // 创建复杂内容
            this.contentBox.innerHTML = `
            <div class="tm_complex-content" id="tmComplexContent"></div>
        `;

            // 组装元素
            this.container.appendChild(this.contentBox);
            this.container.appendChild(this.mainButton);
            document.body.appendChild(this.container);

            // 还原之前保存的位置
            const savedTop = GM_getValue("draggableTop");
            if (savedTop) {
                this.container.style.top = Math.min(window.innerHeight - 50, parseInt(savedTop)) + "px";
            }

            // 创建浮动窗口
            this.createFloatWindow();
        }

        /**
         * 创建浮动窗口和选项
         */
        createFloatWindow() {
            // 创建悬浮窗
            this.floatWindow = document.createElement("div");
            this.floatWindow.style.display = "flex";
            this.floatWindow.style.flexDirection = "column";
            this.floatWindow.id = "myFloatWindow";

            // 创建下载按钮
            this.downloadButton = document.createElement("button");
            this.downloadButton.innerHTML = "点击下载Markdown<br>(支持文章、专栏、用户全部文章页面)";
            this.downloadButton.id = "myDownloadButton";
            this.floatWindow.appendChild(this.downloadButton);

            // 创建选项容器,设置为可滚动
            const optionContainer = document.createElement("div");
            optionContainer.className = "tm_ui-options-container";
            this.floatWindow.appendChild(optionContainer);

            // 创建选项容器禁用遮罩
            const overlay = document.createElement("div");
            overlay.className = "tm_ui-options-disabled-overlay";
            optionContainer.appendChild(overlay);

            // 添加选项分组
            this.createOptionGroups(optionContainer);

            // 创建底部按钮容器
            const buttonsContainer = document.createElement("div");
            buttonsContainer.className = "tm_ui-buttons-container";
            this.floatWindow.appendChild(buttonsContainer);

            // 创建恢复默认设置按钮
            this.resetButton = document.createElement("button");
            this.resetButton.innerHTML = "恢复默认";
            this.resetButton.id = "myResetButton";
            buttonsContainer.appendChild(this.resetButton);

            // 创建去GitHub按钮
            this.gotoRepoButton = document.createElement("button");
            // this.gotoRepoButton.innerHTML = "前往 GitHub 给作者点个 Star ⭐";
            this.gotoRepoButton.innerHTML = `
                <span class="goto-repo-btn-text">前往 GitHub 给作者点个 Star</span>
                <svg aria-hidden="true" fill="currentColor" viewBox="0 0 47.94 47.94" xmlns="http://www.w3.org/2000/svg"
                     width="12px" height="12px" class="goto-repo-btn-icon">
                    <path
                    d="M26.285,2.486l5.407,10.956c0.376,0.762,1.103,1.29,1.944,1.412l12.091,1.757
                    c2.118,0.308,2.963,2.91,1.431,4.403l-8.749,8.528c-0.608,0.593-0.886,1.448-0.742,2.285l2.065,12.042
                    c0.362,2.109-1.852,3.717-3.746,2.722l-10.814-5.685c-0.752-0.395-1.651-0.395-2.403,0l-10.814,5.685
                    c-1.894,0.996-4.108-0.613-3.746-2.722l2.065-12.042c0.144-0.837-0.134-1.692-0.742-2.285l-8.749-8.528
                    c-1.532-1.494-0.687-4.096,1.431-4.403l12.091-1.757c0.841-0.122,1.568-0.65,1.944-1.412l5.407-10.956
                    C22.602,0.567,25.338,0.567,26.285,2.486z"
                    ></path>
                </svg>
            `;
            this.gotoRepoButton.id = "myGotoRepoButton";
            buttonsContainer.appendChild(this.gotoRepoButton);

            // 将浮窗添加到内容区
            document.getElementById("tmComplexContent").appendChild(this.floatWindow);
        }

        /**
         * 创建选项分组
         * @param {HTMLElement} container - 父容器
         */
        createOptionGroups(container) {
            // 下载设置组
            const downloadGroup = this.createOptionGroup(container, "基础下载设置", true);
            this.addBoolOption({
                id: "parallelDownload",
                label: "批量并行下载模式",
                defaultValue: true,
                container: downloadGroup,
                tooltip: "使用Iframe,能够获取JS动态的内容",
            });
            this.addBoolOption({
                id: "fastDownload",
                label: "快速模式(内存占用低,建议文章较多时启用)",
                defaultValue: false,
                container: downloadGroup,
                tooltip: `使用Fetch API,速度快,但<span style="color:red">无法获取JS动态加载内容</span>。<br>如果不启用,则使用iframe下载,容易内存溢出崩溃。<br><center>不启用时占用内存 ≈ 40MB * 文章数量</center><center>启用后占用内存 ≈ 40MB</center>文章较多时建议启用快速模式。下载后需要<span style="color:red">仔细检查内容<br>是否完整</span>,一般不会有问题。<br>另外,单篇文章直接读取当前页面,不受此选项影响。`,
            });
            this.addBoolOption({
                id: "zipCategories",
                label: "下载为压缩包",
                defaultValue: true,
                container: downloadGroup,
                constraints: {
                    false: [{ id: "saveWebImages", value: false }],
                },
            });
            this.addBoolOption({
                id: "saveWebImages",
                label: "将图片保存至本地",
                defaultValue: true,
                container: downloadGroup,
                tooltip: "默认保存到和MD文件同名的文件夹中",
                constraints: {
                    true: [{ id: "zipCategories", value: true }],
                    false: [{ id: "saveAllImagesToAssets", value: false }],
                },
            });
            this.addBoolOption({
                id: "saveAllImagesToAssets",
                label: "图片保存至assets文件夹",
                defaultValue: true,
                container: downloadGroup,
                tooltip: "如不启用,则保存到和MD文件同名的文件夹中",
                constraints: {
                    true: [
                        { id: "zipCategories", value: true },
                        { id: "saveWebImages", value: true },
                    ],
                },
            });
            this.addBoolOption({
                id: "enableCustomFileName",
                label: "启用批量下载文件名模板",
                defaultValue: true,
                container: downloadGroup,
                tooltip: "启用后,批量下载的文件名将根据下方模板生成",
            });
            this.addStringOption({
                id: "customFileNamePattern",
                label: "批量下载文件名模板",
                defaultValue: "{no}_{title}",
                container: downloadGroup,
                tooltip: "可用变量:{title}、{author}、{index}、{no}(有前导0)",
            });

            const advancedDownloadGroup = this.createOptionGroup(container, "高级下载设置");
            this.addIntOption({
                id: "maxConcurrentDownloads",
                label: "最大并行解析数",
                defaultValue: 4,
                container: advancedDownloadGroup,
                min: 1,
                max: 128,
                step: 1,
                tooltip: "越小越稳定,过大容易风控、内存溢出",
            });
            this.addIntOption({
                id: "delayBetweenDownloads",
                label: "下载间隔(毫秒)",
                defaultValue: 100,
                container: advancedDownloadGroup,
                min: 0,
                max: 60000,
                step: 1,
                tooltip:
                    "每次下载文章之间的延时,单位毫秒。<br>在并行时每个worker的间隔是独立的。<br>用于进一步减慢串行下载避免风控(放到最慢)",
            });
            this.addIntOption({
                id: "downloadAssetRetryCount",
                label: "下载资源失败重试次数",
                defaultValue: 3,
                container: advancedDownloadGroup,
                min: 0,
                max: 32,
                step: 1,
                tooltip: "下载网页、图片等失败时重试次数,0表示不重试",
            });
            this.addIntOption({
                id: "downloadAssetRetryDelay",
                label: "下载资源失败重试延时(毫秒)",
                defaultValue: 1000,
                container: advancedDownloadGroup,
                min: 0,
                max: 60000,
                step: 1,
                tooltip: "下载网页、图片等失败时重试前的延时,单位毫秒。<br>避免过快重试导致服务器风控",
            });
            this.addIntOption({
                id: "startArticleIndex",
                label: "从第几篇文章开始下载",
                defaultValue: 1,
                container: advancedDownloadGroup,
                min: 1,
                max: 10000,
                step: 1,
                tooltip: "从第几篇文章开始下载,1表示第一篇<br>避免一次下载多篇文章时风控,用于分批下载",
            });
            this.addIntOption({
                id: "endArticleIndex",
                label: "下载到第几篇文章",
                defaultValue: 10000,
                container: advancedDownloadGroup,
                min: 1,
                max: 10000,
                step: 1,
                tooltip: "下载到第几篇文章,超出范围则下载到最后一篇<br>避免一次下载多篇文章时风控,用于分批下载",
            });
            this.addSelectOption({
                id: "zipLibrary",
                label: "压缩文件时使用的库",
                defaultValue: "fflate",
                container: advancedDownloadGroup,
                options: [
                    { value: "fflate", label: "fflate(默认,更快)" },
                    { value: "jszip", label: "JSZip(备选,较慢)" },
                ],
                tooltip: "如果提示没有找到fflate,请尝试切换到jszip。<br>流式压缩下载时只能使用fflate。",
            });
            this.addBoolOption({
                id: "enableStreaming",
                label: "启用流式压缩下载(节省内存,实验性功能)",
                defaultValue: false,
                container: advancedDownloadGroup,
                tooltip: "稍微减少内存占用。如下载失败,请关闭此选项。",
                constraints: {
                    true: [{ id: "zipCategories", value: true }],
                },
            });

            // 文章内容组
            const contentGroup = this.createOptionGroup(container, "文章内容设置");
            this.addBoolOption({
                id: "addArticleInfoInYaml",
                label: "添加文章元信息",
                defaultValue: false,
                container: contentGroup,
                tooltip: "以YAML格式添加,对于转Hexo博客比较有用",
                constraints: {
                    true: [{ id: "mergeArticleContent", value: false }],
                },
            });
            this.addBoolOption({
                id: "addArticleTitleToMarkdown",
                label: "添加文章标题",
                defaultValue: true,
                container: contentGroup,
                tooltip: "以一级标题形式添加",
            });
            this.addBoolOption({
                id: "addArticleInfoInBlockquote",
                label: "添加文章阅读量、点赞等信息",
                defaultValue: true,
                container: contentGroup,
                tooltip: "以引用块形式添加",
            });
            this.addBoolOption({
                id: "removeCSDNSearchLink",
                label: "移除CSDN搜索链接",
                defaultValue: true,
                container: contentGroup,
            });
            this.addBoolOption({
                id: "enableColorText",
                label: "启用彩色文字",
                defaultValue: true,
                container: contentGroup,
                tooltip: "使用&lt;span&gt;格式实现彩色文字",
            });
            this.addBoolOption({
                id: "enableImageSize",
                label: "启用图片宽高属性",
                defaultValue: true,
                container: contentGroup,
                tooltip:
                    "仅当网页提供宽高属性时生效。<br>如果启用,则具有宽高的图片会以&lt;img&gt;标签<br>插入文本中。如果不启用,则会以![]()格式插入。",
            });
            this.addBoolOption({
                id: "forceImageCentering",
                label: "全部图片居中",
                defaultValue: false,
                container: contentGroup,
                tooltip:
                    "忽略网页原有的图片对齐方式,全部居中。<br>图片靠左对齐是通过在图片前添加空格实现的,<br>不加空格则Typora显示为居中。",
            });
            this.addBoolOption({
                id: "enableMarkdownEscape",
                label: "启用Markdown特殊字符转义",
                defaultValue: false,
                container: contentGroup,
                tooltip: "把会影响Markdown解析的字符用\\转义",
            });
            this.addStringOption({
                id: "markdownEscapePattern",
                label: "需要转义的Markdown字符",
                defaultValue: "`*_[]{}()#+-.!",
                container: contentGroup,
                tooltip: "填入字符即可,这不是正则表达式,注意不要空格",
            });

            // 批量文章处理组
            const batchGroup = this.createOptionGroup(container, "合并文章设置");
            this.addBoolOption({
                id: "mergeArticleContent",
                label: "启用合并文章",
                defaultValue: false,
                container: batchGroup,
                tooltip: "将多篇文章保存为单个MD文件",
                constraints: {
                    true: [{ id: "addArticleInfoInYaml", value: false }],
                },
            });
            this.addBoolOption({
                id: "addSerialNumberToTitle",
                label: "添加序号到文章标题前",
                defaultValue: false,
                container: batchGroup,
                tooltip: "在合并文章时可能有用",
            });
            this.addBoolOption({
                id: "addArticleInfoInBlockquote_batch",
                label: "合并文章时添加栏目总信息",
                defaultValue: true,
                container: batchGroup,
                tooltip: "以引用块形式添加栏目总阅读量、点赞等",
            });
        }

        /**
         * 创建选项分组
         * @param {HTMLElement} container - 父容器
         * @param {string} title - 分组标题
         * @param {boolean} expanded - 是否展开(默认折叠)
         * @returns {HTMLElement} - 创建的分组元素内容容器
         */
        createOptionGroup(container, title, expanded = false) {
            const group = document.createElement("div");
            group.className = "tm_ui-option-group" + (expanded ? "" : " tm_ui-option-group-collapsed");

            // 创建分组头部(可点击折叠/展开)
            const header = document.createElement("div");
            header.className = "tm_ui-option-group-header";

            const titleElem = document.createElement("div");
            titleElem.className = "tm_ui-option-group-title";
            titleElem.textContent = title;

            // 创建折叠图标
            const icon = document.createElement("span");
            icon.className = "collapse-icon";
            icon.innerHTML = `<svg width="12" height="12" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path d="M4 6L8 10L12 6" stroke="#555555" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
        </svg>`;

            header.appendChild(titleElem);
            header.appendChild(icon);
            group.appendChild(header);

            // 创建内容区域
            const content = document.createElement("div");
            content.className = "tm_ui-option-group-content";
            group.appendChild(content);

            // 添加点击事件,实现折叠/展开功能
            header.addEventListener("click", () => {
                // 展开,则设置max-height为scrollHeight,折叠则为0
                if (group.classList.contains("tm_ui-option-group-collapsed")) {
                    group.classList.toggle("tm_ui-option-group-collapsed");
                    content.style.maxHeight = content.scrollHeight + 16 + "px";
                } else {
                    group.classList.toggle("tm_ui-option-group-collapsed");
                    content.style.maxHeight = "0px";
                }
            });

            if (expanded) {
                setTimeout(() => {
                    content.style.maxHeight = content.scrollHeight + 16 + "px"; // 16是padding的总和
                }, 100); // 确保在DOM渲染后执行
            }

            container.appendChild(group);
            return content;
        }

        /**
         * 添加布尔选项(复选框)
         * @param {Object} option - 选项对象
         * @param {string} option.id - 选项ID
         * @param {string} option.label - 选项标签
         * @param {boolean} option.defaultValue - 默认值
         * @param {HTMLElement} option.container - 父容器
         * @param {string} [option.tooltip=""] - 提示信息(可选)
         * @param {Object} [option.constraints={}] - 约束条件(可选)
         */
        addBoolOption(option) {
            const { id, label, defaultValue, container, tooltip = "", constraints = {} } = option;
            // 注册(不可用)到配置管理器
            this.configManager.register(id, defaultValue);

            const optionItem = document.createElement("div");
            optionItem.className = "tm_ui-option-item";

            const checkbox = document.createElement("input");
            checkbox.type = "checkbox";
            checkbox.id = id;
            checkbox.checked = this.configManager.get(id);

            const checkboxWrapper = document.createElement("label");
            checkboxWrapper.className = "tm_ui-switch";
            checkboxWrapper.appendChild(checkbox);
            const slider = document.createElement("span");
            slider.className = "tm_ui-switch-slider";
            checkboxWrapper.appendChild(slider);

            const labelElem = document.createElement("label");
            labelElem.htmlFor = id;
            labelElem.className = "tm_ui-option-label";
            labelElem.textContent = label;

            optionItem.appendChild(labelElem);
            optionItem.appendChild(checkboxWrapper);

            // 添加提示
            if (tooltip) {
                optionItem.appendChild(this.createTooltip(tooltip));
            } else {
                const tooltipElem = this.createTooltip("");
                tooltipElem.style.visibility = "hidden"; // 默认隐藏
                tooltipElem.style.opacity = "0"; // 默认透明
                optionItem.appendChild(tooltipElem);
            }

            container.appendChild(optionItem);

            // 事件监听
            checkbox.addEventListener("change", () => {
                this.configManager.set(id, checkbox.checked);

                // 处理约束
                if (checkbox.checked && constraints.true) {
                    for (const constraint of constraints.true) {
                        if (constraint.id !== undefined && constraint.value !== undefined) {
                            this.configManager.set(constraint.id, constraint.value);
                            this.updateOption(constraint.id);
                        }
                    }
                } else if (!checkbox.checked && constraints.false) {
                    for (const constraint of constraints.false) {
                        if (constraint.id !== undefined && constraint.value !== undefined) {
                            this.configManager.set(constraint.id, constraint.value);
                            this.updateOption(constraint.id);
                        }
                    }
                }
            });

            return optionItem;
        }

        /**
         * 添加整数选项(数字输入框)
         * @param {Object} option - 选项对象
         * @param {string} option.id - 选项ID
         * @param {string} option.label - 选项标签
         * @param {number} option.defaultValue - 默认值
         * @param {HTMLElement} option.container - 父容器
         * @param {number} option.min - 最小值
         * @param {number} option.max - 最大值
         * @param {number} option.step - 步长
         * @param {string} [option.tooltip=""] - 提示信息
         */
        addIntOption(option) {
            const { id, label, defaultValue, container, min, max, step, tooltip = "" } = option;
            // 注册(不可用)到配置管理器
            this.configManager.register(id, defaultValue);

            const optionItem = document.createElement("div");
            optionItem.className = "tm_ui-option-item";

            const labelElem = document.createElement("label");
            labelElem.className = "tm_ui-option-label";
            labelElem.textContent = label;

            const inputContainer = document.createElement("div");
            inputContainer.className = "tm_ui-input-container";

            const input = document.createElement("input");
            input.type = "number";
            input.className = "tm_ui-input-number";
            input.id = id;
            input.min = min;
            input.max = max;
            input.step = step;
            input.value = this.configManager.get(id);

            inputContainer.appendChild(input);

            optionItem.appendChild(labelElem);
            optionItem.appendChild(inputContainer);

            // 添加提示
            if (tooltip) {
                optionItem.appendChild(this.createTooltip(tooltip));
            }

            container.appendChild(optionItem);

            // 事件监听
            input.addEventListener("change", () => {
                let value = parseInt(input.value);
                if (isNaN(value)) value = defaultValue;
                if (value < min) value = min;
                if (value > max) value = max;

                input.value = value;
                this.configManager.set(id, value);
            });

            return optionItem;
        }

        /**
         * 添加浮点数选项(滑块)
         * @param {Object} option - 选项对象
         * @param {string} option.id - 选项ID
         * @param {string} option.label - 选项标签
         * @param {number} option.defaultValue - 默认值
         * @param {HTMLElement} option.container - 父容器
         * @param {number} option.min - 最小值
         * @param {number} option.max - 最大值
         * @param {number} option.step - 步长
         * @param {string} [option.tooltip=""] - 提示信息
         */
        addFloatOption(option) {
            const { id, label, defaultValue, container, min, max, step, tooltip = "" } = option;
            // 注册(不可用)到配置管理器
            this.configManager.register(id, defaultValue);

            const optionItem = document.createElement("div");
            optionItem.className = "tm_ui-option-item";

            const labelElem = document.createElement("label");
            labelElem.className = "tm_ui-option-label";
            labelElem.textContent = label;

            // 创建滑块容器
            const inputContainer = document.createElement("div");
            inputContainer.className = "tm_ui-input-container";

            const rangeContainer = document.createElement("div");
            rangeContainer.className = "tm_ui-range-container";

            const slider = document.createElement("input");
            slider.type = "range";
            slider.className = "tm_ui-range-input";
            slider.id = id;
            slider.min = min;
            slider.max = max;
            slider.step = step;
            slider.value = this.configManager.get(id);

            const valueDisplay = document.createElement("span");
            valueDisplay.className = "tm_ui-range-value";
            valueDisplay.textContent = slider.value;

            rangeContainer.appendChild(slider);
            rangeContainer.appendChild(valueDisplay);
            inputContainer.appendChild(rangeContainer);

            optionItem.appendChild(labelElem);
            optionItem.appendChild(inputContainer);

            // 添加提示
            if (tooltip) {
                optionItem.appendChild(this.createTooltip(tooltip));
            }

            container.appendChild(optionItem);

            // 事件监听
            slider.addEventListener("input", () => {
                valueDisplay.textContent = slider.value;
                this.configManager.set(id, parseFloat(slider.value));
            });

            return optionItem;
        }

        /**
         * 添加字符串选项(文本框)
         * @param {Object} option - 选项对象
         * @param {string} option.id - 选项ID
         * @param {string} option.label - 选项标签
         * @param {string} option.defaultValue - 默认值
         * @param {HTMLElement} option.container - 父容器
         * @param {string} [option.tooltip=""] - 提示信息
         */
        addStringOption(option) {
            const { id, label, defaultValue, container, tooltip = "" } = option;
            // 注册(不可用)到配置管理器
            this.configManager.register(id, defaultValue);

            const optionItem = document.createElement("div");
            optionItem.className = "tm_ui-option-item";

            const labelElem = document.createElement("label");
            labelElem.className = "tm_ui-option-label";
            labelElem.textContent = label;

            const inputContainer = document.createElement("div");
            inputContainer.className = "tm_ui-input-container";

            const input = document.createElement("input");
            input.type = "text";
            input.className = "tm_ui-input-text";
            input.id = id;
            input.value = this.configManager.get(id);

            inputContainer.appendChild(input);

            optionItem.appendChild(labelElem);
            optionItem.appendChild(inputContainer);

            // 添加提示
            if (tooltip) {
                optionItem.appendChild(this.createTooltip(tooltip));
            }

            container.appendChild(optionItem);

            // 事件监听
            input.addEventListener("change", () => {
                this.configManager.set(id, input.value);
            });

            return optionItem;
        }

        /**
         * 添加下拉选择选项
         * @param {Object} option - 选项对象
         * @param {string} option.id - 选项ID
         * @param {string} option.label - 选项标签
         * @param {string} option.defaultValue - 默认值
         * @param {HTMLElement} option.container - 父容器
         * @param {Array} option.options - 选项数组,格式为[{value: '', label: ''}]
         * @param {string} [option.tooltip=""] - 提示信息
         */
        addSelectOption(option) {
            const { id, label, defaultValue, container, options, tooltip = "" } = option;
            // 注册(不可用)到配置管理器
            this.configManager.register(id, defaultValue);

            const optionItem = document.createElement("div");
            optionItem.className = "tm_ui-option-item";

            const labelElem = document.createElement("label");
            labelElem.className = "tm_ui-option-label";
            labelElem.textContent = label;

            const inputContainer = document.createElement("div");
            inputContainer.className = "tm_ui-input-container";

            const select = document.createElement("select");
            select.className = "tm_ui-select";
            select.id = id;

            // 添加选项
            options.forEach((option) => {
                const optElem = document.createElement("option");
                optElem.value = option.value;
                optElem.textContent = option.label;
                select.appendChild(optElem);
            });

            // 设置当前值
            select.value = this.configManager.get(id);

            inputContainer.appendChild(select);

            optionItem.appendChild(labelElem);
            optionItem.appendChild(inputContainer);

            // 添加提示
            if (tooltip) {
                optionItem.appendChild(this.createTooltip(tooltip));
            }

            container.appendChild(optionItem);

            // 事件监听
            select.addEventListener("change", () => {
                this.configManager.set(id, select.value);
            });

            return optionItem;
        }

        /**
         * 创建提示工具
         * @param {string} text - 提示文本
         * @returns {HTMLElement} 提示元素
         */
        createTooltip(text) {
            const tooltip = document.createElement("div");
            tooltip.className = "tm_ui-tooltip";

            const icon = document.createElement("div");
            icon.className = "tm_ui-tooltip-icon";
            icon.textContent = "?";

            const tooltipText = document.createElement("div");
            tooltipText.className = "tm_ui-tooltip-text";
            tooltipText.innerHTML = text;

            tooltip.appendChild(icon);
            tooltip.appendChild(tooltipText);

            return tooltip;
        }

        /**
         * 更新指定选项的UI状态
         * @param {string} id - 选项ID
         */
        updateOption(id) {
            const element = document.getElementById(id);
            if (!element) return;

            const value = this.configManager.get(id);

            switch (element.type) {
                case "checkbox":
                    element.checked = value;
                    break;
                case "range":
                case "number":
                case "text":
                    element.value = value;
                    break;
                case "select-one":
                    element.value = value;
                    break;
            }
        }

        /**
         * 更新所有选项的状态
         */
        updateAllOptions() {
            for (const id of this.configManager.getAllKeys()) {
                this.updateOption(id);
            }
        }

        /**
         * 设置事件监听器
         */
        setupEventListeners() {
            // 主按钮点击事件
            this.mainButton.addEventListener("click", (e) => {
                e.stopPropagation();
                this.toggleContent();
            });

            // 点击外部区域关闭
            document.addEventListener("click", (e) => {
                if (!this.container.contains(e.target)) {
                    this.closeContent();
                }
            });

            // 阻止内容区域点击关闭
            this.contentBox.addEventListener("click", (e) => {
                e.stopPropagation();
            });

            // 下载按钮点击事件
            this.downloadButton.addEventListener("click", async () => {
                await this.runMain();
            });

            // 默认设置按钮点击事件
            this.resetButton.addEventListener("click", () => {
                this.configManager.resetToDefaults();
                this.updateAllOptions();
                this.showFloatTip("已恢复默认设置", 1500);
            });

            // GitHub按钮点击事件
            this.gotoRepoButton.addEventListener("click", () => {
                window.open(this.repo_url, "_blank");
            });

            // 拖拽功能
            const draggable = document.getElementById("draggable");
            draggable.addEventListener("mousedown", (e) => {
                if (e.target === this.mainButton || this.mainButton.contains(e.target)) {
                    this.isDragging = true;
                    this.offsetX = e.clientX - draggable.offsetLeft;
                    this.offsetY = e.clientY - draggable.offsetTop;
                }
            });

            document.addEventListener("mousemove", (e) => {
                if (this.isDragging) {
                    draggable.style.top =
                        Math.min(window.innerHeight - 100, Math.max(0, e.clientY - this.offsetY)) + "px";
                }
            });

            document.addEventListener("mouseup", () => {
                if (this.isDragging) {
                    this.isDragging = false;
                    GM_setValue("draggableTop", draggable.style.top);
                }
            });

            // 监视页面缩放事件
            window.addEventListener("resize", () => {
                const savedTop = GM_getValue("draggableTop");
                if (savedTop) {
                    this.container.style.top = Math.min(window.innerHeight - 100, parseInt(savedTop)) + "px";
                }
            });

            // 监听窗口聚焦事件
            window.addEventListener("focus", () => {
                this.updateAllOptions();
            });
        }

        /**
         * 切换内容区域显示状态
         */
        toggleContent() {
            this.isOpen = !this.isOpen;
            this.contentBox.classList.toggle("open", this.isOpen);
            this.mainButton.style.transform = this.isOpen ? "scale(1.1) rotate(360deg)" : "scale(1) rotate(0deg)";
        }

        /**
         * 关闭内容区域
         */
        closeContent() {
            this.isOpen = false;
            this.contentBox.classList.remove("open");
            this.mainButton.style.transform = "scale(1) rotate(0deg)";
        }

        /**
         * 启用悬浮窗
         */
        enableFloatWindow() {
            this.downloadButton.disabled = false;
            this.downloadButton.innerHTML = "下载CSDN文章为Markdown<br>(支持专栏、文章、用户全部文章页面)";
            this.resetButton.disabled = false;

            // 启用所有输入元素
            const inputs = this.floatWindow.querySelectorAll("input, select");
            inputs.forEach((input) => {
                input.disabled = false;
            });

            // 隐藏遮罩层
            const overlay = this.floatWindow.querySelector(".tm_ui-options-disabled-overlay");
            function preventScroll(e) {
                e.preventDefault(); // 关键:阻止默认滚动
                e.stopPropagation(); // 可选:阻止事件冒泡
            }
            overlay.removeEventListener("wheel", preventScroll);
            overlay.removeEventListener("touchmove", preventScroll);
            if (overlay) overlay.style.display = "none";
        }

        /**
         * 禁用悬浮窗
         */
        disableFloatWindow() {
            this.downloadButton.innerHTML = "正在下载,请稍候...";
            this.downloadButton.disabled = true;
            this.resetButton.disabled = true;

            // 禁用所有输入元素
            const inputs = this.floatWindow.querySelectorAll("input, select");
            inputs.forEach((input) => {
                input.disabled = true;
            });

            // 显示遮罩层
            const overlay = this.floatWindow.querySelector(".tm_ui-options-disabled-overlay");
            function preventScroll(e) {
                e.preventDefault(); // 关键:阻止默认滚动
                e.stopPropagation(); // 可选:阻止事件冒泡
            }
            overlay.addEventListener("wheel", preventScroll, { passive: false });
            overlay.addEventListener("touchmove", preventScroll, { passive: false });
            if (overlay) overlay.style.display = "block";
        }

        /**
         * 显示悬浮提示
         * @param {string} text - 提示内容
         * @param {number} timeout - 自动关闭时间(毫秒)
         */
        showFloatTip(text, timeout = 0) {
            if (document.getElementById("myInfoFloatTip")) {
                document.getElementById("myInfoFloatTip").innerHTML = text;
            } else {
                const floatTip = document.createElement("div");
                floatTip.style.position = "fixed";
                floatTip.style.top = "40%";
                floatTip.style.left = "50%";
                floatTip.style.transform = "translateX(-50%)";
                floatTip.style.padding = "7px 10px";
                floatTip.style.backgroundColor = "rgba(0, 0, 0, 0.8)";
                floatTip.style.color = "#fff";
                floatTip.style.borderRadius = "3px";
                floatTip.style.boxShadow = "0 2px 8px rgba(0, 0, 0, 0.2)";
                floatTip.style.zIndex = "10000";
                floatTip.style.fontSize = "12px";
                floatTip.innerHTML = text;
                floatTip.id = "myInfoFloatTip";
                document.body.appendChild(floatTip);
            }

            if (timeout > 0) {
                setTimeout(() => {
                    this.hideFloatTip();
                }, timeout);
            }
        }

        /**
         * 显示一个高度可定制的对话框。
         * 这是核心方法,返回一个Promise。
         * @param {object} options - 对话框的配置选项。
         * @param {string} options.title - (可选) 对话框的标题。
         * @param {string} options.message - 对话框的主体信息。
         * @param {Array<object>} options.buttons - 按钮配置数组。
         * @param {string} options.buttons[].text - 按钮上显示的文本。
         * @param {any} options.buttons[].value - 点击按钮后Promise resolve的值。
         * @param {'primary'|'default'|'danger'} [options.buttons[].type='default'] - 按钮类型,用于应用不同样式。
         * @returns {Promise<any>} 当用户点击按钮时,Promise会resolve,并返回对应按钮的value。
         */
        _createDialog(options) {
            return new Promise((resolve) => {
                // 将请求加入队列
                const dialogRequest = { options, resolve };

                if (this.isDialogActive) {
                    this.dialogQueue.push(dialogRequest);
                    return;
                }

                this._displayDialog(dialogRequest);
            });
        }

        /**
         * 显示一个预设的确认对话框(兼容旧功能)。
         * @param {string|object} options - 对话框配置或消息字符串。
         * @param {string} [options.message] - 对话框的主体信息。
         * @param {string} [options.title] - (可选) 对话框的标题。
         * @param {function} [onConfirm] - (可选) 确认回调。
         * @param {function} [onCancel] - (可选) 取消回调。
         * @returns {Promise<'confirm'|'cancel'>} 返回一个Promise,方便链式调用。
         */
        async showConfirmDialog(options, onConfirm, onCancel) {
            if (typeof options === "string") {
                options = { message: options };
            }
            const result = await this._createDialog({
                ...options,
                buttons: [
                    { text: "取消", value: "cancel", type: "default" },
                    { text: "确定", value: "confirm", type: "primary" },
                ],
            });
            if (result === "confirm" && typeof onConfirm === "function") {
                onConfirm();
            } else if (result === "cancel" && typeof onCancel === "function") {
                onCancel();
            }
            return result;
        }

        /**
         * 显示一个自定义对话框。
         * @param {object|string} options - 对话框配置或消息字符串。
         * @param {...object} items - 按钮配置数组或单个按钮对象
         * @param {string} items[].text - 按钮文本。
         * @param {any} items[].value - 按钮点击后返回的值
         * @param {'primary'|'default'|'danger'} [items[].type='default'] - 按钮类型。
         * @param {function} [items[].callback] - 按钮点击时的回调函数。
         * @returns {Promise<any>} 返回一个Promise,resolve为点击的按钮的value
         */
        async showDialog(options, ...items) {
            if (typeof options === "string") {
                options = { message: options };
            } else if (Array.isArray(options)) {
                items = options;
                options = { message: "请选择操作" };
            } else if (typeof options !== "object") {
                throw new Error("Expected options to be a string, array, or object");
            }
            const result = await this._createDialog({
                ...options,
                buttons: items.map((item, index) => ({
                    text: item.text || `按钮${index + 1}`,
                    type: item.type || "default",
                    value: index,
                })),
            });
            if (items[result].callback && typeof items[result].callback === "function") {
                items[result].callback();
            }
            return items[result].value || items[result].text || result;
        }

        /**
         * @private
         * 内部方法,用于实际创建和显示对话框。
         * @param {object} dialogRequest - 包含options和resolve函数的请求对象。
         */
        _displayDialog({ options, resolve }) {
            this.isDialogActive = true;

            // 默认配置
            const config = {
                title: "",
                ...options,
            };

            // 创建遮罩层
            const overlay = this._createOverlay();

            // 创建对话框容器
            const dialog = this._createDialogContainer();

            // 添加标题 (如果提供)
            if (config.title) {
                const titleEl = this._createTitle(config.title);
                dialog.appendChild(titleEl);
            }

            // 添加消息
            const messageEl = this._createMessage(config.message);
            dialog.appendChild(messageEl);

            // 创建并添加按钮
            const btnBox = this._createButtonContainer();
            config.buttons.forEach((btnConfig) => {
                const button = this._createButton(btnConfig, (value) => {
                    // 关闭对话框的逻辑
                    document.body.removeChild(overlay);
                    this._processNextDialog();
                    resolve(value);
                });
                btnBox.appendChild(button);
            });

            dialog.appendChild(btnBox);
            overlay.appendChild(dialog);
            document.body.appendChild(overlay);
        }

        /**
         * @private
         * 处理队列中的下一个对话框。
         */
        _processNextDialog() {
            this.isDialogActive = false;
            if (this.dialogQueue.length > 0) {
                const nextRequest = this.dialogQueue.shift();
                // 加一个短暂的延迟,避免视觉上两个弹窗无缝衔接
                setTimeout(() => this._displayDialog(nextRequest), 100);
            }
        }

        // --- DOM元素创建的辅助方法 ---

        _createOverlay() {
            const overlay = document.createElement("div");
            Object.assign(overlay.style, {
                position: "fixed",
                top: "0",
                left: "0",
                width: "100vw",
                height: "100vh",
                background: "rgba(0,0,0,0.5)",
                zIndex: "10000",
                backdropFilter: "blur(10px)",
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
            });
            return overlay;
        }

        _createDialogContainer() {
            const dialog = document.createElement("div");
            Object.assign(dialog.style, {
                background: "#fff",
                padding: "20px 24px",
                borderRadius: "12px",
                boxShadow: "0 5px 20px rgba(0,0,0,0.2)",
                minWidth: "300px",
                maxWidth: "calc(100vw - 40px)",
                wordBreak: "break-word",
                fontSize: "14px",
                display: "flex",
                flexDirection: "column",
                gap: "16px", // 统一内容间距
            });
            return dialog;
        }

        _createTitle(titleText) {
            const title = document.createElement("h3");
            title.textContent = titleText;
            Object.assign(title.style, {
                margin: "0",
                fontSize: "18px",
                fontWeight: "600",
                color: "#111",
                textAlign: "center",
            });
            return title;
        }

        _createMessage(messageText) {
            const msg = document.createElement("div");
            msg.innerHTML = messageText.replace(/\n/g, "<br>");
            Object.assign(msg.style, {
                textAlign: "left",
                lineHeight: "1.6",
                color: "#333",
                maxHeight: "60vh",
                overflowY: "auto",
            });
            return msg;
        }

        _createButtonContainer() {
            const btnBox = document.createElement("div");
            Object.assign(btnBox.style, {
                display: "flex",
                justifyContent: "flex-end", // 按钮靠右更常见
                gap: "12px",
                marginTop: "8px",
            });
            return btnBox;
        }

        _getButtonStyles(type = "default") {
            const baseStyle = {
                padding: "6px 18px",
                border: "none",
                borderRadius: "5px",
                cursor: "pointer",
                transition: "all 0.2s ease",
                fontWeight: "500",
                fontSize: "13px",
            };

            const typeStyles = {
                primary: {
                    background: "linear-gradient(135deg, #12c2e9 0%, #c471ed 50%, #f64f59 100%)",
                    color: "#fff",
                },
                danger: {
                    background: "#e74c3c",
                    color: "#fff",
                },
                default: {
                    background: "#f0f0f0",
                    color: "#333",
                    border: "1px solid #ddd",
                },
            };

            return { ...baseStyle, ...(typeStyles[type] || typeStyles["default"]) };
        }

        _createButton(btnConfig, closeCallback) {
            const button = document.createElement("button");
            button.textContent = btnConfig.text;

            Object.assign(button.style, this._getButtonStyles(btnConfig.type));

            button.onclick = () => closeCallback(btnConfig.value);

            // 添加悬停效果
            button.onmouseover = () => (button.style.opacity = "0.85");
            button.onmouseout = () => (button.style.opacity = "1");

            return button;
        }

        /**
         * 跳转到 GitHub issue 页面,并将信息参数化到 URL 中
         * @param {string} title - 标题
         * @param {string} info - 要传递的信息
         */
        gotoGithubIssue(title, info) {
            const url = `${this.repo_url}/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(
                info
            )}`;
            window.open(url, "_blank");
        }

        /**
         * 隐藏悬浮提示
         */
        hideFloatTip() {
            if (document.getElementById("myInfoFloatTip")) {
                document.getElementById("myInfoFloatTip").remove();
            }
        }

        async mainErrorHandler(error) {
            // 使用对话框
            const now = new Date();
            const timeStr = now
                .toISOString()
                .replace("T", " ")
                .replace(/\.\d+Z$/, "");

            const script_config = this.configManager.exportAll();

            // More detailed error capturing with formatted stack trace
            let errorDetails = "";
            if (error instanceof Error) {
                errorDetails += `name: ${error.name}\n`;
                errorDetails += `message: ${error.message}\n`;

                // Format stack trace to be more readable
                if (error.stack) {
                    errorDetails += "stack trace:\n";
                    const stackLines = error.stack.split("\n");

                    // Process each line of the stack trace
                    stackLines.forEach((line) => {
                        line = decodeURIComponent(line.trim());
                        // Extract the relevant parts from each stack line
                        const match = line.match(/([^@\s]+)@(.*?):(\d+):(\d+)/);
                        if (match) {
                            const [_, functionName, filePath, lineNum, colNum] = match;

                            // Get just the filename from the path
                            const fileName = filePath.split("/").pop().split("?")[0];

                            // filename 里被编码为url的特殊字符需要解码,以便查看
                            const decodedFileName = decodeURIComponent(fileName);

                            // Add formatted line to error details
                            errorDetails += `  → func:${functionName} (file:${decodedFileName}@line:${lineNum}@col:${colNum})\n`;
                        } else {
                            // For lines that don't match the pattern, include them as is
                            errorDetails += `  ${line.trim()}\n`;
                        }
                    });
                }

                // Capture custom properties
                for (const key in error) {
                    if (
                        Object.prototype.hasOwnProperty.call(error, key) &&
                        key !== "stack" &&
                        key !== "message" &&
                        key !== "name"
                    ) {
                        errorDetails += `${key}: ${JSON.stringify(error[key])}\n`;
                    }
                }
            } else if (typeof error === "object" && error !== null) {
                errorDetails = JSON.stringify(error, null, 2);
            } else {
                errorDetails = String(error);
            }
            errorDetails = errorDetails.trim();

            await this.showConfirmDialog(
                {
                    title: "⚠️ 警告",
                    message: `下载文章时出错!是否前往Github提交Issue以告知开发者进行修复?(您需要拥有Github账号)\n错误详情:\n${errorDetails}`,
                },
                () =>
                    this.gotoGithubIssue(
                        `[BUG] 下载失败 (${getCurrentPageType()}页面)`,
                        `#### 时间\n\n${timeStr}\n\n#### 错误内容\n\n\`\`\`\n${errorDetails}\n\`\`\`\n\n#### 其他信息\n\n- URL:\`${
                            window.location.href
                        }\`\n- 脚本版本:\`${GM_info.script.version}\`\n- 脚本配置:\n\`\`\`json\n${JSON.stringify(
                            script_config,
                            null,
                            4
                        )}\n\`\`\`\n`
                    ),
                this.showFloatTip("感谢您的反馈!", 2000),
                () => {
                    this.showFloatTip("已取消。", 2000);
                    console.error("下载文章时出错:", error);
                }
            );
        }

        /**
         * 主函数 - 下载文章入口
         */
        async runMain() {
            this.disableFloatWindow();
            const nowConfig = this.configManager.exportAll();
            try {
                switch (getCurrentPageType()) {
                    case "unknown":
                        alert("无法识别的页面。请确保在CSDN文章页面、专栏文章列表页面或用户全部文章列表页面。");
                        break;
                    case "article":
                        await this.downloadManager.downloadSingleArticle(nowConfig);
                        break;
                    case "category":
                        await this.downloadManager.downloadCategory(nowConfig);
                        break;
                    case "user_all_articles":
                        await this.downloadManager.downloadUserAllArticles(nowConfig);
                        break;
                }
            } catch (error) {
                this.mainErrorHandler(error);
            } finally {
                this.enableFloatWindow();
                this.downloadManager.reset(); // 重置FileManager
            }
        }
    }

    /**
     * 配置管理器类
     * 用于管理应用配置
     */
    class ConfigManager {
        constructor() {
            this.configs = new Map(); // 仅用于存储key,实际值从GM_getValue获取
            this.defaults = new Map();
        }

        /**
         * 注册(不可用)配置项
         * @param {string} key - 配置键
         * @param {any} defaultValue - 默认值
         */
        register(key, defaultValue) {
            this.defaults.set(key, defaultValue);

            // 如果未设置,则使用默认值
            if (GM_getValue(key) === undefined) {
                GM_setValue(key, defaultValue);
            }

            // 加入配置映射
            this.configs.set(key, GM_getValue(key));
        }

        /**
         * 获取配置项值
         * @param {string} key - 配置键
         * @returns {any} 配置值
         */
        get(key) {
            // 直接从GM_getValue获取最新值
            return GM_getValue(key);
        }

        /**
         * 设置配置项值
         * @param {string} key - 配置键
         * @param {any} value - 配置值
         */
        set(key, value) {
            GM_setValue(key, value);
            this.configs.set(key, value);
        }

        /**
         * 重置所有配置到默认值
         */
        resetToDefaults() {
            for (const [key, defaultValue] of this.defaults.entries()) {
                this.set(key, defaultValue);
            }
        }

        /**
         * 获取所有配置键
         * @returns {Array} 配置键数组
         */
        getAllKeys() {
            return Array.from(this.configs.keys());
        }

        /**
         * 导出所有配置
         * @returns {Object} 配置对象
         */
        exportAll() {
            const result = {};
            for (const key of this.getAllKeys()) {
                result[key] = this.get(key);
            }
            return result;
        }

        /**
         * 导入配置
         * @param {Object} configs - 配置对象
         */
        importAll(configs) {
            for (const [key, value] of Object.entries(configs)) {
                if (this.configs.has(key)) {
                    this.set(key, value);
                }
            }
        }
    }
    /**
     * 模块: 文件管理
     * 处理文件相关的操作
     */
    class FileManager {
        constructor() {
            this.fileQueue = [];
            this.imageCount = {};
            this.imageSet = {};

            this.zipStream = null;
            this.zipStreamName = "";
            this.zipStreamSize = 0;
            this.zipStreamPendingFiles = new Map(); // 用于存储待处理的Blob对象
            this.zipStreamFileCount = 0;
            this.zipStreamProgressCallback = null;
            this.zipStreamErrorCallback = null;
            this.zipStreamEndFlag = null;
        }

        /**
         * 将文本保存为文件
         * @param {string} content - 文件内容
         * @param {string} filename - 文件名
         * @param {number} index - 文件索引(用于排序)
         */
        async addTextFile(content, filename, index = 0, streaming = false) {
            filename = Utils.safeFilename(filename);

            if (streaming) {
                // 如果是流式处理,直接添加到zip流
                this.addFileToZipStream(filename, content);
            } else {
                // 保存到队列中,等待打包
                this.fileQueue.push({ filename, type: "text/plain", content, index });
            }
        }

        /**
         * 将SVG内容保存到本地,添加到fileQueue,并返回本地路径
         * @param {string} svgText - SVG内容
         * @param {string} assetDirName - 资源文件夹名
         * @param {string} imgPrefix - 图片前缀
         * @returns {Promise<string>} 本地SVG路径
         */
        async addSvgFile(svgText, assetDirName, imgPrefix = "", streaming = false) {
            // 检查参数是否合法
            if (typeof svgText !== "string") {
                throw new Error("[saveSvgToLocal] Invalid argument: svgText must be a string.");
            }

            const imgOwner = imgPrefix + assetDirName;

            // 初始化
            if (!this.imageCount[imgOwner]) {
                this.imageSet[imgOwner] = {};
                this.imageCount[imgOwner] = 0;
            }
            // 检查是否已保存过该SVG(通过内容哈希)
            const svgHash = Utils.simpleHash(svgText, 16); // 使用16位哈希
            if (this.imageSet[imgOwner][svgHash]) {
                return this.imageSet[imgOwner][svgHash];
            }

            // 记录图片数量
            this.imageCount[imgOwner]++;
            const index = this.imageCount[imgOwner];
            const filename = `${assetDirName}/${imgPrefix}${index}.svg`;

            // 记录已保存的SVG
            this.imageSet[imgOwner][svgHash] = `./${filename}`;

            // 创建SVG的Blob对象
            const blob = new Blob([svgText], { type: "image/svg+xml" });

            if (streaming) {
                // 如果是流式处理,直接添加到zip流
                this.addFileToZipStream(filename, blob);
            } else {
                // 添加到文件队列
                this.fileQueue.push({ filename, content: blob, type: "image/svg+xml", index });
            }

            // 返回本地路径
            return `./${filename}`;
        }

        /**
         * 将网络图片保存到本地,添加到fileQueue,并返回本地路径
         * @param {string} imgUrl - 图片URL
         * @param {string} assetDirName - 资源文件夹名
         * @param {string} imgPrefix - 图片前缀
         * @returns {Promise<string>} 本地图片路径
         */
        async addWebImageFile(
            imgUrl,
            assetDirName,
            imgPrefix = "",
            streaming = false,
            retryCount = 3,
            retryDelay = 1000
        ) {
            // 检查参数是否合法
            if (typeof imgUrl !== "string") {
                throw new Error("[saveWebImageToLocal] Invalid argument: imgUrl must be a string.");
            }

            // 清理URL
            imgUrl = Utils.clearUrl(imgUrl);

            const imgOwner = imgPrefix + assetDirName;

            // 初始化
            if (!this.imageCount[imgOwner]) {
                this.imageSet[imgOwner] = {};
                this.imageCount[imgOwner] = 0;
            }

            // 检查是否已保存过该图片
            if (this.imageSet[imgOwner][imgUrl]) {
                return this.imageSet[imgOwner][imgUrl];
            }

            // 记录图片数量
            this.imageCount[imgOwner]++;
            const index = this.imageCount[imgOwner];
            let ext = imgUrl.split(".").pop();
            const allowedExt = ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "ico", "avif"];
            if (!allowedExt.includes(ext)) {
                console.warn(`[saveWebImageToLocal] Unsupported image format: ${ext}`);
                ext = "";
            } else {
                ext = `.${ext}`;
            }
            const filename = `${assetDirName}/${imgPrefix}${index}${ext}`;

            // 记录已保存的图片
            this.imageSet[imgOwner][imgUrl] = `./${filename}`;

            // 获取图片的Blob对象
            // 返回的Promise Blob对象,需要等到打包时进行等待
            const blob = this.fetchResource(imgUrl, "blob", retryCount, retryDelay, "GM_xmlhttpRequest");

            if (streaming) {
                // 存储 Promise 对象
                this.zipStreamPendingFiles.set(
                    filename,
                    new Promise(async (resolve) => {
                        // 等待Blob解析完成
                        try {
                            const blobData = await blob;
                            await this.addFileToZipStream(filename, blobData);
                        } catch (error) {
                            console.dir(`Error fetching image ${imgUrl}:`, error);
                            if (this.zipStreamErrorCallback) {
                                this.zipStreamErrorCallback(error);
                            }
                        }
                        resolve();
                        this.zipStreamPendingFiles.delete(filename); // 完成后从待处理列表中删除
                    })
                );
            } else {
                // 添加到文件队列
                this.fileQueue.push({ filename, content: blob, type: blob.type, index });
            }

            // 返回本地路径
            return `./${filename}`;
        }

        /**
         * 获取网络资源
         * @param {string} url - 资源URL
         * @param {string} [responseType='blob'] - 响应类型,默认为'blob',可选值为'text'、'json'等
         * @param {number} retryCount - 重试次数,默认值为3
         * @returns {Promise<Blob|string|Object>} - 返回资源的Blob对象或文本内容
         * @throws {Error} - 如果获取资源失败,抛出错误
         */
        async fetchResource(url, responseType = "blob", retryCount = 3, retryDelay = 1000, api = "GM_xmlhttpRequest") {
            return new Promise((resolve, reject) => {
                function attemptFetch(remaining) {
                    if (api === "GM_xmlhttpRequest") {
                        GM_xmlhttpRequest({
                            method: "GET",
                            url: url,
                            responseType: responseType,
                            onload: function (response) {
                                if (response.status === 200) {
                                    resolve(response.response);
                                } else {
                                    if (remaining > 0) {
                                        console.warn(`Retrying fetch for ${url}, attempts left: ${remaining - 1}`);
                                        setTimeout(() => {
                                            attemptFetch(remaining - 1);
                                        }, retryDelay);
                                    } else {
                                        reject(
                                            `Failed to fetch resource: ${url}\nStatus: ${response.status} ${response.statusText}`
                                        );
                                    }
                                }
                            },
                            onerror: function () {
                                if (remaining > 0) {
                                    console.warn(`Retrying fetch for ${url}, attempts left: ${remaining - 1}`);
                                    setTimeout(() => {
                                        attemptFetch(remaining - 1);
                                    }, retryDelay);
                                } else {
                                    reject(`Error fetching resource: ${url}`);
                                }
                            },
                        });
                    } else if (api === "fetch") {
                        fetch(url)
                            .then((response) => {
                                if (!response.ok) {
                                    throw new Error(`HTTP error! status: ${response.status}`);
                                }
                                if (responseType === "json") return response.json();
                                if (responseType.startsWith("text")) return response.text();
                                if (responseType === "blob") return response.blob();
                                throw new Error(`Unsupported response type: ${responseType}`);
                            })
                            .then((data) => {
                                resolve(data);
                            })
                            .catch((error) => {
                                if (remaining > 0) {
                                    console.warn(`Retrying fetch for ${url}, attempts left: ${remaining - 1}`);
                                    setTimeout(() => {
                                        attemptFetch(remaining - 1);
                                    }, retryDelay);
                                } else {
                                    reject(`Error fetching resource: ${url}\n${error.message}`);
                                }
                            });
                    } else {
                        reject(new Error(`Unsupported API: ${api}. Use 'GM_xmlhttpRequest' or 'fetch'.`));
                    }
                }
                attemptFetch(retryCount);
            });
        }

        /**
         * 合并文章内容
         * @param {string} outputFileName - 合并后的文件名
         * @param {string} extraTopContent - 额外的顶部内容
         */
        mergeTextFile(outputFileName, extraTopContent = "") {
            // 检查队列是否只有一个md文件
            let mdCount = 0;
            this.fileQueue.forEach((file) => {
                if (file.type === "text/plain") {
                    mdCount++;
                }
            });
            if (mdCount <= 1) {
                return;
            }

            // 合并文章内容
            const textArray = [];
            const newFileQueue = [];
            this.fileQueue.forEach((file) => {
                if (file.type === "text/plain") {
                    textArray.push({ content: file.content, index: file.index });
                } else {
                    newFileQueue.push(file);
                }
            });

            // 按照index排序
            textArray.sort((a, b) => a.index - b.index);
            const mergedContent = textArray.map((item) => item.content).join("\n\n\n\n");

            newFileQueue.push({
                filename: `${outputFileName}.md`,
                type: "text/plain",
                content: `${extraTopContent}${mergedContent}`,
            });
            this.fileQueue = newFileQueue;
        }

        /**
         * 创建ZIP流,使用 fflate 库和 streamSaver
         * @param {Array<{name: string, data: Uint8Array|string}>} files - 文件对象数组
         * @param {string} zipName - ZIP文件名
         * @param {function(string, number):void} [onProgress] - 可选的进度回调,接收文件名和索引
         * @param {function(string, number, number):void} [onFinished] - 可选的完成回调,接收当前zip文件名、文件总数、zip大小
         * @param {function(Error):void} [onError] - 可选的错误回调
         **/
        async initializeZipStream(zipName, onProgress = null, onFinished = null, onError = null) {
            if (!zipName.endsWith(".zip")) {
                zipName += ".zip"; // 确保ZIP文件名以.zip结尾
            }
            const downloadStream = streamSaver.createWriteStream(zipName);
            let writer = downloadStream.getWriter();

            // 定义一个在页面卸-载前中止写入的函数
            const abortStream = () => {
                if (writer) {
                    console.dir("页面即将卸载,中止写入流...");
                    writer.abort("用户中断了下载").catch((e) => console.error("中止流时出错:", e));
                    writer = null; // 清理writer引用
                }
            };
            // 注册(不可用)事件监听器
            window.addEventListener("beforeunload", abortStream);

            // 如果已经有zipStream存在,先结束之前的流
            if (this.zipStream) {
                console.dir("Ending previous ZIP stream before creating a new one.");
                await this.endZipStream();
            }

            this.zipStreamName = zipName;
            this.zipStreamSize = 0;
            this.zipStreamFileCount = 0;

            if (onProgress && typeof onProgress === "function") this.zipStreamProgressCallback = onProgress;
            else this.zipStreamProgressCallback = null;
            if (onError && typeof onError === "function") this.zipStreamErrorCallback = onError;
            else this.zipStreamErrorCallback = null;

            let zipFinalResolve = () => {
                console.dir("ZIP stream resolved.");
                this.zipStream = null; // 清理zipStream引用
            };
            let zipFinalReject = () => {};
            this.zipStreamEndFlag = new Promise((resolve, reject) => {
                zipFinalResolve = resolve;
                zipFinalReject = reject;
            });

            this.zipStream = new fflate.Zip((err, chunk, final) => {
                try {
                    if (err) {
                        writer.abort(err); // 如果出错,中止写入
                        console.dir(`ZIP stream error: ${err}`);
                        if (onError && typeof onError === "function") onError(err);
                        zipFinalReject(err);
                        return;
                    }
                    if (chunk) {
                        this.zipStreamSize += chunk.length; // 累计ZIP数据大小,单位为字节
                        writer.write(chunk);
                    }
                    if (final) {
                        writer.close();
                        writer = null; // 清理writer引用
                        if (onFinished && typeof onFinished === "function") {
                            onFinished(this.zipStreamName, this.zipStreamFileCount, this.zipStreamSize);
                        }
                        zipFinalResolve();
                    }
                } catch (error) {
                    writer.abort(error); // 如果处理过程中出错,中止写入
                    console.dir(`ZIP processing error: ${error}`);
                    if (onError && typeof onError === "function") onError(error);
                    zipFinalReject(error);
                }
            });
        }

        /**
         * 向ZIP流中添加文件
         * @param {string} filename - 文件名
         * @param {string|Uint8Array|Blob|Promise} content - 文件内容,可以是字符串、Uint8Array或Blob
         * @throws {Error} 如果zipStream未初始化或内容类型不支持
         * @return {Promise<void>} 返回一个Promise,表示文件已添加到ZIP流中
         **/
        async addFileToZipStream(filename, content) {
            if (!this.zipStream) throw new Error("ZIP stream is not initialized. Call createZipStream first.");

            let data = null;
            if (typeof content === "string") data = new TextEncoder().encode(content);
            else if (content instanceof Uint8Array) data = content;
            else if (content instanceof Blob) data = new Uint8Array(await content.arrayBuffer());
            else throw new Error("Unsupported content type. Must be string, Uint8Array, or Blob.");

            // 增加文件计数
            this.zipStreamFileCount++;

            console.dir(
                `Add file to stream (No: ${this.zipStreamFileCount}, Now Size: ${Utils.formatFileSize(
                    this.zipStreamSize
                )}): ${filename}`
            );

            const fileStream = new fflate.ZipPassThrough(filename);
            this.zipStream.add(fileStream);
            fileStream.push(data, true);

            if (this.zipStreamProgressCallback && typeof this.zipStreamProgressCallback === "function") {
                this.zipStreamProgressCallback(this.zipStreamFileCount, filename);
            }
        }

        /**
         * 将所有文件添加到ZIP流中
         * @param {boolean} [endStream=true] - 是否在添加完所有文件结束ZIP流
         * @param {function(string, number, number):void} [addFilesFinalCallback=null] - 可选的最终回调,接收当前zip文件名、文件总数、zip大小
         * @throws {Error} 如果zipStream未初始化
         * @return {Promise<void>} 返回一个Promise,表示所有文件已添加到ZIP流中
         **/
        async addAllFilesInQueueToZipStream(endStream = false, addFilesFinalCallback = null) {
            if (!this.zipStream) throw new Error("ZIP stream is not initialized. Call createZipStream first.");

            if (this.fileQueue.length === 0) {
                console.dir("没有文件需要打包到ZIP中");
            } else {
                // 使用 for...of 循环替代 forEach,以便正确处理 async/await
                for (let idx = 0; idx < this.fileQueue.length; idx++) {
                    let status = true;
                    const file = this.fileQueue[idx];

                    // content 可能是 promise(Blob对象),需要等待
                    if (file.content instanceof Promise) {
                        try {
                            file.content = await file.content; // 等待Blob对象
                        } catch (err) {
                            console.dir(`Error resolving content for file ${file.filename}: ${err}`);
                            if (this.zipStreamErrorCallback && typeof this.zipStreamErrorCallback === "function") {
                                this.zipStreamErrorCallback(err);
                            }
                            status = false;
                        }
                    }
                    if (!status) {
                        console.dir(`Skipping file ${file.filename} due to download failure.`);
                        continue; // 如果下载失败,跳过当前文件
                    }
                    // 将文件添加到ZIP中
                    await this.addFileToZipStream(file.filename, file.content);
                }
            }

            // 确保ZIP流已结束
            if (endStream) {
                await this.endZipStream();
                console.dir("Ending ZIP stream after adding all files.");
            }

            // 调用最终回调
            if (addFilesFinalCallback && typeof addFilesFinalCallback === "function") {
                addFilesFinalCallback(this.zipStreamName, this.zipStreamFileCount, this.zipStreamSize);
            }

            // 清空文件队列
            this.reset();
        }

        /**
         * 结束ZIP流,完成写入
         * @throws {Error} 如果zipStream未初始化
         * @return {Promise<void>} 返回一个Promise,表示ZIP流已结束
         **/
        async endZipStream() {
            if (!this.zipStream) throw new Error("ZIP stream is not initialized. Call createZipStream first.");
            // 如果有待处理的Blob对象,等待它们完成
            for (const [filename, blobProcessPromise] of this.zipStreamPendingFiles.entries()) {
                if (blobProcessPromise instanceof Promise) {
                    try {
                        await blobProcessPromise; // 等待Blob对象解析
                    } catch (error) {
                        console.dir(`Error processing pending file ${filename}: ${error}`);
                        if (this.zipStreamErrorCallback && typeof this.zipStreamErrorCallback === "function") {
                            this.zipStreamErrorCallback(error);
                        }
                    }
                }
            }
            // 清空待处理的Blob对象
            this.zipStreamPendingFiles.clear();
            this.zipStream.end();
            try {
                await this.zipStreamEndFlag; // 返回结束的Promise
            } catch (error) {
                console.dir(`Error ending ZIP stream: ${error}`);
                if (this.zipStreamErrorCallback && typeof this.zipStreamErrorCallback === "function") {
                    this.zipStreamErrorCallback(error);
                }
            }
            this.zipStream = null; // 清理zipStream引用
        }

        /**
         * 使用 fflate 将文件打包成 ZIP,支持进度回调
         * @param {Array<{name: string, data: Uint8Array|string}>} files - 文件对象数组
         * @param {function(number, string):void} [onProgress] - 可选的进度回调,接收百分比和文件名
         * @param {function(Error):void} [onError] - 可选的错误回调
         * @return {Promise<Uint8Array>} 返回包含 ZIP 数据的 Promise
         **/
        async createZipWithProgress(files, onProgress = null, onError = null) {
            return new Promise((resolve, reject) => {
                const encoder = new TextEncoder();
                const chunks = [];
                let totalFiles = files.length;
                let processedFiles = 0;

                const zip = new fflate.Zip((err, chunk, final) => {
                    if (err) {
                        // Logger.error("ZIP creation failed:", err);
                        console.dir(`ZIP creation failed: ${err}`);
                        if (onError && typeof onError === "function") {
                            onError(err);
                        }
                        return reject(err);
                    }
                    if (chunk) chunks.push(chunk);
                    if (final) {
                        const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
                        const result = new Uint8Array(totalLength);
                        let offset = 0;
                        for (const chunk of chunks) {
                            result.set(chunk, offset);
                            offset += chunk.length;
                        }
                        resolve(result);
                    }
                });

                if (totalFiles === 0) {
                    zip.end();
                    return;
                }

                files.forEach((file, index) => {
                    const data = typeof file.data === "string" ? encoder.encode(file.data) : file.data;
                    const fileStream = new fflate.ZipPassThrough(file.name);
                    zip.add(fileStream);
                    fileStream.push(data, true);
                    processedFiles++;
                    const percentage = Math.round((processedFiles / totalFiles) * 100);
                    if (onProgress && typeof onProgress === "function") {
                        try {
                            onProgress(percentage, file.name);
                        } catch (e) {
                            // Logger.error("Progress callback error:", e);
                            console.dir(`Progress callback error: ${e}`);
                            if (onError && typeof onError === "function") {
                                onError(e);
                            }
                            return reject(e);
                        }
                    }
                    if (processedFiles === totalFiles) zip.end();
                });
            });
        }

        /**
         * 将文件队列打包为ZIP下载(fflate)
         * @param {string} zipName - ZIP文件名
         */
        async zipAllFilesInQueue(zipName, progressCallback = null, finalCallback = null, api = "fflate") {
            // 检查是否有 fflate 库
            if (typeof fflate === "undefined" || api !== "fflate") {
                // 如果没有 fflate 库,使用 jszip 作为备选方案
                console.warn("使用 jszip 作为备选方案");
                return this.zipAllFilesInQueue_jszip(zipName, progressCallback, finalCallback);
            }

            if (this.fileQueue.length === 0) {
                console.dir("没有文件需要保存");
                return;
            }

            if (!zipName.endsWith(".zip")) {
                zipName = zipName + ".zip"; // 确保ZIP文件名以.zip结尾
            }

            zipName = Utils.safeFilename(zipName);

            const zipFiles = [];

            // 使用 for...of 循环替代 forEach,以便正确处理 async/await
            for (let idx = 0; idx < this.fileQueue.length; idx++) {
                let status = true;
                const file = this.fileQueue[idx];
                // content 可能是 promise(Blob对象),需要等待
                if (file.content instanceof Promise) {
                    if (progressCallback && typeof progressCallback === "function") {
                        progressCallback(`正在下载资源:${file.filename} (${idx + 1}/${this.fileQueue.length})`);
                    }
                    try {
                        file.content = await file.content; // 等待Blob对象
                    } catch (err) {
                        if (progressCallback && typeof progressCallback === "function") {
                            progressCallback(`下载资源失败:${err}`);
                        }
                        status = false;
                    }
                }
                if (!status) {
                    continue; // 如果下载失败,跳过当前文件
                }
                // 将文件添加到ZIP中
                zipFiles.push({
                    name: file.filename,
                    data:
                        file.content instanceof Blob
                            ? new Uint8Array(await file.content.arrayBuffer())
                            : file.content instanceof Uint8Array
                            ? file.content
                            : new TextEncoder().encode(file.content),
                });
            }

            // 获取当前时间,以便计算剩余时间
            const startTime = Date.now();

            // 使用 fflate 创建 ZIP 文件
            const zipContent = await this.createZipWithProgress(
                zipFiles,
                (percent, currentFile) => {
                    // 进度回调
                    if (progressCallback) {
                        // percent: 当前进度百分比
                        // currentFile: 当前正在处理的文件名
                        progressCallback(
                            `正在打包:${currentFile} (${percent}%)(剩余时间:${Utils.formatSeconds(
                                ((Date.now() - startTime) / 1000 / percent) * (100 - percent)
                            )})`
                        );
                    }
                },
                async (error) => {
                    console.error("Error generating ZIP file:", error);
                    if (finalCallback && typeof finalCallback === "function") {
                        finalCallback(`下载失败:${zipName},错误信息:${error}`);
                        this.reset(); // 清空文件队列
                        throw new Error(`下载失败:${zipName},错误信息:${error}`);
                    }
                }
            );

            const zipBlob = new Blob([zipContent], { type: "application/octet-stream" });

            // 调用最终回调
            if (finalCallback && typeof finalCallback === "function") {
                finalCallback(
                    `打包完成:${zipName},文件大小:${Utils.formatFileSize(zipBlob.size)}\n请等待下载完成。`
                );
            }

            this.reset(); // 清空文件队列

            this.fileQueue.push({
                filename: zipName,
                type: "application/zip",
                content: zipBlob,
            });
        }

        /**
         * 将文件队列打包为ZIP下载
         * @param {string} zipName - ZIP文件名
         */
        async zipAllFilesInQueue_jszip(zipName, progressCallback = null, finalCallback = null) {
            if (this.fileQueue.length === 0) {
                console.error("没有文件需要保存");
                return;
            }

            if (!zipName.endsWith(".zip")) {
                zipName = zipName + ".zip"; // 确保ZIP文件名以.zip结尾
            }

            zipName = Utils.safeFilename(zipName);

            // 创建JSZip实例
            const zip = new JSZip();

            // 使用 for...of 循环替代 forEach,以便正确处理 async/await
            for (let idx = 0; idx < this.fileQueue.length; idx++) {
                let status = true;
                const file = this.fileQueue[idx];
                // content 可能是 promise(Blob对象),需要等待
                if (file.content instanceof Promise) {
                    if (progressCallback) {
                        progressCallback(`正在下载资源:${file.filename} (${idx + 1}/${this.fileQueue.length})`);
                    }
                    try {
                        file.content = await file.content; // 等待Blob对象
                    } catch (err) {
                        if (progressCallback) {
                            progressCallback(`下载资源失败:${err}`);
                        }
                        status = false;
                    }
                }
                if (!status) {
                    continue; // 如果下载失败,跳过当前文件
                }
                // 将文件添加到ZIP中
                zip.file(file.filename, file.content);
            }

            // 获取当前时间,以便计算剩余时间
            const startTime = Date.now();

            return new Promise((resolve, reject) => {
                // 生成ZIP文件
                zip.generateAsync({ type: "blob" }, (metadata) => {
                    // 进度回调
                    if (progressCallback) {
                        // metadata.percent: 当前进度百分比
                        // metadata.currentFile: 当前正在处理的文件名
                        progressCallback(
                            `正在打包:${metadata.currentFile} (${Math.round(
                                metadata.percent
                            )}%)(剩余时间:${Utils.formatSeconds(
                                ((Date.now() - startTime) / 1000 / metadata.percent) * (100 - metadata.percent)
                            )})`
                        );
                    }
                })
                    .then((blob) => {
                        // 调用最终回调
                        if (finalCallback) {
                            finalCallback(
                                `打包完成:${zipName},文件大小:${Utils.formatFileSize(blob.size)}\n请等待下载完成。`
                            );
                        }
                        this.reset(); // 清空文件队列
                        this.fileQueue.push({
                            filename: zipName,
                            type: "application/zip",
                            content: blob,
                        });
                        resolve();
                    })
                    .catch((error) => {
                        // 处理错误
                        this.reset(); // 清空文件队列
                        console.error("Error generating ZIP file:", error);
                        if (finalCallback) {
                            finalCallback(`下载失败:${zipName},错误信息:${error}`);
                            throw new Error(`下载失败:${zipName},错误信息:${error}`);
                        }
                        reject(error);
                    });
            });
        }

        /**
         * 下载队列里的全部文件
         */
        async downloadAllFilesInQueue() {
            if (this.fileQueue.length === 0) {
                console.dir("没有文件需要下载");
                return;
            }

            for (let i = 0; i < this.fileQueue.length; i++) {
                const file = this.fileQueue[i];
                let content = file.content;

                // 如果content是Promise,等待其完成
                if (content instanceof Promise) {
                    try {
                        content = await content;
                    } catch (error) {
                        console.error(`下载文件 ${file.filename} 失败:`, error);
                        continue;
                    }
                }

                // 创建Blob对象
                const blob = content instanceof Blob ? content : new Blob([content], { type: file.type });

                // 创建下载链接
                const url = URL.createObjectURL(blob);
                const a = document.createElement("a");
                a.href = url;
                a.download = file.filename;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);

                // 添加小延迟避免浏览器阻止多文件下载
                await new Promise((resolve) => setTimeout(resolve, 100));
            }

            this.reset(); // 清空文件队列
        }

        /**
         * 重置图片计数器和缓存
         */
        clearImageCache() {
            this.imageCount = {};
            this.imageSet = {};
        }

        /**
         * 清空文件队列
         */
        clearFileQueue() {
            this.fileQueue = [];
        }

        /**
         * 重置FileManager
         */
        reset() {
            this.clearFileQueue();
            this.clearImageCache();
        }
    }

    /**
     * 模块: Markdown转换
     * 将HTML转换为Markdown
     */
    class MarkdownConverter {
        /**
         * 创建HTML标签到处理函数的映射表
         * @returns {Object} 标签名称到处理方法的映射
         */
        static createTagHandlers() {
            return {
                h1: this.prototype.handleHeading,
                h2: this.prototype.handleHeading,
                h3: this.prototype.handleHeading,
                h4: this.prototype.handleHeading,
                h5: this.prototype.handleHeading,
                h6: this.prototype.handleHeading,
                p: this.prototype.handleParagraph,
                strong: this.prototype.handleStrong,
                b: this.prototype.handleStrong,
                em: this.prototype.handleEmphasis,
                i: this.prototype.handleEmphasis,
                u: this.prototype.handleUnderline,
                s: this.prototype.handleStrikethrough,
                strike: this.prototype.handleStrikethrough,
                a: this.prototype.handleAnchor,
                img: this.prototype.handleImage,
                ul: this.prototype.handleList,
                ol: this.prototype.handleList,
                blockquote: this.prototype.handleBlockquote,
                pre: this.prototype.handlePreformatted,
                code: this.prototype.handleCode,
                hr: this.prototype.handleHorizontalRule,
                br: this.prototype.handleLineBreak,
                table: this.prototype.handleTable,
                div: this.prototype.handleDiv,
                span: this.prototype.handleSpan,
                kbd: this.prototype.handleKeyboard,
                mark: this.prototype.handleMark,
                sub: this.prototype.handleSubscript,
                sup: this.prototype.handleSuperscript,
                svg: this.prototype.handleSvg,
                section: this.prototype.handleSection,
                input: this.prototype.handleInput,
                dl: this.prototype.handleDefinitionList,
                abbr: this.prototype.handleAbbreviation,
                font: this.prototype.handleFont,
                td: this.prototype.handleTableCell,
                th: this.prototype.handleTableCell,
                center: this.prototype.handleCenter,
            };
        }

        /**
         * @param {FileManager} fileManager - 文件管理实例
         * @constructor
         */
        constructor(fileManager) {
            this.fileManager = fileManager;
            this.tagHandlers = MarkdownConverter.createTagHandlers();

            // 预定义的特殊字段
            // 内容之间保持两个换行符
            this.CONSTANT_DOUBLE_NEW_LINE = "<|CSDN2MD@CONSTANT_DOUBLE_NEW_LINE@23hy7b|>";
            // 分隔符用于美化,比如公式和文本之间加上空格会更美观
            this.SEPARATION_BEAUTIFICATION = "<|CSDN2MD@SEPARATION_BEAUTIFICATION@2caev2|>";

            this.DDNL = this.escapeRegExp(this.CONSTANT_DOUBLE_NEW_LINE);
            this.SEPB = this.escapeRegExp(this.SEPARATION_BEAUTIFICATION);

            // 1. 连续的 "\n" 与 CONSTANT_DOUBLE_NEW_LINE 替换为 "\n\n"
            this.RE_DOUBLE_NL = new RegExp(`(?:\\n|${this.DDNL})*${this.DDNL}(?:\\n|${this.DDNL})*`, "g");
            // 2. 连续的 SEPARATION_BEAUTIFICATION 替换为 " ",但如果前面是换行符,替换为 ""
            this.RE_SEP_NOLINE = new RegExp(`(?<!\\n)(?:${this.SEPB})+`, "g");
            this.RE_SEP_WITHNL = new RegExp(`(\\n)(?:${this.SEPB})+`, "g");

            // 节点类型常量
            this.ELEMENT_NODE = 1;
            this.TEXT_NODE = 3;
            this.COMMENT_NODE = 8;
        }

        /**
         * 将HTML内容转换为Markdown格式
         * @param {Element} articleElement - 文章DOM元素
         * @param {Object} config - 配置选项
         * @param {string} [config.assetDirName=""] - 资源文件夹名
         * @param {boolean} [config.enableTOC=true] - 是否启用目录
         * @param {string} [config.imgPrefix=""] - 图片文件前缀
         * @param {boolean} [config.saveWebImages=false] - 是否将网络图片保存到本地
         * @param {number} [config.downloadAssetRetryCount=3] - 下载资源失败时的重试次数
         * @param {boolean} [config.enableStreaming=false] - 是否启用流式处理
         * @param {boolean} [config.forceImageCentering=false] - 是否强制所有图片居中
         * @param {boolean} [config.enableImageSize=false] - 是否保留图片尺寸
         * @param {boolean} [config.enableColorText=false] - 是否保留彩色文本
         * @param {boolean} [config.removeCSDNSearchLink=true] - 是否移除CSDN搜索链接
         * @returns {Promise<string>} Markdown内容
         */
        async htmlToMarkdown(articleElement, config = {}) {
            // 设置默认配置
            const defaultConfig = {
                assetDirName: "",
                enableTOC: true,
                imgPrefix: "",
                saveWebImages: false,
                downloadAssetRetryCount: 3,
                downloadAssetRetryDelay: 1000,
                enableStreaming: false,
                forceImageCentering: false,
                enableImageSize: false,
                enableColorText: false,
                removeCSDNSearchLink: true,
                enableMarkdownEscape: true,
                markdownEscapePattern: "`*_[]{}()#+-.!",
            };

            // 合并用户配置和默认配置,并添加上下文信息
            const context = {
                ...defaultConfig,
                ...config,
                listLevel: 0,
            };

            // 处理文章元素的子节点
            const markdown = await this.processChildren(articleElement, context);

            // 后处理Markdown内容,美化输出
            return this.postProcessMarkdown(markdown.trim());
        }

        /**
         * 处理单个DOM节点
         * @param {Node} node - 当前DOM节点
         * @param {Object} context - 处理上下文
         * @returns {Promise<string>} 节点的Markdown字符串
         */
        async processNode(node, context) {
            switch (node.nodeType) {
                case this.ELEMENT_NODE:
                    const tagName = node.tagName.toLowerCase();
                    const handler = this.tagHandlers[tagName];
                    return handler
                        ? await handler.call(this, node, context)
                        : await this.handleDefaultElement(node, context);

                case this.TEXT_NODE:
                    // 处理文本节点(即没有被单独的标签包裹的文本)
                    if (context.enableMarkdownEscape) {
                        // 如果启用了Markdown转义,则转义文本内容
                        return this.escapeMarkdown(node.textContent, context.markdownEscapePattern);
                    } else {
                        return node.textContent.trim(); // 如果没有启用转义,则直接返回文本内容
                    }
                case this.COMMENT_NODE:
                    // 忽略注释
                    return "";

                default:
                    return "";
            }
        }

        /**
         * 处理元素的子节点
         * @param {Node} node - 父节点
         * @param {Object} context - 处理上下文
         * @returns {Promise<string>} 子节点拼接后的Markdown字符串
         */
        async processChildren(node, context) {
            let result = "";
            for (const child of node.childNodes) {
                result += await this.processNode(child, context);
            }
            return result;
        }

        /**
         * 转义特殊的Markdown字符
         * @param {string} text - 需要转义的文本
         * @returns {string} 转义后的文本
         */
        escapeMarkdown(text, escapePattern = "`*_[]{}()#+-.!") {
            // 注:原代码中有一个被注释掉的转义逻辑,这里只保留了trim操作
            // return text.replace(/([\\`*_\{\}\[\]()#+\-.!])/g, "\\$1").trim();
            // return text.trim(); // 不转义特殊字符

            // 使用正则表达式转义特殊字符
            escapePattern = this.escapeRegExp(escapePattern);
            const escapeRegex = new RegExp(`([${escapePattern}])`, "g");
            return text.replace(escapeRegex, "\\$1").trim();
        }

        /**
         * 对常量做正则转义
         * @param {string} s - 需要转义的字符串
         * @returns {string} 转义后的字符串
         */
        escapeRegExp(s) {
            return s.replaceAll(/[\-\.\*\+\?\^\$\{\}\(\)\|\[\]\\]/g, "\\$&");
        }

        /**
         * 特殊字符串修剪函数:移除字符串开头和结尾的分隔符和空白字符
         * @param {string} text - 需要修剪的字符串
         * @returns {string} 修剪后的字符串
         */
        specialTrim(text = "") {
            return text
                .replace(new RegExp(`^(?:${this.SEPB}|\\s)+`), "")
                .replace(new RegExp(`(?:${this.SEPB}|\\s)+$`), "");
        }

        /**
         * 后处理Markdown内容
         * @param {string} markdown - 原始Markdown内容
         * @returns {string} 处理后的Markdown内容
         */
        postProcessMarkdown(markdown) {
            return markdown
                .replaceAll(this.RE_DOUBLE_NL, "\n\n") // 吃掉前后重复换行和标记,统一为两个换行
                .replaceAll(this.RE_SEP_NOLINE, " ") // 非换行前的标记串 → 空格
                .replaceAll(this.RE_SEP_WITHNL, "$1"); // 换行后的标记串 → 保留换行
        }

        /****************************************
         * 标签处理函数
         ****************************************/

        /**
         * 处理标题元素(h1-h6)
         */
        async handleHeading(node, context) {
            const level = parseInt(node.tagName[1]);

            // 移除节点内部开头的空 <a> 标签
            node.querySelectorAll("a").forEach((aTag) => {
                if (aTag && aTag.textContent.trim() === "") {
                    aTag.remove();
                }
            });

            let content = await this.processChildren(node, context);

            // 按行分割分别处理
            // 如果该行内容不为空且不包含图片,则添加标题前缀
            content = content
                .split("\n")
                .map((line) => {
                    if (line.trim() !== "") {
                        // 如果该行内容是 <img /> 标签,则不添加前缀
                        if (line.trim().search("<img") !== -1 && line.trim().search("/>") !== -1) {
                            return line;
                        }
                        return `${"#".repeat(level)} ${line}`;
                    }
                    return line;
                })
                .join("\n");

            return `${content}${this.CONSTANT_DOUBLE_NEW_LINE}`;
        }

        /**
         * 处理段落元素
         */
        async handleParagraph(node, context) {
            const cls = node.getAttribute("class");
            const style = node.getAttribute("style");

            if (cls && cls.includes("img-center")) {
                // 处理图片居中,类似 <center> 标签
                this.addPicCenterToImages(node);
                return (await this.processChildren(node, context)) + this.CONSTANT_DOUBLE_NEW_LINE;
            }

            // 处理目录
            if (node.getAttribute("id") === "main-toc") {
                if (context.enableTOC) {
                    return `**目录**\n\n[TOC]\n\n`;
                }
                return "";
            }

            let text = await this.processChildren(node, context);

            // 处理带样式的段落
            if (style) {
                if (style.includes("padding-left")) {
                    return "";
                }
                if (style.includes("text-align:center")) {
                    return `<div style="text-align:center;">${Utils.shrinkHtml(node.innerHTML)}</div>\n\n`;
                } else if (style.includes("text-align:right")) {
                    return `<div style="text-align:right;">${Utils.shrinkHtml(node.innerHTML)}</div>\n\n`;
                }
            }

            return `${text}\n\n`;
        }

        /**
         * 处理加粗元素
         */
        async handleStrong(node, context) {
            const content = this.specialTrim(await this.processChildren(node, context));
            if (content === "") return "";
            return `${this.SEPARATION_BEAUTIFICATION}**${content}**${this.SEPARATION_BEAUTIFICATION}`;
        }

        /**
         * 处理斜体元素
         */
        async handleEmphasis(node, context) {
            const content = this.specialTrim(await this.processChildren(node, context));
            if (content === "") return "";
            return `${this.SEPARATION_BEAUTIFICATION}*${content}*${this.SEPARATION_BEAUTIFICATION}`;
        }

        /**
         * 处理下划线元素
         */
        async handleUnderline(node, context) {
            const content = this.specialTrim(await this.processChildren(node, context));
            if (content === "") return "";
            return `${this.SEPARATION_BEAUTIFICATION}<u>${content}</u>${this.SEPARATION_BEAUTIFICATION}`;
        }

        /**
         * 处理删除线元素
         */
        async handleStrikethrough(node, context) {
            const content = this.specialTrim(await this.processChildren(node, context));
            if (content === "") return "";
            return `${this.SEPARATION_BEAUTIFICATION}~~${content}~~${this.SEPARATION_BEAUTIFICATION}`;
        }

        /**
         * 处理链接元素
         */
        async handleAnchor(node, context) {
            const nodeClass = node.getAttribute("class");
            // 忽略脚注返回链接
            if (nodeClass && nodeClass.includes("footnote-backref")) {
                return "";
            }

            const href = node.getAttribute("href") || "";
            // 处理卡片链接
            if (nodeClass && nodeClass.includes("has-card")) {
                const desc = node.title || "";
                return `[${desc}](${href}) `;
            }

            let text = await this.processChildren(node, context);
            // 处理CSDN搜索链接
            if (href.includes("https://so.csdn.net/so/search") && context.removeCSDNSearchLink) {
                return text;
            }

            // 适配旧版CSDN的 "OLE_LINK{xxx}" 链接
            const name = node.getAttribute("name") || "";
            if (name.startsWith("OLE_LINK")) {
                text = text.replaceAll("\n", "");
            }

            // 如果链接和文本都为空,则返回空字符串
            if (text === "" && href === "") return "";
            return `${this.SEPARATION_BEAUTIFICATION}[${text}](${href})${this.SEPARATION_BEAUTIFICATION}`;
        }

        /**
         * 处理图片元素
         */
        async handleImage(node, context) {
            let src = node.getAttribute("src") || "";
            const alt = node.getAttribute("alt") || "";
            const cls = node.getAttribute("class") || "";
            const width = node.getAttribute("width") || "";
            const height = node.getAttribute("height") || "";
            let result = "";

            // 处理数学代码图片
            if (cls.includes("mathcode")) {
                return `${this.SEPARATION_BEAUTIFICATION}\$\$\n${alt}\n\$\$`;
            } else {
                // 根据图片是否居中添加空格
                if (src.includes("#pic_center") || context.forceImageCentering) {
                    result = this.CONSTANT_DOUBLE_NEW_LINE;
                } else {
                    result = " ";
                }

                // 保存网络图片到本地(如果配置启用)
                if (context.saveWebImages) {
                    src = await this.fileManager.addWebImageFile(
                        src,
                        context.assetDirName,
                        context.imgPrefix,
                        context.enableStreaming,
                        context.downloadAssetRetryCount,
                        context.downloadAssetRetryDelay
                    );
                }

                // 处理图片尺寸
                if (height && context.enableImageSize) {
                    // 如果 height 是数字,则添加 px;如果带有单位,则直接使用
                    const heightValue = height.replace(/[^0-9]/g, "");
                    const heightUnit = height.replace(/[0-9]/g, "") || "px";
                    const heightStyle = heightValue ? `max-height:${heightValue}${heightUnit};` : "";
                    result += `<img src="${src}" alt="${alt}" style="${heightStyle} box-sizing:content-box;" />`;
                } else if (width && context.enableImageSize) {
                    // 如果 width 是数字,则添加 px;如果带有单位,则直接使用
                    const widthValue = width.replace(/[^0-9]/g, "");
                    const widthUnit = width.replace(/[0-9]/g, "") || "px";
                    const widthStyle = widthValue ? `max-width:${widthValue}${widthUnit};` : "";
                    result += `<img src="${src}" alt="${alt}" style="${widthStyle} box-sizing:content-box;" />`;
                } else {
                    result += `![${alt}](${src})`;
                }

                return result + this.CONSTANT_DOUBLE_NEW_LINE;
            }
        }

        /**
         * 处理列表元素(ul/ol)
         */
        async handleList(node, context) {
            const ordered = node.tagName.toLowerCase() === "ol";
            // 创建新的上下文,增加列表嵌套级别
            const newContext = { ...context, listLevel: context.listLevel + 1 };

            let result = this.CONSTANT_DOUBLE_NEW_LINE;
            // 筛选出所有li元素
            const children = Array.from(node.children).filter((child) => child.tagName.toLowerCase() === "li");

            for (let index = 0; index < children.length; index++) {
                const child = children[index];

                // 根据列表类型选择前缀和缩进
                const prefix = ordered ? `${index + 1}. ` : `- `;
                const indent = ordered ? "   " : "  ";

                let childText = await this.processChildren(child, newContext);

                // 处理嵌套列表的换行和缩进
                childText = childText.replaceAll(this.RE_DOUBLE_NL, "\n\n");

                // 对除第一行外的所有行添加缩进
                childText = childText
                    .split("\n")
                    .map((line, i) => {
                        // 如果是空行或首行,则不添加缩进
                        if (line.trim() === "" || i === 0) {
                            return line;
                        }
                        return `${indent}${line}`;
                    })
                    .join("\n");

                result += `${prefix}${childText}${this.CONSTANT_DOUBLE_NEW_LINE}`;
            }

            return result;
        }

        /**
         * 处理引用块元素
         */
        async handleBlockquote(node, context) {
            // 处理每一行,添加引用标记 >
            const text = (await this.processChildren(node, context))
                .trim()
                .split("\n")
                .map((line) => (line ? `> ${line}` : "> "))
                .join("\n");

            return `${text}\n\n`;
        }

        /**
         * 处理预格式化代码块
         */
        async handlePreformatted(node, context) {
            const codeNode = node.querySelector("code");
            if (codeNode) {
                const className = codeNode.className || "";
                let language = "";

                // 提取语言信息
                // 新版本的代码块,class含有language-xxx
                if (className.includes("language-")) {
                    for (const item of className.split(" ")) {
                        if (item.startsWith("language-")) {
                            language = item.replace("language-", "");
                            break;
                        }
                    }
                }
                // 老版本的代码块
                else if (className.startsWith("hljs")) {
                    const languageMatch = className.split(" ");
                    language = languageMatch.length > 1 ? languageMatch[1] : "";
                }

                return `\`\`\`${language}\n${await this.processCodeBlock(codeNode)}\`\`\`\n\n`;
            } else {
                const codeText = node.textContent.replace(/^\s+|\s+$/g, "");
                return `\`\`\`\n${codeText}\n\`\`\`\n\n`;
            }
        }

        /**
         * 处理行内代码元素
         */
        async handleCode(node, context) {
            const codeText = node.textContent;
            return `${this.SEPARATION_BEAUTIFICATION}\`${codeText}\`${this.SEPARATION_BEAUTIFICATION}`;
        }

        /**
         * 处理水平分割线元素
         */
        async handleHorizontalRule(node, context) {
            if (node.getAttribute("id") !== "hr-toc") {
                return `---\n\n`;
            }
            return "";
        }

        /**
         * 处理换行元素
         */
        async handleLineBreak(node, context) {
            return `\n`;
        }

        /**
         * 处理表格元素
         */
        async handleTable(node, context) {
            const rows = Array.from(node.querySelectorAll("tr"));
            if (rows.length === 0) return "";

            let table = "";

            // 处理表头
            const headerCells = Array.from(rows[0].querySelectorAll("th, td"));

            const headers = await Promise.all(
                headerCells.map(async (cell) => {
                    const content = await this.processNode(cell, context);
                    return content.trim().replaceAll(this.RE_DOUBLE_NL, "<br />");
                })
            );

            table += `| ${headers.join(" | ")} |\n`;

            // 处理分隔符行(对齐方式)
            const alignments = headerCells.map((cell) => {
                const align = cell.getAttribute("align");
                if (align === "center") return ":---:";
                if (align === "right") return "---:";
                if (align === "left") return ":---";
                return ":---:"; // 默认居中
            });

            table += `|${alignments.join("|")}|\n`;

            // 处理表格内容行
            for (let i = 1; i < rows.length; i++) {
                const cells = Array.from(rows[i].querySelectorAll("td"));
                const rowContent = await Promise.all(
                    cells.map(async (cell) => {
                        const content = await this.processNode(cell, context);
                        return content.trim().replaceAll(this.RE_DOUBLE_NL, "<br />");
                    })
                );

                table += `| ${rowContent.join(" | ")} |`;
                if (i < rows.length - 1) {
                    table += "\n";
                }
            }

            return table + "\n\n";
        }

        /**
         * 处理div元素
         */
        async handleDiv(node, context) {
            const className = node.getAttribute("class") || "";

            // 处理视频盒子
            if (className.includes("csdn-video-box")) {
                const iframe = node.querySelector("iframe");
                if (iframe) {
                    const src = iframe.getAttribute("src") || "";
                    const titleElem = node.querySelector("p");
                    const title = titleElem ? titleElem.textContent || "" : "";

                    const iframeHTML = iframe.outerHTML.replace(
                        "></iframe>",
                        ' style="width: 100%; aspect-ratio: 2;"></iframe>'
                    );

                    return `<div align="center" style="border: 3px solid gray;border-radius: 27px;overflow: hidden;"> <a class="link-info" href="${src}" rel="nofollow" title="${title}">${title}</a>${iframeHTML}</div>\n\n`;
                }
            }
            // 处理目录
            else if (className.includes("toc")) {
                if (context.enableTOC) {
                    const titleElem = node.querySelector("h4");
                    const customTitle = titleElem ? titleElem.textContent || "" : "";
                    return `**${customTitle}**\n\n[TOC]\n\n`;
                }
            }

            return `${await this.processChildren(node, context)}\n`;
        }

        /**
         * 处理span元素
         */
        async handleSpan(node, context) {
            const nodeClass = node.getAttribute("class");

            // 处理KaTeX数学公式
            if (nodeClass) {
                if (nodeClass.includes("katex--inline") || nodeClass.includes("katex--display")) {
                    return this.handleKatexElement(node, nodeClass);
                }
            }

            // 处理带颜色的文本
            const style = node.getAttribute("style") || "";
            if ((style.includes("background-color") || style.includes("color")) && context.enableColorText) {
                if (node.childNodes.length === 1 && node.childNodes[0].nodeType === this.TEXT_NODE) {
                    return `<span style="${style}">${await this.processChildren(node, context)}</span>`;
                }
            }

            return await this.processChildren(node, context);
        }

        /**
         * 处理KaTeX数学公式元素
         */
        handleKatexElement(node, nodeClass) {
            const katexMathmlElem = node.querySelector(".katex-mathml");
            const katexHtmlElem = node.querySelector(".katex-html");

            if (!katexMathmlElem || !katexHtmlElem) return "";

            // 清理KaTeX元素
            this.cleanKatexElements(katexMathmlElem);

            const mathml = Utils.clearSpecialChars(katexMathmlElem.textContent);
            const katexHtml = Utils.clearSpecialChars(katexHtmlElem.textContent);

            // 处理行内公式和行间公式
            if (nodeClass.includes("katex--inline")) {
                // 行内公式
                if (mathml.startsWith(katexHtml)) {
                    return `${this.SEPARATION_BEAUTIFICATION}\$${mathml.replace(katexHtml, "")}\$${
                        this.SEPARATION_BEAUTIFICATION
                    }`;
                } else {
                    return `${this.SEPARATION_BEAUTIFICATION}\$${Utils.clearKatexMathML(
                        katexMathmlElem.textContent
                    )}\$${this.SEPARATION_BEAUTIFICATION}`;
                }
            } else {
                // 行间公式
                if (mathml.startsWith(katexHtml)) {
                    return `${this.CONSTANT_DOUBLE_NEW_LINE}\$\$\n${mathml.replace(katexHtml, "")}\n\$\$${
                        this.CONSTANT_DOUBLE_NEW_LINE
                    }`;
                } else {
                    return `${this.CONSTANT_DOUBLE_NEW_LINE}\$\$\n${Utils.clearKatexMathML(
                        katexMathmlElem.textContent
                    )}\n\$\$${this.CONSTANT_DOUBLE_NEW_LINE}`;
                }
            }
        }

        /**
         * 清理KaTeX元素
         * 移除可能导致公式显示错乱的元素
         */
        cleanKatexElements(katexMathmlElem) {
            const elementsToRemove = [".MathJax_Display", ".MathJax_Preview", ".MathJax_Error"];

            elementsToRemove.forEach((selector) => {
                if (katexMathmlElem.querySelector(selector) && katexMathmlElem.querySelector("script")) {
                    katexMathmlElem.querySelectorAll(selector).forEach((elem) => elem.remove());
                }
            });
        }

        /**
         * 处理键盘按键元素
         */
        async handleKeyboard(node, context) {
            return `${this.SEPARATION_BEAUTIFICATION}<kbd>${node.textContent}</kbd>${this.SEPARATION_BEAUTIFICATION}`;
        }

        /**
         * 处理标记(高亮)元素
         */
        async handleMark(node, context) {
            return `${this.SEPARATION_BEAUTIFICATION}<mark>${await this.processChildren(node, context)}</mark>${
                this.SEPARATION_BEAUTIFICATION
            }`;
        }

        /**
         * 处理下标元素
         */
        async handleSubscript(node, context) {
            return `<sub>${await this.processChildren(node, context)}</sub>`;
        }

        /**
         * 处理上标元素
         */
        async handleSuperscript(node, context) {
            const nodeClass = node.getAttribute("class");
            // 处理脚注引用
            if (nodeClass && nodeClass.includes("footnote-ref")) {
                return `[^${node.textContent}]`;
            } else {
                return `<sup>${await this.processChildren(node, context)}</sup>`;
            }
        }

        /**
         * 处理SVG元素
         */
        async handleSvg(node, context) {
            const style = node.getAttribute("style");
            if (style && style.includes("display: none")) {
                return "";
            }

            // 为foreignObject里的div添加属性xmlns="http://www.w3.org/1999/xhtml",否则typora无法识别
            const foreignObjects = node.querySelectorAll("foreignObject");
            for (const foreignObject of foreignObjects) {
                const divs = foreignObject.querySelectorAll("div");
                divs.forEach((div) => {
                    div.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
                });
            }

            // 保存SVG图像
            if (context.saveWebImages) {
                const svgSavePath = await this.fileManager.addSvgFile(
                    node.outerHTML,
                    context.assetDirName,
                    context.imgPrefix,
                    context.enableStreaming
                );
                return `![](${svgSavePath})${this.CONSTANT_DOUBLE_NEW_LINE}`;
            } else {
                // 检查是否有style标签存在于svg元素内,如果有则转换为base64形式
                if (node.querySelector("style")) {
                    const base64 = Utils.svgToBase64(node.outerHTML);
                    return `![](data:image/svg+xml;base64,${base64})${this.CONSTANT_DOUBLE_NEW_LINE}`;
                } else {
                    return `<div align="center">${node.outerHTML}</div>${this.CONSTANT_DOUBLE_NEW_LINE}`;
                }
            }
        }

        /**
         * 处理section元素
         */
        async handleSection(node, context) {
            const nodeClass = node.getAttribute("class");
            // 处理脚注内容
            if (nodeClass && nodeClass.includes("footnotes")) {
                return await this.processFootnotes(node);
            }
            return await this.processChildren(node, context);
        }

        /**
         * 处理input元素
         */
        async handleInput(node, context) {
            // 仅处理checkbox类型的input元素
            if (node.getAttribute("type") === "checkbox") {
                return `[${node.checked ? "x" : " "}] `;
            }
            return "";
        }

        /**
         * 处理定义列表元素
         */
        async handleDefinitionList(node, context) {
            // 自定义列表,直接用HTML
            return `${Utils.shrinkHtml(node.outerHTML)}\n\n`;
        }

        /**
         * 处理缩写元素
         */
        async handleAbbreviation(node, context) {
            return Utils.shrinkHtml(node.outerHTML);
        }

        /**
         * 处理字体元素
         */
        async handleFont(node, context) {
            // 避免进入 default,直接处理子元素
            return await this.processChildren(node, context);
        }

        /**
         * 处理表格单元格元素
         */
        async handleTableCell(node, context) {
            return await this.processChildren(node, context);
        }

        /**
         * 处理居中元素
         */
        async handleCenter(node, context) {
            if (node.childNodes.length === 1 && node.childNodes[0].nodeType === this.TEXT_NODE) {
                // 只有一个文本子节点时,使用center标签
                return `<center>${node.textContent.trim().replaceAll("\n", "<br>")}</center>\n\n`;
            } else {
                // 处理含有图片的居中标签,为图片添加#pic_center后缀
                this.addPicCenterToImages(node);
                return (await this.processChildren(node, context)) + this.CONSTANT_DOUBLE_NEW_LINE;
            }
        }

        /**
         * 默认元素处理器,用于没有特定处理器的元素
         */
        async handleDefaultElement(node, context) {
            return (await this.processChildren(node, context)) + this.CONSTANT_DOUBLE_NEW_LINE;
        }

        /****************************************
         * 辅助方法
         ****************************************/

        /**
         * 为图片添加#pic_center后缀以实现居中效果
         */
        addPicCenterToImages(node) {
            node.querySelectorAll("img").forEach((img) => {
                const src = img.getAttribute("src");
                if (src && !src.includes("#pic_center")) {
                    img.setAttribute("src", src + "#pic_center");
                }
            });
        }

        /**
         * 处理代码块内容
         * 支持新旧两种代码块格式
         */
        async processCodeBlock(codeNode) {
            // 查找code内部是否有ol元素(新版代码块格式)
            const olNode = codeNode.querySelector("ol");

            if (!olNode || olNode.tagName.toLowerCase() !== "ol") {
                // 老版本的代码块,直接返回文本内容
                return codeNode.textContent.replace(/\n$/, "") + "\n";
            }

            // 新版本的代码块,处理每行代码
            const listItems = olNode.querySelectorAll("li");
            let result = "";

            // 遍历每个<li>元素(每行代码)
            listItems.forEach((li) => {
                result += li.textContent + "\n";
            });

            return result;
        }

        /**
         * 处理脚注
         * 将脚注列表转换为Markdown格式
         */
        async processFootnotes(node) {
            const footnotes = Array.from(node.querySelectorAll("li"));
            let result = "";

            for (let index = 0; index < footnotes.length; index++) {
                const li = footnotes[index];
                // 移除换行和返回符号,格式化脚注内容
                const text = (await this.processNode(li, {})).replaceAll("\n", " ").replaceAll("↩︎", "").trim();

                result += `[^${index + 1}]: ${text}\n`;
            }

            return result;
        }
    }

    /**
     * 模块: 文章下载管理
     * 协调各模块完成文章下载功能
     */
    class ArticleDownloader {
        /**
         * @param {UIManager} [uiManager=null] - UI管理器实例
         */
        constructor(uiManager = null) {
            /** @type {FileManager} */
            this.fileManager = new FileManager();
            /** @type {MarkdownConverter} */
            this.markdownConverter = new MarkdownConverter(this.fileManager);
            /** @type {UIManager} */
            this.uiManager = uiManager;
        }

        /**
         * 设置UI管理器
         * @param {UIManager} uiManager - UI管理器实例
         **/
        setUIManager(uiManager) {
            this.uiManager = uiManager;
        }

        reset() {
            this.fileManager.reset();
        }

        async unfoldHideArticleBox(document_body) {
            // 展开隐藏的文章内容
            const hideArticleBox = document_body.querySelector(".hide-article-box");
            if (!hideArticleBox) return;

            const readAllContentBtn = hideArticleBox.querySelector(".read-all-content-btn");
            if (!readAllContentBtn) return;

            readAllContentBtn.click();
            console.dir("已展开隐藏的文章内容。");

            // 动态等待 #article_content 加载完成
            const articleContent = document_body.querySelector("#article_content");
            if (!articleContent) {
                throw new Error("未找到文章内容元素 #article_content");
            }

            // 创建动态等待函数
            const waitForContentStable = (element, timeout = 30000, stabilityDelay = 1000) => {
                return new Promise((resolve, reject) => {
                    let stabilityTimer = null;
                    let timeoutTimer = null;
                    const observer = new MutationObserver(() => {
                        if (timeoutTimer) {
                            clearTimeout(timeoutTimer);
                            timeoutTimer = null; // 清除超时计时器
                        }
                        if (stabilityTimer) clearTimeout(stabilityTimer);
                        stabilityTimer = setTimeout(resolve, stabilityDelay); // 重置稳定倒计时
                    });
                    observer.observe(element, {
                        childList: true, // 监听子元素变化
                        subtree: true, // 监听所有后代
                        attributes: true, // 监听属性变化
                    });
                    // 设置超时强制返回
                    setTimeout(() => {
                        observer.disconnect();
                        reject(new Error(`等待加载超时 (${timeout}ms)`));
                    }, timeout);
                });
            };
            await waitForContentStable(articleContent);
            console.dir("内容展开完成");
        }

        /**
         * 解析网页并转换为Markdown格式
         * @param {Document} doc_body - 文章的body元素
         * @param {Object} config - 配置选项
         * @param {string} config.articleUrl - 文章链接
         * @param {number} config.fileIndex - 文件索引(用于批量下载)
         * @param {number} config.fileTotal - 文件总数(用于批量下载)
         * @param {boolean} config.enableStreaming - 是否启用流式处理
         * @param {boolean} config.mergeArticleContent - 是否合并文章内容
         * @param {boolean} config.saveAllImagesToAssets - 是否将所有图片保存
         * @param {boolean} config.addSerialNumberToTitle - 是否在标题前添加序号
         * @param {boolean} config.addArticleInfoInBlockquote - 是否在引用块中添加文章信息
         * @param {boolean} config.addArticleTitleToMarkdown - 是否在Markdown中添加文章标题
         * @param {boolean} config.addArticleInfoInYaml - 是否在YAML中添加文章信息
         * @param {boolean} config.saveWebImages - 是否保存网络图片
         * @param {number} config.downloadAssetRetryCount - 下载网络图片的重试次数
         * @param {boolean} config.forceImageCentering - 是否强制图片居中
         * @param {boolean} config.enableImageSize - 是否启用图片尺寸
         * @param {boolean} config.enableColorText - 是否启用彩色文本
         * @param {boolean} config.removeCSDNSearchLink - 是否移除CSDN搜索链接
         * @param {string} config.customFileNamePattern - 是否使用自定义文件名模式
         * @param {boolean} config.enableCustomFileName - 是否启用自定义文件名
         * @returns {Promise<string>} 解析后的文章标题
         * @throws {Error} 如果未找到文章内容
         */
        async parseArticle(doc_body, config = {}) {
            const { articleUrl = "", fileIndex = 0, fileTotal = 1 } = config;

            await this.unfoldHideArticleBox(doc_body);
            const articleTitle = doc_body.querySelector("#articleContentId")?.textContent.trim() || "未命名文章";
            const articleAuthor = doc_body.querySelector("#uid")?.textContent.trim() || "";
            const articleInfo =
                doc_body
                    .querySelector(".bar-content")
                    ?.textContent.replace(/\s{2,}/g, " ")
                    .trim() || "";
            const htmlInput = doc_body.querySelector("#content_views");
            if (!htmlInput) throw new Error("未找到文章内容。请检查网页结构是否发生变化。");

            const padNo = `${String(fileIndex).padStart(fileTotal.toString().length, "0")}`;
            let fileName = articleTitle;
            if (fileIndex > 0 && config.enableCustomFileName) {
                fileName = config.customFileNamePattern
                    .replaceAll("{no}", padNo)
                    .replaceAll("{title}", articleTitle)
                    .replaceAll("{author}", articleAuthor)
                    .replaceAll("{index}", fileIndex.toString());
            }

            this.uiManager.showFloatTip(
                `正在解析文章:(${fileTotal - Math.max(1, fileIndex) + 1}/${fileTotal}) ` + articleTitle
            );
            let markdown = await this.markdownConverter.htmlToMarkdown(htmlInput, {
                assetDirName: config.mergeArticleContent || config.saveAllImagesToAssets ? "assets" : fileName,
                enableTOC: !config.mergeArticleContent,
                imgPrefix: `${padNo}_`,
                ...config, // 传入其他配置选项
            });

            if (config.addArticleInfoInBlockquote) {
                markdown = `> ${articleInfo}\n> 文章链接:${Utils.clearUrl(articleUrl)}\n\n${markdown}`;
            }
            if (config.addArticleTitleToMarkdown) {
                if (config.addSerialNumberToTitle) {
                    markdown = `# ${padNo} ${articleTitle}\n\n${markdown}`;
                } else {
                    markdown = `# ${articleTitle}\n\n${markdown}`;
                }
            }
            if (config.addArticleInfoInYaml) {
                const article_info_box = doc_body.querySelector(".article-info-box");
                // 文章标题
                const meta_title = config.addSerialNumberToTitle ? `${padNo} ${articleTitle}` : articleTitle;
                // 文章日期 YYYY-MM-DD HH:MM:SS
                const meta_date =
                    article_info_box
                        ?.querySelector(".time")
                        ?.textContent.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/)[0] || "";
                let articleMeta = `title: ${meta_title}\ndate: ${meta_date}\n`;
                // 文章分类和标签
                if (article_info_box) {
                    const meta_category_and_tags = Array.from(article_info_box.querySelectorAll(".tag-link")) || [];
                    if (meta_category_and_tags.length > 0 && article_info_box.textContent.includes("分类专栏")) {
                        articleMeta += `categories:\n- ${meta_category_and_tags[0].textContent}\n`;
                        meta_category_and_tags.shift();
                    }
                    if (meta_category_and_tags.length > 0 && article_info_box.textContent.includes("文章标签")) {
                        articleMeta += `tags:\n${Array.from(meta_category_and_tags)
                            .map((tag) => `- ${tag.textContent}`)
                            .join("\n")}\n`;
                    }
                }
                markdown = `---\n${articleMeta}---\n\n${markdown}`;
            }

            // 这里不启用流式压缩,在解析结束时会统一压缩,文本占用内存比较小
            await this.fileManager.addTextFile(markdown, `${fileName}.md`, fileIndex, false);

            return articleTitle;
        }

        /**
         * 在iframe中下载文章
         * @param {string} url - 文章URL
         * @param {object} config - 配置选项
         * @returns {Promise<void>}
         */
        async downloadArticleInIframe(url, config = {}) {
            return new Promise((resolve, reject) => {
                const originalUrl = url; // 保存原始URL
                let isRedirected = false; // 重置重定向标志

                const hasCaptcha = (doc) => {
                    return doc.body.querySelector(".text-wrap")?.textContent.includes("安全验证");
                };

                const onCheckPassed = () => {
                    // 创建一个隐藏的iframe
                    const iframe = document.createElement("iframe");
                    const showIframe = (iframe_element) => {
                        iframe_element.style.display = "block"; // 显示iframe
                        iframe_element.style.position = "fixed";
                        iframe_element.style.top = "50%";
                        iframe_element.style.left = "50%";
                        iframe_element.style.transform = "translate(-50%, -50%)";
                        iframe_element.style.width = "80vw";
                        iframe_element.style.height = "80vh";
                        iframe_element.style.zIndex = "99999";
                        iframe_element.style.background = "#fff";
                        iframe_element.style.boxShadow = "0 4px 24px rgba(0,0,0,0.18)";
                        iframe_element.style.border = "2px solid #12c2e9";
                        iframe_element.style.borderRadius = "12px";
                    };
                    const hideIframe = (iframe_element) => {
                        iframe_element.style.display = "none"; // 隐藏iframe
                    };
                    hideIframe(iframe); // 初始隐藏iframe
                    document.body.appendChild(iframe);
                    iframe.src = url;

                    // 监听iframe加载完成事件
                    iframe.onload = async () => {
                        console.dir(`iframe加载完成,开始下载文章: Url: ${url}`);
                        try {
                            const doc = iframe.contentDocument || iframe.contentWindow.document;

                            // 检查是否有验证码
                            if (hasCaptcha(doc)) {
                                console.dir(`检测到验证码: Url: ${url}`);
                                await this.uiManager.showConfirmDialog(
                                    {
                                        title: "ℹ️ 提示",
                                        message: `检测到验证码,您需要手动验证通过后,再刷新页面重新进行下载。\n点击确认将显示该验证页面,若取消则无法下载。\nUrl: ${url}`,
                                    },
                                    async () => {
                                        // 用户点击确认后,重新加载iframe
                                        console.dir(`用户确认验证码处理: Url: ${url}`);
                                        showIframe(iframe);
                                    },
                                    () => {
                                        // 用户点击取消后,移除iframe并拒绝Promise
                                        console.dir(`用户取消验证码处理: Url: ${url}`);
                                        document.body.removeChild(iframe);
                                    }
                                );
                                return;
                            }

                            // 调用解析函数
                            await this.parseArticle(doc.body, {
                                ...config, // 传入其他配置选项
                                articleUrl: url,
                            });
                            // 移除iframe
                            document.body.removeChild(iframe);
                            resolve();
                        } catch (error) {
                            // 在发生错误时移除iframe并拒绝Promise
                            document.body.removeChild(iframe);
                            console.dir(
                                `解析文章时出错: Url: ${url} OriginalUrl: ${originalUrl} Redirected: ${isRedirected}. Original error: ${
                                    error.message || error
                                }`
                            );
                            const newError = new Error(
                                `解析文章时出错:Url: ${url} OriginalUrl: ${originalUrl} Redirected: ${isRedirected}. Original error: ${
                                    error.message || error
                                }`
                            );
                            newError.stack = error.stack;
                            reject(newError);
                        }
                    };

                    // 监听iframe加载错误事件
                    iframe.onerror = (error) => {
                        document.body.removeChild(iframe);
                        console.dir(
                            `Iframe加载失败: Url: ${url} OriginalUrl: ${originalUrl} Redirected: ${isRedirected}. Original error: ${
                                error.message || error
                            }`
                        );
                        const newError = new Error(
                            `Iframe加载失败:Url: ${url} OriginalUrl: ${originalUrl} Redirected: ${isRedirected}. Original error: ${
                                error.message || error
                            }`
                        );
                        newError.stack = error.stack || new Error().stack;
                        reject(error);
                    };
                };

                const uiManager = this.uiManager;

                // FIX: 使用 GM_xmlhttpRequest 检测是否存在重定向
                // https://github.com/Qalxry/csdn2md/issues/6
                // https://github.com/Qalxry/csdn2md/issues/7
                GM_xmlhttpRequest({
                    method: "HEAD",
                    url: url,
                    redirect: "manual", // 禁止自动重定向
                    onload: async function (response) {
                        if (response.status === 301 || response.status === 302) {
                            const redirectUrl = response.responseHeaders.match(/Location:\s*(.+)/i)?.[1];
                            console.dir(`检测到重定向: ${url} -> ${redirectUrl}`);
                            isRedirected = true; // 设置重定向标志
                            // 将 http 替换为 https
                            url = redirectUrl.replace(/^http:\/\//, "https://");
                        } else if (response.status !== 200) {
                            console.dir(`文章页面状态码异常:Url: ${url} Response Status: ${response.status}`);
                            // 这里的检测无意义,反而导致用户体验变差
                            // 因为有些文章页面会返回521状态码,但实际可以下载
                            // if (response.status === 521) {
                            //     console.dir(
                            //         `检查文章 ${url} 时状态码异常:${response.status},有风控的可能性。`
                            //     );
                            // } else {
                            //     await uiManager.showConfirmDialog(
                            //         `检查文章 ${url} 时状态码异常:${response.status},是否继续下载?\n(这里只是用HEAD方法预先检查了一下,也许下载时会成功)`,
                            //         () => {
                            //             // 用户点击确认后,继续下载
                            //             console.dir(`用户确认继续下载: Url: ${url}`);
                            //         },
                            //         () => {
                            //             const newError = new Error(
                            //                 `文章页面状态码异常:Url: ${url} Response Status: ${response.status}`
                            //             );
                            //             reject(newError);
                            //         }
                            //     );
                            // }
                        } else {
                            console.dir(`文章页面检查成功:${url}`);
                        }
                        onCheckPassed(); // 检测通过,开始下载
                    },
                    onerror: function (error) {
                        console.dir(`检查文章页面失败: Url: ${url}. Original error: ${error.message || error}`);
                        const newError = new Error(
                            `检查文章页面失败:Url: ${url}. Original error: ${error.message || error}`
                        );
                        newError.stack = error.stack;
                        reject(error);
                    },
                });
            });
        }

        /**
         * 从URL批量下载文章
         * @param {string} url - 文章URL
         * @param {number} index - 文件前缀
         * @param {Object} config - 配置选项
         * @param {boolean} config.fastDownload - 是否快速下载
         * @return {Promise<void>}
         * @throws {Error} 如果下载失败或解析文章时出错
         */
        async downloadOneArticleFromBatch(url, index, total, config = {}) {
            return new Promise(async (resolve, reject) => {
                try {
                    const newConfig = {
                        ...config, // 传入其他配置选项
                        articleUrl: url,
                        fileIndex: index,
                        fileTotal: total,
                    };
                    if (config.fastDownload) {
                        const response = await (await fetch(url)).text();
                        // const response = await this.fileManager.fetchResource(
                        //     url,
                        //     "text/html",
                        //     config.downloadAssetRetryCount || 3,
                        //     config.downloadAssetRetryDelay || 1000,
                        //     "fetch"
                        // );
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(response, "text/html");
                        // 调用解析函数
                        await this.parseArticle(doc.body, newConfig);
                    } else {
                        await this.downloadArticleInIframe(url, newConfig);
                    }
                } catch (error) {
                    reject(error);
                }
                if (config.delayBetweenDownloads && config.delayBetweenDownloads > 0) {
                    setTimeout(() => resolve(), config.delayBetweenDownloads);
                } else {
                    resolve();
                }
            });
        }

        /**
         * 下载专栏的全部文章为Markdown格式
         * @param {Object} config - 配置选项
         * @param {boolean} config.zipCategories - 是否将文章打包成zip
         * @param {string} config.filePrefix - 文件名前缀
         * @param {string} config.articleUrl - 文章链接
         * @param {number} config.maxConcurrentDownloads - 最大并发下载数
         * @param {boolean} config.parallelDownload - 是否并行下载
         * @param {boolean} config.fastDownload - 是否快速下载
         * @param {boolean} config.mergeArticleContent - 是否合并文章内容
         * @param {boolean} config.saveAllImagesToAssets - 是否将所有图片保存
         * @param {boolean} config.addSerialNumber - 是否添加序号
         * @param {boolean} config.addSerialNumberToTitle - 是否在标题前添加序号
         * @param {boolean} config.addArticleInfoInBlockquote - 是否在引用块中添加文章信息
         * @param {boolean} config.addArticleTitleToMarkdown - 是否在Markdown中添加文章标题
         * @param {boolean} config.addArticleInfoInYaml - 是否在YAML中添加文章信息
         * @param {boolean} config.saveWebImages - 是否保存网络图片
         * @param {boolean} config.forceImageCentering - 是否强制图片居中
         * @param {boolean} config.enableImageSize - 是否启用图片尺寸
         * @param {boolean} config.enableColorText - 是否启用彩色文本
         * @param {boolean} config.removeCSDNSearchLink - 是否移除CSDN搜索链接
         * @param {boolean} config.addArticleInfoInBlockquote_batch - 是否在批量下载时在引用块中添加文章信息
         **/
        async downloadCategory(config = {}) {
            // 获取专栏id,注意url可能是/category_数字.html或/category_数字_数字.html,需要第一个数字
            this.uiManager.showFloatTip("正在获取专栏的全部文章链接...");
            const base_url = window.location.href;
            const category_id = base_url.match(/category_(\d+)(?:_\d+)?\.html/)[1];
            const url_list = [];
            let page = 1;
            let doc_body = document.body;

            while (true) {
                let hasNextArticle = false;
                // 获取当前页面的文章列表
                doc_body
                    .querySelector(".column_article_list")
                    .querySelectorAll("a")
                    .forEach((item) => {
                        url_list.push(item.href);
                        hasNextArticle = true;
                    });
                if (!hasNextArticle) break;

                // 下一页
                page++;
                const next_url = base_url.replace(
                    /category_\d+(?:_\d+)?\.html/,
                    `category_${category_id}_${page}.html`
                );
                const response = await fetch(next_url);
                const text = await response.text();
                const parser = new DOMParser();
                doc_body = parser.parseFromString(text, "text/html").body;
            }

            if (url_list.length === 0) {
                this.uiManager.showFloatTip("没有找到文章。");
                return;
            } else {
                this.uiManager.showFloatTip(`找到 ${url_list.length} 篇文章。开始解析...`);
            }

            // FIX: 解决自定义域名在Chrome里下载专栏时,专栏和文章hostname不一致导致跨域问题
            // https://github.com/Qalxry/csdn2md/issues/7
            // 专栏的 url 为 https://blog.csdn.net/{user_id}/category_{category_id}.html
            // 文章的 url 为 https://{custom_domain}.blog.csdn.net/article/details/{article_id}
            //
            // > 方案1:将文章的 url 替换为 https://blog.csdn.net/{user_id}/article/details/{article_id}
            // 会引发 CSDN 的安全策略问题,废弃
            // if (base_url.startsWith("https://blog.csdn.net/")) {
            //     const user_id = base_url.match(/blog\.csdn\.net\/([^\/]+)/)[1];
            //     for (let i = 0; i < url_list.length; i++) {
            //         if (!url_list[i].startsWith("https://blog.csdn.net/")) {
            //             const article_id = url_list[i].match(/\/article\/details\/([^\/]+)/)[1];
            //             url_list[i] = `https://blog.csdn.net/${user_id}/article/details/${article_id}`;
            //         }
            //     }
            // }
            // > 方案2:将专栏的 url 替换为 https://{custom_domain}.blog.csdn.net/category_{category_id}.html
            // 虽然有效,但不确定是否稳定,目前来看可以
            let isAllArticlesCustomDomain = true;
            let isAllArticlesDefaultDomain = true;
            for (let i = 0; i < url_list.length; i++) {
                if (url_list[i].startsWith("https://blog.csdn.net/")) {
                    isAllArticlesCustomDomain = false;
                    break;
                }
            }
            for (let i = 0; i < url_list.length; i++) {
                if (!url_list[i].startsWith("https://blog.csdn.net/")) {
                    isAllArticlesDefaultDomain = false;
                    break;
                }
            }
            if (isAllArticlesCustomDomain) {
                // 如果全部文章都是自定义域名,则将专栏的 url 替换为 https://{custom_domain}.blog.csdn.net/category_{category_id}.html
                if (base_url.startsWith("https://blog.csdn.net/")) {
                    console.dir(
                        `Warning: 文章与专栏的域名不一致,正在将专栏的URL替换为自定义域名。当前专栏URL: ${base_url} 文章URL: ${url_list[0]}`
                    );
                    const custom_domain = url_list[0].match(/https:\/\/([^\/]+)\.blog\.csdn\.net/)[1];
                    GM_setValue("status", {
                        timestamp: Date.now(),
                        action: "downloadCategory",
                        targetUrl: `https://${custom_domain}.blog.csdn.net/category_${category_id}.html`,
                    });
                    window.location.href = `https://${custom_domain}.blog.csdn.net/category_${category_id}.html`;
                }
            } else if (isAllArticlesDefaultDomain) {
                // 如果全部文章都是默认域名,则将专栏的 url 替换为 https://blog.csdn.net/category_{category_id}.html
                if (!base_url.startsWith("https://blog.csdn.net/")) {
                    console.dir(
                        `Warning: 文章与专栏的域名不一致,正在将专栏的URL替换为默认域名。当前专栏URL: ${base_url} 文章URL: ${url_list[0]}`
                    );
                    const user_id = url_list[0].match(/blog\.csdn\.net\/([^\/]+)/)[1];
                    GM_setValue("status", {
                        timestamp: Date.now(),
                        action: "downloadCategory",
                        targetUrl: `https://blog.csdn.net/${user_id}/category_${category_id}.html`,
                    });
                    window.location.href = `https://blog.csdn.net/${user_id}/category_${category_id}.html`;
                }
            } else {
                // 如果文章的域名不一致,则回退为方案1,至少可能可以下载
                console.dir(
                    `Warning: 文章与专栏的域名不一致,可能无法下载。请检查是否有自定义域名。当前专栏URL: ${base_url}`
                );
                if (base_url.startsWith("https://blog.csdn.net/")) {
                    const user_id = base_url.match(/blog\.csdn\.net\/([^\/]+)/)[1];
                    for (let i = 0; i < url_list.length; i++) {
                        if (!url_list[i].startsWith("https://blog.csdn.net/")) {
                            const article_id = url_list[i].match(/\/article\/details\/([^\/]+)/)[1];
                            url_list[i] = `https://blog.csdn.net/${user_id}/article/details/${article_id}`;
                        }
                    }
                }
            }

            if (config.enableStreaming) {
                this.uiManager.showFloatTip("正在初始化流式下载...");
                await this.fileManager.initializeZipStream(
                    `${document.title}`,
                    (fileName, fileIndex) => {},
                    (zipStreamName, zipStreamFileCount, zipStreamSize) => {
                        this.uiManager.showFloatTip(
                            `打包完成:${zipStreamName}。文件数量:${zipStreamFileCount},文件大小:${Utils.formatFileSize(
                                zipStreamSize
                            )}\n请等待下载完成。`,
                            3000
                        );
                    },
                    async (error) => {
                        const newError = new Error(`流式下载中出现错误:${error.message || error}`);
                        newError.stack = error.stack || new Error().stack;
                        this.uiManager.showFloatTip(`流式下载中出现错误:${newError.message}`);
                        await this.uiManager.showConfirmDialog(
                            `出现错误:${newError.message}。是否忽略错误继续下载?`,
                            () => {
                                this.uiManager.showFloatTip("已忽略错误,继续下载。");
                            },
                            () => {
                                throw newError;
                            }
                        );
                    }
                );
            }

            // 下载每篇文章
            const totalArticleCount = url_list.length;
            if (config.endArticleIndex < 1) {
                this.uiManager.showFloatTip(`结束文章索引 ${config.endArticleIndex} 小于1,将不下载任何文章。`, 3000);
                return;
            } else if (config.startArticleIndex > totalArticleCount) {
                this.uiManager.showFloatTip(
                    `开始文章索引 ${config.startArticleIndex} 超过总文章数 ${totalArticleCount},将不下载任何文章。`,
                    3000
                );
                return;
            } else if (config.startArticleIndex > config.endArticleIndex) {
                this.uiManager.showFloatTip(
                    `开始文章索引 ${config.startArticleIndex} 大于结束文章索引 ${config.endArticleIndex},将不下载任何文章。`,
                    3000
                );
                return;
            } else {
                this.uiManager.showFloatTip(
                    `开始下载文章:从第 ${Math.max(1, config.startArticleIndex)} 篇到第 ${Math.min(
                        totalArticleCount,
                        config.endArticleIndex
                    )} 篇,共 ${url_list.length} 篇。(总文章数:${totalArticleCount})`
                );
            }

            const taskCount = Math.min(url_list.length, config.endArticleIndex) - Math.max(1, config.startArticleIndex) + 1;
            if (taskCount >= 100 && config.parallelDownload && !config.fastDownload) {
                let continueDownload = true;
                await this.uiManager.showDialog(
                    {
                        title: "ℹ️ 提示",
                        message: `检测到文章数量超过100篇(将要下载${taskCount}篇,总${url_list.length}篇),\n使用并行下载可能会导致CSDN风控或者内存溢出。\n建议改用串行(慢些)或者启用快速模式(避免内存溢出崩溃)。\n请注意,继续当前模式预计需要${Utils.formatFileSize(
                            taskCount * 30 * 1024 * 1024
                        )}内存。`,
                    },
                    {
                        text: "取消下载",
                        type: "default",
                        callback: () => {
                            this.uiManager.showFloatTip("已取消下载。", 3000, 3000);
                            continueDownload = false; // 取消下载
                        },
                    },
                    {
                        text: "取消并行,使用串行",
                        type: "primary",
                        callback: () => {
                            config.parallelDownload = false; // 串行下载
                            this.uiManager.showFloatTip("已切换为串行下载。");
                        },
                    },
                    {
                        text: "启用快速模式",
                        type: "primary",
                        callback: () => {
                            config.fastDownload = true; // 启用快速下载
                            this.uiManager.showFloatTip("已启用快速下载模式。");
                        },
                    },
                    {
                        text: "继续使用并行下载",
                        type: "danger",
                        callback: () => {
                            this.uiManager.showFloatTip("继续使用并行下载。");
                        },
                    }
                );
                if (!continueDownload) {
                    return; // 如果用户取消下载,则退出
                }
            }

            await Utils.parallelPool(
                url_list,
                async (url, index) => {
                    const articleIndex = totalArticleCount - index; // 反向
                    if (articleIndex >= config.startArticleIndex && articleIndex <= config.endArticleIndex) {
                        await this.downloadOneArticleFromBatch(url, articleIndex, totalArticleCount, config);
                    }
                },
                config.parallelDownload ? config.maxConcurrentDownloads : 1
            );

            if (config.mergeArticleContent) {
                let extraTopContent = "";
                if (config.addArticleTitleToMarkdown) {
                    extraTopContent += `# ${document.title}\n\n`;
                }
                if (config.addArticleInfoInBlockquote_batch) {
                    const batchTitle = document.body.querySelector(".column_title")?.textContent.trim() || "";
                    const batchDesc = document.body.querySelector(".column_text_desc")?.textContent.trim() || "";
                    const batchColumnData =
                        document.body
                            .querySelector(".column_data")
                            ?.textContent.replace(/\s{2,}/g, " ")
                            .trim() || "";
                    const batchAuthor =
                        document.body
                            .querySelector(".column_person_tit")
                            ?.textContent.replace(/\s{2,}/g, " ")
                            .trim() || "";
                    const batchUrl = Utils.clearUrl(base_url);
                    extraTopContent += `> ${batchDesc}\n> ${batchAuthor} ${batchColumnData}\n${batchUrl}\n\n`;
                }
                this.fileManager.mergeTextFile(`${document.title}`, extraTopContent);
            }

            if (config.enableStreaming) {
                this.uiManager.showFloatTip("正在等待流式压缩完成,请稍候...");
                // endStream参数为true,表示结束流式压缩,直接下载
                await this.fileManager.addAllFilesInQueueToZipStream(true);
            } else {
                if (config.zipCategories) {
                    await this.fileManager.zipAllFilesInQueue(
                        `${document.title}`,
                        (info_string) => {
                            this.uiManager.showFloatTip(info_string);
                        },
                        (info_string) => {
                            this.uiManager.showFloatTip(info_string, 3000);
                        },
                        // zip 库选用:fflate / jszip
                        config.zipLibrary || "fflate"
                    );
                }
                await this.fileManager.downloadAllFilesInQueue();
            }
            this.uiManager.showFloatTip("专栏文章全部处理完毕,请等待下载结束。", 3000);
        }

        /**
         * 下载用户的全部文章为Markdown格式
         * @param {Object} config - 配置选项
         * @param {boolean} config.zipCategories - 是否将文章打包成zip
         * @param {string} config.filePrefix - 文件名前缀
         * @param {string} config.articleUrl - 文章链接
         * @param {number} config.maxConcurrentDownloads - 最大并发下载数
         * @param {boolean} config.parallelDownload - 是否并行下载
         * @param {boolean} config.fastDownload - 是否快速下载
         * @param {boolean} config.mergeArticleContent - 是否合并文章内容
         * @param {boolean} config.saveAllImagesToAssets - 是否将所有图片保存
         * @param {boolean} config.addSerialNumber - 是否添加序号
         * @param {boolean} config.addSerialNumberToTitle - 是否在标题前添加序号
         * @param {boolean} config.addArticleInfoInBlockquote - 是否在引用块中添加文章信息
         * @param {boolean} config.addArticleTitleToMarkdown - 是否在Markdown中添加文章标题
         * @param {boolean} config.addArticleInfoInYaml - 是否在YAML中添加文章信息
         * @param {boolean} config.saveWebImages - 是否保存网络图片
         * @param {boolean} config.forceImageCentering - 是否强制图片居中
         * @param {boolean} config.enableImageSize - 是否启用图片尺寸
         * @param {boolean} config.enableColorText - 是否启用彩色文本
         * @param {boolean} config.removeCSDNSearchLink - 是否移除CSDN搜索链接
         * @param {boolean} config.addArticleInfoInBlockquote_batch - 是否在批量下载时在引用块中添加文章信息
         **/
        async downloadUserAllArticles(config = {}) {
            const mainContent = document.body.querySelector(".mainContent");
            const url_list = [];

            // 获取用户原始ID
            // <link rel="canonical" href="https://blog.csdn.net/yanglfree">
            const getUrlListFromAPI = async () => {
                let user_id = document.querySelector("link[rel='canonical']")?.href.match(/\/([^\/]+)$/)?.[1];
                if (!user_id) {
                    console.dir(`Warning: 无法从canonical链接中获取用户ID。`);
                    user_id = document.querySelector(".blog-second-rss-btn a")?.href.match(/\/([^\/]+)\/rss/)?.[1];
                    if (!user_id) {
                        console.dir(`Warning: 无法从RSS链接中获取用户ID。`);
                        throw new Error("无法获取用户ID,请检查页面是否正确。");
                    }
                }
                // 使用 API 获取文章列表
                // https://blog.csdn.net/community/home-api/v1/get-business-list?page=1&size=20&businessType=blog&orderby=&noMore=false&year=&month=&username=yanglfree
                const temp_url_list = [];
                let total_articles = 0;
                let page = 1;

                do {
                    console.dir(
                        `正在获取第 ${page} 页文章链接: https://blog.csdn.net/community/home-api/v1/get-business-list?page=${page}&size=100&businessType=blog&orderby=&noMore=false&year=&month=&username=${user_id}`
                    );
                    // const response = await (
                    //     await fetch(
                    //         `https://blog.csdn.net/community/home-api/v1/get-business-list?page=${page}&size=100&businessType=blog&orderby=&noMore=false&year=&month=&username=${user_id}`
                    //     )
                    // ).json();
                    const response = await this.fileManager.fetchResource(
                        `https://blog.csdn.net/community/home-api/v1/get-business-list?page=${page}&size=100&businessType=blog&orderby=&noMore=false&year=&month=&username=${user_id}`,
                        "json",
                        config.downloadAssetRetryCount || 3,
                        config.downloadAssetRetryDelay || 1000,
                        "fetch"
                    );
                    if (total_articles === 0) total_articles = response.data.total;
                    if (response.data.list.length === 0) break;
                    temp_url_list.push(...response.data.list.map((item) => item.url));
                    console.dir(
                        `获取到第 ${page} 页 ${response.data.list.length} 篇文章链接 (${temp_url_list.length} / ${total_articles}):`
                    );
                    this.uiManager.showFloatTip(
                        `获取到第 ${page} 页 ${response.data.list.length} 篇文章链接 (${temp_url_list.length} / ${total_articles}):`
                    );
                    page++;
                } while (temp_url_list.length < total_articles);

                return temp_url_list;
            };

            try {
                const res = await getUrlListFromAPI();
                if (res.length === 0) {
                    console.dir(`从API获取文章列表失败,尝试从页面获取文章链接。`);
                } else {
                    url_list.push(...res);
                    console.dir(`从API获取到 ${url_list.length} 篇文章链接。`);
                }
            } catch (error) {
                console.dir(`从API获取文章列表失败,尝试从页面获取文章链接。${error.message || error}`);
            }

            // 如果API获取失败,则从页面获取文章链接
            if (url_list.length === 0) {
                // 滚回顶部
                window.scrollTo({
                    top: 0,
                    behavior: "smooth",
                });
                this.uiManager.showFloatTip("正在获取用户全部文章链接。可能需要进行多次页面滚动,请耐心等待。");
                const url_set = new Set();
                while (true) {
                    // 等待2秒,等待页面加载完成
                    await new Promise((resolve) => setTimeout(resolve, 2000));
                    window.scrollTo({
                        top: document.body.scrollHeight,
                        behavior: "smooth",
                    });

                    let end = true;
                    mainContent.querySelectorAll("article").forEach((item) => {
                        const url = item.querySelector("a").href;
                        if (!url_set.has(url)) {
                            url_list.push(url);
                            url_set.add(url);
                            end = false;
                        }
                    });

                    if (end) break;
                }
                // 滚回顶部
                window.scrollTo({
                    top: 0,
                    behavior: "smooth",
                });
            }

            if (url_list.length === 0) {
                this.uiManager.showFloatTip("没有找到文章。");
            } else {
                this.uiManager.showFloatTip(`找到 ${url_list.length} 篇文章。开始解析...`);
            }

            // FIX: 解决自定义域名在Chrome里下载用户主页时,用户主页和文章hostname不一致导致跨域问题
            // https://github.com/Qalxry/csdn2md/issues/7
            // 用户主页的 url 为 https://blog.csdn.net/{user_id} + 可能存在的 ?type=xxx
            // 文章的 url 为 https://{custom_domain}.blog.csdn.net/article/details/{article_id}
            //
            // > 方案1:将文章的 url 替换为 https://blog.csdn.net/{user_id}/article/details/{article_id}
            // 会引发 CSDN 的安全策略问题,废弃
            // if (base_url.startsWith("https://blog.csdn.net/")) {
            //     const user_id = base_url.match(/blog\.csdn\.net\/([^\/]+)/)[1];
            //     for (let i = 0; i < url_list.length; i++) {
            //         if (!url_list[i].startsWith("https://blog.csdn.net/")) {
            //             const article_id = url_list[i].match(/\/article\/details\/([^\/]+)/)[1];
            //             url_list[i] = `https://blog.csdn.net/${user_id}/article/details/${article_id}`;
            //         }
            //     }
            // }
            // > 方案2:将用户主页的 url 替换为 https://{custom_domain}.blog.csdn.net + /?type=xxx
            // 虽然有效,但不确定是否稳定,目前来看可以
            const base_url = window.location.href;
            let isAllArticlesCustomDomain = true;
            let isAllArticlesDefaultDomain = true;
            for (let i = 0; i < url_list.length; i++) {
                if (url_list[i].startsWith("https://blog.csdn.net/")) {
                    isAllArticlesCustomDomain = false;
                    break;
                }
            }
            for (let i = 0; i < url_list.length; i++) {
                if (!url_list[i].startsWith("https://blog.csdn.net/")) {
                    isAllArticlesDefaultDomain = false;
                    break;
                }
            }
            if (isAllArticlesCustomDomain) {
                // 如果全部文章都是自定义域名,则将用户主页的 url 替换为 https://{custom_domain}.blog.csdn.net/category_{category_id}.html
                if (base_url.startsWith("https://blog.csdn.net/")) {
                    console.dir(
                        `Warning: 文章与用户主页的域名不一致,正在将用户主页的URL替换为自定义域名。当前用户主页URL: ${base_url} 文章URL: ${url_list[0]}`
                    );
                    const custom_domain = url_list[0].match(/https:\/\/([^\/]+)\.blog\.csdn\.net/)[1];
                    GM_setValue("status", {
                        timestamp: Date.now(),
                        action: "downloadUserAllArticles",
                        targetUrl: `https://${custom_domain}.blog.csdn.net/?type=blog`,
                    });
                    window.location.href = `https://${custom_domain}.blog.csdn.net/?type=blog`;
                }
            } else if (isAllArticlesDefaultDomain) {
                // 如果全部文章都是默认域名,则将用户主页的 url 替换为 https://blog.csdn.net/category_{category_id}.html
                if (!base_url.startsWith("https://blog.csdn.net/")) {
                    console.dir(
                        `Warning: 文章与用户主页的域名不一致,正在将用户主页的URL替换为默认域名。当前用户主页URL: ${base_url} 文章URL: ${url_list[0]}`
                    );
                    const user_id = url_list[0].match(/blog\.csdn\.net\/([^\/]+)/)[1];
                    GM_setValue("status", {
                        timestamp: Date.now(),
                        action: "downloadUserAllArticles",
                        targetUrl: `https://blog.csdn.net/${user_id}?type=blog`,
                    });
                    window.location.href = `https://blog.csdn.net/${user_id}?type=blog`;
                }
            } else {
                // 如果文章的域名不一致,则回退为方案1,至少可能可以下载
                console.dir(
                    `Warning: 文章与用户主页的域名不一致,可能无法下载。请检查是否有自定义域名。当前用户主页URL: ${base_url}`
                );
                if (base_url.startsWith("https://blog.csdn.net/")) {
                    const user_id = base_url.match(/blog\.csdn\.net\/([^\/]+)/)[1];
                    for (let i = 0; i < url_list.length; i++) {
                        if (!url_list[i].startsWith("https://blog.csdn.net/")) {
                            const article_id = url_list[i].match(/\/article\/details\/([^\/]+)/)[1];
                            url_list[i] = `https://blog.csdn.net/${user_id}/article/details/${article_id}`;
                        }
                    }
                }
            }

            if (config.enableStreaming) {
                this.uiManager.showFloatTip("正在初始化流式下载...");
                await this.fileManager.initializeZipStream(
                    `${document.title}`,
                    (fileName, fileIndex) => {},
                    (zipStreamName, zipStreamFileCount, zipStreamSize) => {
                        this.uiManager.showFloatTip(
                            `打包完成:${zipStreamName}。文件数量:${zipStreamFileCount},文件大小:${Utils.formatFileSize(
                                zipStreamSize
                            )}\n请等待下载完成。`,
                            3000
                        );
                    },
                    async (error) => {
                        const newError = new Error(`流式下载中出现错误:${error.message || error}`);
                        newError.stack = error.stack || new Error().stack;
                        this.uiManager.showFloatTip(`流式下载中出现错误:${newError.message}`);
                        await this.uiManager.showConfirmDialog(
                            `出现错误:${newError.message}。是否忽略错误继续下载?`,
                            () => {
                                this.uiManager.showFloatTip("已忽略错误,继续下载。");
                            },
                            () => {
                                throw newError;
                            }
                        );
                    }
                );
            }

            // 下载每篇文章
            const totalArticleCount = url_list.length;
            if (config.endArticleIndex < 1) {
                this.uiManager.showFloatTip(`结束文章索引 ${config.endArticleIndex} 小于1,将不下载任何文章。`, 3000);
                return;
            } else if (config.startArticleIndex > totalArticleCount) {
                this.uiManager.showFloatTip(
                    `开始文章索引 ${config.startArticleIndex} 超过总文章数 ${totalArticleCount},将不下载任何文章。`,
                    3000
                );
                return;
            } else if (config.startArticleIndex > config.endArticleIndex) {
                this.uiManager.showFloatTip(
                    `开始文章索引 ${config.startArticleIndex} 大于结束文章索引 ${config.endArticleIndex},将不下载任何文章。`,
                    3000
                );
                return;
            } else {
                this.uiManager.showFloatTip(
                    `开始下载文章:从第 ${Math.max(1, config.startArticleIndex)} 篇到第 ${Math.min(
                        totalArticleCount,
                        config.endArticleIndex
                    )} 篇,共 ${url_list.length} 篇。(总文章数:${totalArticleCount})`
                );
            }

            const taskCount = Math.min(url_list.length, config.endArticleIndex) - Math.max(1, config.startArticleIndex) + 1;
            if (taskCount >= 100 && config.parallelDownload && !config.fastDownload) {
                let continueDownload = true;
                await this.uiManager.showDialog(
                    {
                        title: "ℹ️ 提示",
                        message: `检测到文章数量超过100篇(将要下载${taskCount}篇,总${url_list.length}篇),\n使用并行下载可能会导致CSDN风控或者内存溢出。\n建议改用串行(慢些)或者启用快速模式(避免内存溢出崩溃)。\n请注意,继续当前模式预计需要${Utils.formatFileSize(
                            taskCount * 30 * 1024 * 1024
                        )}内存。`,
                    },
                    {
                        text: "取消下载",
                        type: "default",
                        callback: () => {
                            this.uiManager.showFloatTip("已取消下载。", 3000);
                            continueDownload = false; // 取消下载
                        },
                    },
                    {
                        text: "取消并行,使用串行",
                        type: "primary",
                        callback: () => {
                            config.parallelDownload = false; // 串行下载
                            this.uiManager.showFloatTip("已切换为串行下载。");
                        },
                    },
                    {
                        text: "启用快速模式",
                        type: "primary",
                        callback: () => {
                            config.fastDownload = true; // 启用快速下载
                            this.uiManager.showFloatTip("已启用快速下载模式。");
                        },
                    },
                    {
                        text: "继续使用并行下载",
                        type: "danger",
                        callback: () => {
                            this.uiManager.showFloatTip("继续使用并行下载。");
                        },
                    }
                );
                if (!continueDownload) {
                    return; // 如果用户取消下载,则退出
                }
            }

            await Utils.parallelPool(
                url_list,
                async (url, index) => {
                    const articleIndex = totalArticleCount - index; // 反向
                    if (articleIndex >= config.startArticleIndex && articleIndex <= config.endArticleIndex) {
                        await this.downloadOneArticleFromBatch(url, articleIndex, totalArticleCount, config);
                    }
                },
                config.parallelDownload ? config.maxConcurrentDownloads : 1
            );

            if (config.mergeArticleContent) {
                let extraTopContent = "";
                if (config.addArticleTitleToMarkdown) {
                    extraTopContent += `# ${document.title}\n\n`;
                }
                if (config.addArticleInfoInBlockquote_batch) {
                    extraTopContent += `> ${Utils.clearUrl(window.location.href)}\n\n`;
                }
                // 下载每篇文章
                this.fileManager.mergeTextFile(`${document.title}`, extraTopContent);
            }
            if (config.enableStreaming) {
                this.uiManager.showFloatTip("正在等待流式压缩完成,请稍候...");
                // endStream参数为true,表示结束流式压缩,直接下载
                await this.fileManager.addAllFilesInQueueToZipStream(true);
            } else {
                if (config.zipCategories) {
                    await this.fileManager.zipAllFilesInQueue(
                        `${document.title}`,
                        (info_string) => {
                            this.uiManager.showFloatTip(info_string);
                        },
                        (info_string) => {
                            this.uiManager.showFloatTip(info_string, 3000);
                        },
                        // zip 库选用:fflate / jszip
                        config.zipLibrary || "fflate"
                    );
                }
                await this.fileManager.downloadAllFilesInQueue();
            }
            this.uiManager.showFloatTip("用户全部文章处理完毕,请等待下载结束。", 3000);
        }

        /**
         * 下载单篇文章
         * @param {Object} config - 配置选项
         * @param {boolean} config.zipCategories - 是否将文章打包成zip
         * @param {string} config.enableStreaming - 是否启用流式下载
         * @param {string} config.filePrefix - 文件名前缀
         * @param {string} config.articleUrl - 文章链接
         * @param {boolean} config.parallelDownload - 是否并行下载
         * @param {boolean} config.fastDownload - 是否快速下载
         * @param {boolean} config.mergeArticleContent - 是否合并文章内容
         * @param {boolean} config.saveAllImagesToAssets - 是否将所有图片保存
         * @param {boolean} config.addSerialNumber - 是否添加序号
         * @param {boolean} config.addSerialNumberToTitle - 是否在标题前添加序号
         * @param {boolean} config.addArticleInfoInBlockquote - 是否在引用块中添加文章信息
         * @param {boolean} config.addArticleTitleToMarkdown - 是否在Markdown中添加文章标题
         * @param {boolean} config.addArticleInfoInYaml - 是否在YAML中添加文章信息
         * @param {boolean} config.saveWebImages - 是否保存网络图片
         * @param {number} config.downloadAssetRetryCount - 下载网络图片的重试次数
         * @param {boolean} config.forceImageCentering - 是否强制图片居中
         * @param {boolean} config.enableImageSize - 是否启用图片尺寸
         * @param {boolean} config.enableColorText - 是否启用彩色文本
         * @param {boolean} config.removeCSDNSearchLink - 是否移除CSDN搜索链接
         **/
        async downloadSingleArticle(config = {}) {
            const articleTitle = await this.parseArticle(document.body, {
                articleUrl: window.location.href,
                ...config,
                mergeArticleContent: false,
                enableStreaming: false, // 单篇文章下载不支持流式下载
            });
            if (config.zipCategories) {
                await this.fileManager.zipAllFilesInQueue(
                    `${articleTitle}`,
                    (info_string) => {
                        this.uiManager.showFloatTip(info_string);
                    },
                    (info_string) => {
                        this.uiManager.showFloatTip(info_string, 3000);
                    },
                    // zip 库选用:fflate / jszip
                    config.zipLibrary || "fflate"
                );
            }
            await this.fileManager.downloadAllFilesInQueue();
            this.uiManager.showFloatTip("文章下载完毕!", 4000);
        }
    }

    /**
     * 判断当前页面类型
     * @returns {"category"|"article"|"user_all_articles"|"unknown"}
     */
    function getCurrentPageType() {
        const url = window.location.href;
        if (url.includes("category")) {
            return "category";
        } else if (url.includes("article/details")) {
            return "article";
        } else if (
            url.includes("type=blog") ||
            url.includes("type=lately") ||
            url.match(/^https:\/\/[^.]+\.blog\.csdn\.net\/$/) ||
            url.match(/^https:\/\/blog\.csdn\.net\/[^\/]+\/?$/)
        ) {
            return "user_all_articles";
        } else {
            return "unknown";
        }
    }

    // 初始化应用
    function initApp() {
        // 确保在目标页面
        if (getCurrentPageType() === "unknown") {
            console.dir({
                message: "当前页面不是CSDN文章页面、专栏文章列表页面或用户全部文章列表页面,脚本不会执行。",
                url: window.location.href,
            });
            return;
        }

        // 初始化App
        const uiManager = new UIManager();

        // 检查是否有下载任务
        const status = GM_getValue("status");
        if (
            status &&
            status.timestamp &&
            status.action &&
            status.targetUrl &&
            Date.now() - status.timestamp < 5 * 60 * 1000 // 检查下载任务是否在5分钟内
        ) {
            GM_setValue("status", null); // 清除下载任务状态
            if (
                status.action === "downloadCategory" &&
                Utils.clearUrl(status.targetUrl) === Utils.clearUrl(window.location.href)
            ) {
                // 如果有下载任务,直接跳转到下载页面
                console.dir(`检测到下载任务,开始自动下载专栏文章: ${status.targetUrl}`);
                uiManager.showFloatTip(`检测到下载任务,开始自动下载专栏文章: ${status.targetUrl}`);
                uiManager.downloadButton.click();
            } else if (
                status.action === "downloadUserAllArticles" &&
                Utils.clearUrl(status.targetUrl) === Utils.clearUrl(window.location.href)
            ) {
                // 如果有下载任务,直接跳转到下载页面
                console.dir(`检测到下载任务,开始自动下载用户全部文章: ${status.targetUrl}`);
                uiManager.showFloatTip(`检测到下载任务,开始自动下载用户全部文章: ${status.targetUrl}`);
                uiManager.downloadButton.click();
            } else {
                console.dir(
                    `检测到下载任务,但当前页面与任务目标页面不一致,跳过自动下载。当前页面:${window.location.href} 任务目标页面:${status.targetUrl}`
                );
                uiManager.showFloatTip(
                    `检测到下载任务,但当前页面与任务目标页面不一致,跳过自动下载。当前页面:${window.location.href} 任务目标页面:${status.targetUrl}`,
                    5000
                );
            }
        }
    }

    // 启动应用
    initApp();
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。