XHS-Downloader

提取小红书作品/用户链接,下载小红书无水印图文/视频作品文件

当前为 2025-07-28 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         XHS-Downloader
// @namespace    https://github.com/JoeanAmier/XHS-Downloader
// @version      2.0.4
// @description  提取小红书作品/用户链接,下载小红书无水印图文/视频作品文件
// @author       JoeanAmier
// @match        http*://xhslink.com/*
// @match        http*://www.xiaohongshu.com/explore*
// @match        http*://www.xiaohongshu.com/user/profile/*
// @match        http*://www.xiaohongshu.com/search_result*
// @match        http*://www.xiaohongshu.com/board/*
// @icon         data:image/png;base64,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
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        unsafeWindow
// @grant        GM_setClipboard
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @license      GNU General Public License v3.0
// @run-at       document-end
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.9.1/jszip.min.js
// ==/UserScript==

(function () {
    'use strict';

    const iconBase64 = "data:image/png;base64,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";

    let config = {
        disclaimer: GM_getValue("disclaimer", false),
        packageDownloadFiles: GM_getValue("packageDownloadFiles", true),
        autoScrollSwitch: GM_getValue("autoScrollSwitch", false),
        maxScrollCount: GM_getValue("maxScrollCount", 50),
        fileNameFormat: undefined,
        imageFileFormat: undefined,
        icon: {
            type: 'image', // 可选: image/svg/font
            image: {
                url: iconBase64, // 图片URL或Base64
                size: 64, // 图标尺寸
                borderRadius: '50%' // 形状(50%为圆形)
            },
        }, // 位置配置
        position: {
            bottom: '8rem', left: '2rem'
        }, // 动画配置
        animation: {
            duration: 0.35, // 动画时长(s)
            easing: 'cubic-bezier(0.4, 0, 0.2, 1)'
        }
    };

    const readme = () => {
        const instructions = `
XHS-Downloader 用户脚本 功能清单:
1. 下载小红书无水印作品文件
2. 提取推荐页面作品链接
3. 提取账号发布作品链接
4. 提取账号收藏作品链接
5. 提取账号专辑作品链接
6. 提取账号点赞作品链接
7. 提取搜索结果作品链接
8. 提取搜索结果用户链接

XHS-Downloader 用户脚本 详细说明:
1. 下载小红书无水印作品文件时,脚本需要花费时间处理文件,请等待片刻,请勿多次点击下载按钮
2. 无水印作品文件较大,可能需要较长的时间处理,页面跳转可能会导致下载失败
3. 提取账号发布、收藏、点赞、专辑作品链接时,脚本可以自动滚动页面直至加载全部作品
4. 提取推荐作品链接、搜索作品、用户链接时,脚本可以自动滚动指定次数加载更多内容,默认滚动次数:50 次
5. 自动滚动页面功能默认关闭;用户可以自由开启,并修改滚动页面次数,修改后立即生效
6. 如果未开启自动滚动页面功能,用户需要手动滚动页面以便加载更多内容后再进行其他操作
7. 支持作品文件打包下载;该功能默认开启,多个文件的作品将会以压缩包格式下载

项目开源地址:https://github.com/JoeanAmier/XHS-Downloader
`
        const disclaimer_content = `
关于 XHS-Downloader 的 免责声明:

1. 使用者对本项目的使用由使用者自行决定,并自行承担风险。作者对使用者使用本项目所产生的任何损失、责任、或风险概不负责。
2. 本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者尽力确保代码的正确性和安全性,但不保证代码完全没有错误或缺陷。
3. 使用者在使用本项目时必须严格遵守 GNU General Public License v3.0 的要求,并在适当的地方注明使用了 GNU General Public License v3.0 的代码。
4. 使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行为联系起来,或要求其对使用者使用本项目所产生的任何损失或损害负责。
5. 使用者在使用本项目的代码和功能时,必须自行研究相关法律法规,并确保其使用行为合法合规。任何因违反法律法规而导致的法律责任和风险,均由使用者自行承担。
6. 本项目的作者不会提供 XHS-Downloader 项目的付费版本,也不会提供与 XHS-Downloader 项目相关的任何商业服务。
7. 基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关,原创作者不承担与二次开发行为或其结果相关的任何责任,使用者应自行对因二次开发可能带来的各种情况负全部责任。

在使用本项目的代码和功能之前,请您认真考虑并接受以上免责声明。如果您对上述声明有任何疑问或不同意,请不要使用本项目的代码和功能。如果您使用了本项目的代码和功能,则视为您已完全理解并接受上述免责声明,并自愿承担使用本项目的一切风险和后果。

是否已阅读 XHS-Downloader 功能说明与免责声明(YES/NO)
`
        alert(instructions);
        if (!config.disclaimer) {
            const answer = prompt(disclaimer_content, "");
            if (!answer) {
                GM_setValue("disclaimer", false);
                config.disclaimer = false;
            } else {
                GM_setValue("disclaimer", answer.toUpperCase() === "YES" || answer.toUpperCase() === "Y");
                config.disclaimer = GM_getValue("disclaimer");
            }
        }
    };

    if (!config.disclaimer) {
        readme();
    }

    console.info("用户接受 XHS-Downloader 免责声明", config.disclaimer)

    GM_registerMenuCommand("阅读脚本说明和免责声明", function () {
        readme();
    });

    const updatePackageDownloadFiles = (value) => {
        config.packageDownloadFiles = value;
        GM_setValue("packageDownloadFiles", config.packageDownloadFiles);
    };

    const updateAutoScrollSwitch = (value) => {
        config.autoScrollSwitch = value;
        GM_setValue("autoScrollSwitch", config.autoScrollSwitch);
    };

    const updateMaxScrollCount = (value) => {
        config.maxScrollCount = parseInt(value) || 50;
        GM_setValue("maxScrollCount", config.maxScrollCount);
    };

    const updateFileNameFormat = (value) => {
        config.fileNameFormat = value;
        GM_setValue("fileNameFormat", config.fileNameFormat);
    };

    const about = () => {
        window.open('https://github.com/JoeanAmier/XHS-Downloader', '_blank');
    }

    const abnormal = (text) => {
        alert(`${text}请向作者反馈!\n项目开源地址:https://github.com/JoeanAmier/XHS-Downloader`);
    };

    const generateVideoUrl = note => {
        try {
            return [`https://sns-video-bd.xhscdn.com/${note.video.consumer.originVideoKey}`];
        } catch (error) {
            console.error("Error generating video URL:", error);
            return [];
        }
    };

    const generateImageUrl = note => {
        let images = note.imageList;
        const regex = /http:\/\/sns-webpic-qc\.xhscdn.com\/\d+\/[0-9a-z]+\/(\S+)!/;
        let urls = [];
        try {
            images.forEach((item) => {
                let match = item.urlDefault.match(regex);
                if (match && match[1]) {
                    urls.push(`https://ci.xiaohongshu.com/${match[1]}?imageView2/format/png`);
                }
            })
            return urls
        } catch (error) {
            console.error("Error generating image URLs:", error);
            return [];
        }
    };

    const extractImageWebpUrls = (note, urls,) => {
        try {
            let items = []
            let {imageList} = note;
            if (urls.length !== imageList.length) {
                console.error("图片数量不一致!")
                return []
            }
            for (const [index, item] of imageList.entries()) {
                if (item.urlDefault) {
                    items.push({
                        webp: item.urlDefault, index: index + 1, url: urls[index],
                    })
                } else {
                    console.error("提取图片预览链接失败", item)
                    break
                }
            }
            return items;
        } catch (error) {
            console.error("Error occurred in generating image object:", error);
            return []
        }
    };

    const download = async (urls, note) => {
        const name = extractName();
        console.info(`文件名称 ${name}`);
        if (note.type === "video") {
            await downloadVideo(urls[0], name);
        } else {
            let items = extractImageWebpUrls(note, urls);
            if (items.length === 0) {
                console.error("解析图文作品数据失败", note)
                abnormal("解析图文作品数据发生异常!")
            } else if (urls.length > 1) {
                showImageSelectionModal(items, name,)
            } else {
                await downloadImage(items, name);
            }
        }
    };

    const exploreDeal = async note => {
        try {
            let links;
            if (note.type === "normal") {
                links = generateImageUrl(note);
            } else {
                links = generateVideoUrl(note);
            }
            if (links.length > 0) {
                console.info("下载链接", links);
                await download(links, note);
            } else {
                abnormal("处理下载链接发生异常!")
            }
        } catch (error) {
            console.error("Error in exploreDeal function:", error);
            abnormal("下载作品文件发生异常!");
        }
    };

    const extractNoteInfo = () => {
        const regex = /\/explore\/([^?]+)/;
        const match = currentUrl.match(regex);
        if (match) {
            return unsafeWindow.__INITIAL_STATE__.note.noteDetailMap[match[1]]
        } else {
            console.error("从链接提取作品 ID 失败", currentUrl,);
        }
    };

    const extractDownloadLinks = async () => {
        if (currentUrl.includes("https://www.xiaohongshu.com/explore/")) {
            let note = extractNoteInfo();
            if (note.note) {
                await exploreDeal(note.note);
            } else {
                abnormal("读取作品数据发生异常!");
            }
        }
    };

    const triggerDownload = (name, blob) => {
        // 创建 Blob 对象的 URL
        const blobUrl = URL.createObjectURL(blob);

        // 创建一个临时链接元素
        const tempLink = document.createElement("a");
        tempLink.href = blobUrl;
        tempLink.download = name;

        // 将链接添加到 DOM 并模拟点击
        document.body.appendChild(tempLink); // 避免某些浏览器安全限制
        tempLink.click();

        // 清理临时链接元素
        document.body.removeChild(tempLink); // 从 DOM 中移除临时链接
        URL.revokeObjectURL(blobUrl); // 释放 URL

        console.info(`文件已成功下载: ${name}`);
    }

    const downloadFile = async (link, name, trigger = true, retries = 5) => {
        for (let attempt = 1; attempt <= retries; attempt++) {
            try {
                // 使用 fetch 获取文件数据
                const response = await fetch(link, {
                    "headers": {
                        "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
                        "accept-language": "zh-SG,zh;q=0.9",
                    },
                    "method": "GET",
                });

                // 检查响应状态码
                if (!response.ok) {
                    console.error(`下载失败,状态码: ${response.status},URL: ${link},尝试次数: ${attempt}`);
                    continue; // 继续下一次尝试
                }

                const blob = await response.blob();

                if (trigger) {
                    triggerDownload(name, blob);
                    return true;
                } else {
                    return blob;
                }
            } catch (error) {
                console.error(`下载失败 (${name}),错误信息:`, error, `尝试次数: ${attempt}`);
                if (attempt === retries) {
                    return false; // 如果达到最大重试次数,返回失败
                }
            }
        }
        return false; // 如果所有尝试都失败,返回失败
    };

    const downloadFiles = async (items, name,) => {
        const downloadResults = []; // 用于存储下载结果

        const downloadPromises = items.map(async (item) => {
            let fileName;
            if (item.index) {
                fileName = `${name}_${item.index}.png`; // 根据索引生成文件名
            } else {
                fileName = `${name}.png`;
            }
            const result = await downloadFile(item.url, fileName, false); // 调用单个文件下载方法
            if (result) {
                downloadResults.push({name: fileName, file: result});
                return true; // 成功
            } else {
                return false; // 失败
            }
        });

        // 等待所有下载操作完成
        const results = await Promise.all(downloadPromises);

        if (results.every(result => result === true)) {
            try {
                const zip = new JSZip();
                downloadResults.forEach((item) => {
                    zip.file(item.name, item.file);
                });

                const content = await zip.generateAsync({type: "blob", compression: "STORE"});
                triggerDownload(`${name}.zip`, content,)
                return true;
            } catch (error) {
                console.error('生成 ZIP 文件或保存失败,错误信息:', error);
                return false;
            }
        } else {
            return false;
        }
    };

    const truncateString = (str, maxLength) => {
        if (str.length > maxLength) {
            const halfLength = Math.floor(maxLength / 2) - 1; // 减去 1 留出省略号的空间
            return str.slice(0, halfLength) + '...' + str.slice(-halfLength);
        }
        return str;
    };

    const extractName = () => {
        let name = document.title.replace(/ - 小红书$/, "").replace(/[^\u4e00-\u9fa5a-zA-Z0-9 ~!@#$%&()_\-+=\[\];"',.!()【】:“”,。《》?]/g, "");
        name = truncateString(name, 64,);
        let match = currentUrl.match(/\/([^\/]+)$/);
        let id = match ? match[1] : null;
        return name === "" ? id : name
    };

    const downloadVideo = async (url, name) => {
        if (!await downloadFile(url, `${name}.mp4`)) {
            abnormal("下载视频作品文件发生异常!");
        }
    };

    const downloadImage = async (items, name) => {
        let success;
        if (!config.packageDownloadFiles && items.length > 1) {
            let result = [];
            for (let item of items) {
                result.push(await downloadFile(item.url, `${name}_${item.index}.png`));
            }
            success = result.every(item => item === true);
        } else if (items.length === 1) {
            success = await downloadFile(items[0].url, `${name}.png`);
        } else {
            success = await downloadFiles(items, name,);
        }
        if (!success) {
            abnormal("下载图文作品文件发生异常!");
        }
    };

    const window_scrollBy = (x, y,) => {
        window.scrollBy(x, y,);
    }

    // 随机整数生成函数
    const getRandomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;

    // 判断是否需要暂停,模拟用户的停顿行为
    const shouldPause = () => Math.random() < 0.2;  // 20%几率停顿

    // 执行一次增量滚动
    const scrollOnce = () => {
        const scrollDistanceMin = 100;  // 最小滚动距离
        const scrollDistanceMax = 300; // 最大滚动距离
        const scrollDistance = getRandomInt(scrollDistanceMin, scrollDistanceMax);
        window_scrollBy(0, scrollDistance);  // 增量滚动
    };

    // 检查是否已经滚动到底部
    const isAtBottom = () => {
        const docHeight = document.documentElement.scrollHeight;
        const winHeight = window.innerHeight;
        const scrollPos = window.scrollY;

        return (docHeight - winHeight - scrollPos <= 10);  // 如果距离底部小于10px,认为滚动到底部
    };

    // 自动滚动主函数
    const scrollScreen = (callback, endless = false, scrollCount = 0,) => {
        const timeoutMin = 250;  // 最小滚动间隔
        const timeoutMax = 500;  // 最大滚动间隔

        const scrollInterval = setInterval(() => {
            if (shouldPause()) {
                // 停顿,模拟用户的休息
                clearInterval(scrollInterval);
                setTimeout(() => {
                    scrollScreen(callback, endless, scrollCount,);  // 重新启动滚动
                }, getRandomInt(timeoutMin, timeoutMax,));  // 随机停顿时间
            } else if (endless) {
                // 无限滚动至底部模式
                if (!isAtBottom()) {
                    scrollOnce();  // 执行一次滚动
                } else {
                    // 到达底部,停止滚动
                    clearInterval(scrollInterval);
                    callback();  // 调用回调函数
                }
            } else if (scrollCount < config.maxScrollCount && !isAtBottom()) {
                scrollOnce();  // 执行一次滚动
                scrollCount++;
            } else {
                // 如果到达底部或滚动次数已满,停止滚动
                clearInterval(scrollInterval);
                callback();  // 调用回调函数
            }
        }, getRandomInt(timeoutMin, timeoutMax));  // 随机滚动间隔
    };

    const scrollScreenEvent = (callback, endless = false) => {
        if (config.autoScrollSwitch) {
            scrollScreen(callback, endless,);
        } else {
            callback();
        }
    };

    const extractNotesInfo = order => {
        const notesRawValue = unsafeWindow.__INITIAL_STATE__.user.notes._rawValue[order];
        return notesRawValue.map(item => [item.id, item.xsecToken,]);
    };

    const extractBoardInfo = () => {
        // 定义正则表达式来匹配 URL 中的 ID
        const regex = /\/board\/([a-z0-9]+)\?/;

        // 使用 exec 方法执行正则表达式
        const match = regex.exec(currentUrl);

        // 检查是否有匹配
        if (match) {
            // 提取 ID
            const id = match[1]; // match[0] 是整个匹配的字符串,match[1] 是第一个括号内的匹配

            const notesRawValue = unsafeWindow.__INITIAL_STATE__.board.boardFeedsMap._rawValue[id].notes;
            return notesRawValue.map(item => [item.noteId, item.xsecToken,]);
        } else {
            console.error("从链接提取专辑 ID 失败", currentUrl,);
            return [];
        }
    };

    const extractFeedInfo = () => {
        const notesRawValue = unsafeWindow.__INITIAL_STATE__.feed.feeds._rawValue;
        return notesRawValue.map(item => [item.id, item.xsecToken,]);
    };

    const extractSearchNotes = () => {
        const notesRawValue = unsafeWindow.__INITIAL_STATE__.search.feeds._rawValue;
        return notesRawValue.map(item => [item.id, item.xsecToken,]);
    }

    const extractSearchUsers = () => {
        const notesRawValue = unsafeWindow.__INITIAL_STATE__.search.userLists._rawValue;
        return notesRawValue.map(item => item.id);
    }

    const generateNoteUrls = data => data.map(([id, token,]) => `https://www.xiaohongshu.com/discovery/item/${id}?source=webshare&xhsshare=pc_web&xsec_token=${token}&xsec_source=pc_share`).join(" ");

    const generateUserUrls = data => data.map(id => `https://www.xiaohongshu.com/user/profile/${id}`).join(" ");

    const extractAllLinks = (callback, order) => {
        scrollScreenEvent(() => {
            let data;
            if (order >= 0 && order <= 2) {
                data = extractNotesInfo(order);
            } else if (order === 3) {
                data = extractSearchNotes();
            } else if (order === 4) {
                data = extractSearchUsers();
            } else if (order === -1) {
                data = extractFeedInfo()
            } else if (order === 5) {
                data = extractBoardInfo()
            } else {
                data = [];
            }
            let urlsString = order !== 4 ? generateNoteUrls(data) : generateUserUrls(data);
            callback(urlsString);
        }, [0, 1, 2, 5].includes(order))
    };

    const extractAllLinksEvent = (order = 0) => {
        extractAllLinks(urlsString => {
            if (urlsString) {
                GM_setClipboard(urlsString, "text", () => {
                    alert('作品/用户链接已复制到剪贴板!');
                });
            } else {
                alert("未提取到任何作品/用户链接!")
            }
        }, order);
    };

    if (typeof JSZip === 'undefined') {
        alert("XHS-Downloader 用户脚本依赖库 JSZip 加载失败,作品文件打包下载功能无法使用,请尝试刷新网页或者向作者反馈!");
    }

    /* ==================== 样式定义 ==================== */
    let style = document.createElement('style');
    style.textContent = `
        /* 弹窗基础样式 */
        #SettingsOverlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.32);
            backdrop-filter: blur(4px);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 10000;
            animation: fadeIn 0.3s;
        }

        .optimized-scroll-modal {
            background: white;
            border-radius: 16px;
            width: 380px; /* 缩小窗口宽度 */
            max-width: 95vw;
            box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
            overflow: hidden;
            animation: scaleUp 0.3s;
        }

        /* 头部样式 */
        .modal-header {
            padding: 1rem;
            border-bottom: 1px solid #eee;
            text-align: center;
        }

        .modal-header span {
            font-size: 1.25rem;
            font-weight: 500;
            color: #212121;
        }

        /* 内容区域 */
        .modal-body {
            padding: 1rem; /* 减小内边距 */
        }

        /* 设置项样式 */
        .setting-item {
            margin: 0.5rem 0; /* 减少设置项间距 */
            padding: 10px;
            border-radius: 8px;
            transition: background 0.2s;
        }

        .setting-item:hover {
            background: #f0f0f0;
        }

        .setting-item label {
            display: flex;
            justify-content: space-between;
            align-items: center;
            width: 100%;
        }

        /* 设置项标题 */
        .setting-item label span {
            font-size: 1rem; /* 增大标题文字 */
            font-weight: 500;
            color: #333;
        }

        /* 开关样式 */
        .toggle-switch {
            position: relative;
            width: 40px;
            height: 20px;
        }

        .toggle-switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }

        .slider {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: #ccc;
            transition: 0.4s;
            border-radius: 34px;
        }

        .slider:before {
            content: "";
            position: absolute;
            height: 16px;
            width: 16px;
            left: 2px;
            bottom: 2px;
            background: white;
            border-radius: 50%;
            transition: 0.4s;
        }

        input:checked + .slider {
            background: #2196F3;
        }

        input:checked + .slider:before {
            transform: translateX(20px);
        }

        /* 数值输入 */
        .number-input {
            display: flex;
            align-items: center;
            border: 1px solid #ddd;
            border-radius: 8px;
            overflow: hidden;
            margin: 6px 0;
        }

        .number-input input {
            width: 60px;
            text-align: center;
            border: none;
        }

        .number-button {
            padding: 4px 8px;
            background: #f0f0f0;
            border: none;
            cursor: pointer;
            transition: all 0.2s;
        }

        /* 文本输入框 */
        .text-input {
            width: 100%;
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 0.9rem;
            margin-top: 8px; /* 增加与标题的距离 */
            transition: border-color 0.2s;
        }

        .text-input:focus {
            outline: none;
            border-color: #2196F3;
            box-shadow: 0 0 4px rgba(33, 150, 243, 0.3);
        }

        /* 设置项说明 */
        .setting-description {
            font-size: 0.875rem;
            color: #757575;
            margin-top: 4px;
            line-height: 1.4;
            text-align: left; /* 左对齐 */
        }

        /* 底部按钮 */
        .modal-footer {
            padding: 1rem;
            border-top: 1px solid #eee;
            display: flex;
            justify-content: flex-end;
            gap: 12px;
        }

        .primary-btn {
            background: #2196F3;
            color: white;
            padding: 8px 24px;
            border-radius: 24px;
            cursor: pointer;
            transition: all 0.2s;
        }

        .secondary-btn {
            background: #f0f0f0;
            color: #666;
            padding: 8px 24px;
            border-radius: 24px;
            cursor: pointer;
            transition: all 0.2s;
        }

        /* 动画 */
        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }

        @keyframes scaleUp {
            from { transform: scale(0.98); }
            to { transform: scale(1); }
        }
    `;
    document.head.appendChild(style);

    // 创建开关项
    const createSettingItem = ({label, description, checked}) => {
        const item = document.createElement('div');
        item.className = 'setting-item';

        item.innerHTML = `
            <label>
                <span>${label}</span>
                <div class="toggle-switch">
                    <input type="checkbox" ${checked ? 'checked' : ''}>
                    <span class="slider"></span>
                </div>
            </label>
            <div class="setting-description">${description}</div>
        `;

        return item;
    };

    // 创建数值输入项
    const createNumberInput = ({label, description, value, min, max, disabled}) => {
        const item = document.createElement('div');
        item.className = 'setting-item';

        const numberInput = document.createElement('div');
        numberInput.className = 'number-input';
        numberInput.style.opacity = disabled ? 0.6 : 1;
        numberInput.innerHTML = `
            <button class="number-button" data-action="decrement">−</button>
            <input type="number" value="${value}" min="${min}" max="${max}" ${disabled ? 'disabled' : ''}>
            <button class="number-button" data-action="increment">+</button>
        `;

        item.innerHTML = `
            <label>
                <span>${label}</span>
                ${numberInput.outerHTML}
            </label>
            <div class="setting-description">${description}</div>
        `;

        // 绑定数值按钮事件
        const container = item.querySelector('.number-input');
        container.querySelectorAll('.number-button').forEach(btn => {
            btn.addEventListener('click', () => {
                const input = container.querySelector('input');
                if (input.disabled) {
                    return;
                }

                let val = parseInt(input.value);
                if (btn.dataset.action === 'increment') {
                    val = Math.min(val + 1, max);
                } else {
                    val = Math.max(val - 1, min);
                }
                input.value = val;
            });
        });

        return item;
    };

    // 创建文本输入项
    const createTextInput = ({label, description, placeholder, value}) => {
        const item = document.createElement('div');
        item.className = 'setting-item';

        item.innerHTML = `
            <div>
                <span style="font-size: 1rem; font-weight: 500; color: #333;">${label}</span>
            </div>
            <div class="setting-description">${description}</div>
            <input type="text" class="text-input" placeholder="${placeholder}" value="${value}">
        `;

        return item;
    };

    // 关闭弹窗函数
    const closeSettingsModal = () => {
        const overlay = document.getElementById('SettingsOverlay');
        if (overlay) {
            overlay.style.animation = 'fadeOut 0.2s';
            setTimeout(() => overlay.remove(), 200);
        }
    };

    /* ==================== 弹窗逻辑 ==================== */
    const showSettings = () => {
        if (document.getElementById('SettingsOverlay')) {
            return;
        }

        // 创建覆盖层
        const overlay = document.createElement('div');
        overlay.id = 'SettingsOverlay';

        // 创建弹窗
        const modal = document.createElement('div');
        modal.className = 'optimized-scroll-modal';

        // 创建头部
        const header = document.createElement('div');
        header.className = 'modal-header';
        header.innerHTML = `
            <span>用户脚本设置</span>
        `;

        // 创建内容区域
        const body = document.createElement('div');
        body.className = 'modal-body';

        // 自动滚动开关
        const autoScroll = createSettingItem({
            label: '自动滚动页面',
            description: '启用后,页面将根据规则自动滚动以便加载更多内容',
            checked: GM_getValue("autoScrollSwitch", false),
        });

        // 文件打包开关
        const filePack = createSettingItem({
            label: '文件打包下载',
            description: '启用后,多个文件的作品将会以压缩包格式下载',
            checked: GM_getValue("packageDownloadFiles", true)
        });

        // 滚动次数设置
        const scrollCount = createNumberInput({
            label: '自动滚动次数',
            description: '自动滚动页面的次数(仅在启用自动滚动页面时可用)',
            value: GM_getValue("maxScrollCount", 50),
            min: 10,
            max: 5000,
            disabled: !GM_getValue("autoScrollSwitch", false),
        });

        // 名称格式设置
        // const nameFormat = createTextInput({
        //     label: '文件名称格式',
        //     description: '设置文件的名称格式(例如:{date}-{title})。',
        //     placeholder: '{date}-{title}',
        //     value: GM_getValue("fileNameFormat",)
        // });

        // 绑定自动滚动开关控制次数输入
        autoScroll.querySelector('input').addEventListener('change', (e) => {
            scrollCount.querySelector('input').disabled = !e.target.checked;
            scrollCount.querySelector('.number-input').style.opacity = e.target.checked ? 1 : 0.6;
        });

        // 组合内容
        body.appendChild(filePack);
        body.appendChild(autoScroll);
        body.appendChild(scrollCount);
        // body.appendChild(nameFormat);

        // 创建底部按钮
        const footer = document.createElement('div');
        footer.className = 'modal-footer';
        const saveBtn = document.createElement('button');
        saveBtn.className = 'primary-btn';
        saveBtn.textContent = '保存设置';
        const cancelBtn = document.createElement('button');
        cancelBtn.className = 'secondary-btn';
        cancelBtn.textContent = '放弃修改';
        footer.appendChild(saveBtn);
        footer.appendChild(cancelBtn);

        // 组装弹窗
        modal.appendChild(header);
        modal.appendChild(body);
        modal.appendChild(footer);
        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        // 保存事件
        saveBtn.addEventListener('click', () => {
            updateAutoScrollSwitch(autoScroll.querySelector('input').checked);
            updatePackageDownloadFiles(filePack.querySelector('input').checked);
            updateMaxScrollCount(parseInt(scrollCount.querySelector('input').value) || 50)
            // updateFileNameFormat(nameFormat.querySelector('.text-input').value.trim() || null);
            closeSettingsModal();
        });

        // 关闭事件
        cancelBtn.addEventListener('click', closeSettingsModal);
        overlay.addEventListener('click', (e) => e.target === overlay && closeSettingsModal());
    };

    /* ==================== 样式定义 ==================== */
    style = document.createElement('style');
    style.textContent = `
        /* 弹窗基础样式 */
        #imageSelectionOverlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.32);
            backdrop-filter: blur(4px);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 10000;
            animation: fadeIn 0.3s;
        }

        .image-selection-modal {
            background: white;
            border-radius: 16px;
            width: 80%;
            max-width: 900px;
            max-height: 90vh;
            box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
            overflow: hidden;
            animation: scaleUp 0.3s;
            display: flex;
            flex-direction: column;
        }

        /* 头部样式 */
        .modal-header {
            padding: 1rem;
            border-bottom: 1px solid #eee;
            text-align: center;
        }

        .modal-header span {
            font-size: 1.25rem;
            font-weight: 500;
            color: #212121;
        }

        /* 内容区域 */
        .modal-body {
            flex: 1;
            padding: 1rem;
            overflow-y: auto;
        }

        /* 图片网格 */
        .image-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
            gap: 12px;
        }

        .image-item {
            position: relative;
            border-radius: 8px;
            overflow: hidden;
            cursor: pointer;
            transition: all 0.2s;
            border: 2px solid transparent;
        }

        .image-item img {
            width: 100%;
            height: 100px;
            object-fit: cover;
            border-radius: 6px;
        }

        .image-item.selected {
            border-color: #2196F3;
        }

        .image-checkbox {
            position: absolute;
            top: 8px;
            right: 8px;
            width: 20px;
            height: 20px;
            opacity: 0;
        }

        .image-checkbox + label {
            position: absolute;
            top: 8px;
            right: 8px;
            width: 20px;
            height: 20px;
            background: white;
            border: 1px solid #ccc;
            border-radius: 50%;
            cursor: pointer;
            display: flex;
            justify-content: center;
            align-items: center;
            transition: all 0.2s;
        }

        .image-checkbox:checked + label {
            background: #2196F3;
            border-color: #2196F3;
        }

        .image-checkbox:checked + label::after {
            content: "✓";
            color: white;
            font-size: 12px;
        }

        /* 底部按钮 */
        .modal-footer {
            padding: 1rem;
            border-top: 1px solid #eee;
            display: flex;
            justify-content: flex-end;
            gap: 12px;
        }

        .primary-btn {
            background: #2196F3;
            color: white;
            padding: 8px 24px;
            border-radius: 24px;
            cursor: pointer;
            transition: all 0.2s;
        }

        .secondary-btn {
            background: #f0f0f0;
            color: #666;
            padding: 8px 24px;
            border-radius: 24px;
            cursor: pointer;
            transition: all 0.2s;
        }

        /* 动画 */
        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }

        @keyframes scaleUp {
            from { transform: scale(0.98); }
            to { transform: scale(1); }
        }
    `;
    document.head.appendChild(style);

    // 关闭弹窗函数
    const closeImagesModal = () => {
        const overlay = document.getElementById('imageSelectionOverlay');
        if (overlay) {
            overlay.style.animation = 'fadeOut 0.2s';
            setTimeout(() => overlay.remove(), 200);
        }
    };

    /* ==================== 弹窗逻辑 ==================== */
    const showImageSelectionModal = (imageUrls, name) => {
        if (document.getElementById('imageSelectionOverlay')) {
            return;
        }

        // 创建覆盖层
        const overlay = document.createElement('div');
        overlay.id = 'imageSelectionOverlay';

        // 创建弹窗
        const modal = document.createElement('div');
        modal.className = 'image-selection-modal';

        // 创建头部
        const header = document.createElement('div');
        header.className = 'modal-header';
        header.innerHTML = `
            <span>请选中需要下载的图片</span>
        `;

        // 创建内容区域
        const body = document.createElement('div');
        body.className = 'modal-body';

        // 创建图片网格
        const imageGrid = document.createElement('div');
        imageGrid.className = 'image-grid';

        // 动态生成图片项
        imageUrls.forEach((image) => {
            const item = document.createElement('div');
            item.className = 'image-item';

            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.className = 'image-checkbox';
            checkbox.id = `image-checkbox-${image.index}`;
            checkbox.checked = true;

            const label = document.createElement('label');
            label.htmlFor = `image-checkbox-${image.index}`;

            const img = document.createElement('img');
            img.src = image.webp;
            img.index = image.index;
            img.url = image.url;
            img.alt = `图片_${image.index}`;

            item.appendChild(checkbox);
            item.appendChild(label);
            item.appendChild(img);

            // 绑定点击事件
            item.addEventListener('click', (e) => {
                if (e.target.tagName !== 'INPUT') {
                    checkbox.checked = !checkbox.checked;
                    item.classList.toggle('selected', checkbox.checked);
                }
            });

            imageGrid.appendChild(item);
        });

        body.appendChild(imageGrid);

        // 创建底部按钮
        const footer = document.createElement('div');
        footer.className = 'modal-footer';
        const confirmBtn = document.createElement('button');
        confirmBtn.className = 'primary-btn';
        confirmBtn.textContent = '开始下载';
        const closeBtn = document.createElement('button');
        closeBtn.className = 'secondary-btn';
        closeBtn.textContent = '关闭窗口';
        footer.appendChild(confirmBtn);
        footer.appendChild(closeBtn);

        // 组装弹窗
        modal.appendChild(header);
        modal.appendChild(body);
        modal.appendChild(footer);
        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        // 确认事件
        confirmBtn.addEventListener('click', async () => {
            const selectedImages = Array.from(document.querySelectorAll('.image-checkbox:checked')).map((checkbox) => {
                let item = checkbox.parentElement.querySelector('img');
                return {
                    index: item.index, url: item.url,
                }
            });
            if (selectedImages.length === 0) {
                alert('请至少选择一张图片!');
                return;
            }
            closeImagesModal();
            await downloadImage(selectedImages, name)
        });

        // 关闭事件
        closeBtn.addEventListener('click', closeImagesModal);
        overlay.addEventListener('click', (e) => e.target === overlay && closeImagesModal());
    };

    // 创建主图标
    const createIcon = () => {
        const icon = document.createElement('div');
        icon.style = `
            position: fixed;
            bottom: ${config.position.bottom};
            left: ${config.position.left};
            width: ${config.icon[config.icon.type].size}px;
            height: ${config.icon[config.icon.type].size}px;
            background: white;
            border-radius: ${config.icon.image.borderRadius || '8px'};
            cursor: pointer;
            z-index: 9999;
            box-shadow: 0 3px 5px rgba(0,0,0,0.12), 0 3px 5px rgba(0,0,0,0.24);
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all ${config.animation.duration}s ${config.animation.easing};
        `;

        icon.style.backgroundImage = `url(${config.icon.image.url})`;
        icon.style.backgroundSize = 'cover';

        return icon;
    };

    // 创建菜单容器
    const menu = document.createElement('div');
    menu.style = `
        position: fixed;
        bottom: calc(${config.position.bottom} + ${config.icon[config.icon.type].size}px + 1rem);
        left: ${config.position.left};
        width: 255px;
        max-width: calc(100vw - 4rem);
        background: white;
        border-radius: 16px;
        box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
        overflow: hidden;
        display: none;
        z-index: 9998;
        transform-origin: bottom left;
        transition: all ${config.animation.duration}s ${config.animation.easing};
        opacity: 0;
        transform: translateY(10px) scaleY(0.95);
        will-change: transform, opacity;
    `;

    // 创建菜单内容容器
    const menuContent = document.createElement('div');
    menuContent.style = `
        max-height: 400px;
        overflow-y: auto;
        overscroll-behavior: contain;
    `;
    menu.appendChild(menuContent);

    // 初始化样式
    style = document.createElement('style');
    style.textContent = `
        :root {
            --primary: #2196F3;
            --surface: #ffffff;
            --on-surface: #212121;
            --ripple-color: rgba(33, 150, 243, 0.15);
            --border-radius: 12px;
        }

        .menu-item {
            display: flex;
            padding: 1rem 1.5rem;
            cursor: pointer;
            position: relative;
            transition: all 0.2s ease;
            align-items: center;
        }

        .menu-item:hover {
            background: var(--ripple-color);
        }

        .menu-item:not(:last-child) {
            border-bottom: 1px solid #eee;
        }

        .icon-container {
            margin-right: 1rem;
            display: flex;
            align-items: center;
        }

        .material-icons {
            font-size: 24px;
            color: var(--primary);
        }

        .content {
            flex: 1;
        }

        .xhstitle {
            font-size: 0.95rem;
            color: var(--on-surface);
            font-weight: 500;
            margin-bottom: 2px;
        }

        .subtitle {
            font-size: 0.75rem;
            color: #757575;
            line-height: 1.4;
        }

        .menu-enter {
            animation: slideIn ${config.animation.duration}s ${config.animation.easing};
        }

        .menu-exit {
            animation: slideOut ${config.animation.duration}s ${config.animation.easing};
        }

        @keyframes slideIn {
            from {
                opacity: 0;
                transform: translateY(10px) scaleY(0.95);
            }
            to {
                opacity: 1;
                transform: translateY(0) scaleY(1);
            }
        }

        @keyframes slideOut {
            from {
                opacity: 1;
                transform: translateY(0) scaleY(1);
            }
            to {
                opacity: 0;
                transform: translateY(10px) scaleY(0.95);
            }
        }

        .ripple {
            position: absolute;
            border-radius: 50%;
            transform: scale(0);
            animation: ripple 0.6s linear;
            background: var(--ripple-color);
            pointer-events: none;
        }

        @keyframes ripple {
            to {
                transform: scale(2);
                opacity: 0;
            }
        }

        /* 滚动条样式 */
        ::-webkit-scrollbar {
            width: 8px;
        }

        ::-webkit-scrollbar-track {
            background: #f1f1f1;
            border-radius: 10px;
        }

        ::-webkit-scrollbar-thumb {
            background: #c1c1c1;
            border-radius: 10px;
        }

        ::-webkit-scrollbar-thumb:hover {
            background: #a8a8a8;
        }
    `;
    document.head.appendChild(style);

    // 涟漪效果
    const createRipple = e => {
        const target = e.currentTarget;
        const rect = target.getBoundingClientRect();
        const size = Math.max(rect.width, rect.height);
        const x = e.clientX - rect.left - size / 2;
        const y = e.clientY - rect.top - size / 2;

        const ripple = document.createElement('span');
        ripple.className = 'ripple';
        ripple.style.width = ripple.style.height = `${size}px`;
        ripple.style.left = `${x}px`;
        ripple.style.top = `${y}px`;

        target.appendChild(ripple);
        setTimeout(() => ripple.remove(), 600);
    };

    // 隐藏菜单
    let hideTimeout;

    const hideMenu = () => {
        hideTimeout = setTimeout(() => {
            menu.classList.remove('menu-enter');
            menu.classList.add('menu-exit');
            menu.style.opacity = 0;
            menu.style.transform = 'translateY(10px) scaleY(0.95)';

            setTimeout(() => {
                menu.style.display = 'none';
                menu.classList.remove('menu-exit');
                isMenuVisible = false;
            }, config.animation.duration * 1000);
        }, 100);
    };

    let currentUrl;

    // 动态生成菜单内容
    const updateMenuContent = () => {
        menuContent.innerHTML = '';

        // 根据URL生成不同菜单项
        currentUrl = window.location.href;
        const menuItems = [];

        if (!config.disclaimer) {
            menuItems.push({
                text: 'README', icon: ' 📄 ', action: readme, description: '阅读脚本说明和免责声明'
            },);
        } else if (currentUrl === "https://www.xiaohongshu.com/explore" || currentUrl.includes("https://www.xiaohongshu.com/explore?")) {
            menuItems.push({
                text: '提取推荐作品链接',
                icon: ' ⛓ ',
                action: () => extractAllLinksEvent(-1),
                description: '提取当前页面的作品链接至剪贴板'
            },);
        } else if (currentUrl.includes("https://www.xiaohongshu.com/explore/")) {
            menuItems.push({
                text: '下载作品文件', icon: ' 📦 ', action: extractDownloadLinks, description: '下载当前作品的无水印文件'
            },);
        } else if (currentUrl.includes("https://www.xiaohongshu.com/user/profile/")) {
            menuItems.push({
                text: '提取发布作品链接',
                icon: ' ⛓ ',
                action: () => extractAllLinksEvent(0),
                description: '提取账号发布作品链接至剪贴板'
            }, {
                text: '提取点赞作品链接',
                icon: ' ⛓ ',
                action: () => extractAllLinksEvent(2),
                description: '提取账号点赞作品链接至剪贴板'
            }, {
                text: '提取收藏作品链接',
                icon: ' ⛓ ',
                action: () => extractAllLinksEvent(1),
                description: '提取账号收藏作品链接至剪贴板'
            },);
        } else if (currentUrl.includes("https://www.xiaohongshu.com/search_result")) {
            menuItems.push({
                text: '提取作品链接', icon: ' ⛓ ', action: () => extractAllLinksEvent(3), description: '提取搜索结果的作品链接至剪贴板'
            }, {
                text: '提取用户链接', icon: ' ⛓ ', action: () => extractAllLinksEvent(4), description: '提取搜索结果的用户链接至剪贴板'
            },);
        } else if (currentUrl.includes("https://www.xiaohongshu.com/board/")) {
            menuItems.push({
                text: "提取专辑作品链接",
                icon: ' ⛓ ',
                action: () => extractAllLinksEvent(5),
                description: '提取当前专辑的作品链接至剪贴板'
            },);
        }

        // 常用功能
        menuItems.push({
            separator: true
        }, {
            text: '修改用户脚本设置', icon: ' ⚙️ ', action: showSettings, description: '修改用户脚本设置'
        }, {
            text: '访问项目开源仓库', icon: ' 📒 ', action: about, description: '访问项目 GitHub 开源仓库'
        });

        // 创建菜单项
        menuItems.forEach(item => {
            if (item.separator) {
                const divider = document.createElement('div');
                divider.style = `
                    height: 8px;
                    background: #f5f5f5;
                `;
                menuContent.appendChild(divider);
                return;
            }

            const btn = document.createElement('div');
            btn.className = 'menu-item';
            btn.innerHTML = `
                <div class="icon-container">
                    <span class="material-icons">${item.icon}</span>
                </div>
                <div class="content">
                    <div class="xhstitle">${item.text}</div>
                    <div class="subtitle">${item.description}</div>
                </div>
            `;

            btn.addEventListener('click', (e) => {
                e.stopPropagation();
                item.action();
                hideMenu();
            });

            btn.addEventListener('mousedown', createRipple);

            menuContent.appendChild(btn);
        });
    };

    // URL监测相关
    let lastUrl = window.location.href;
    let isMenuVisible = false;

    // 显示菜单
    const showMenu = () => {
        clearTimeout(hideTimeout);
        menu.style.display = 'block';
        void menu.offsetHeight; // 触发重绘
        menu.classList.add('menu-enter');
        menu.style.opacity = 1;
        menu.style.transform = 'translateY(0) scaleY(1)';
        updateMenuContent();
        isMenuVisible = true;
    };

    // 事件监听
    const icon = createIcon();
    icon.addEventListener('mouseenter', showMenu);
    icon.addEventListener('mouseleave', hideMenu);
    menu.addEventListener('mouseenter', () => clearTimeout(hideTimeout));
    menu.addEventListener('mouseleave', hideMenu);

    // URL变化监听
    const setupUrlListener = () => {
        const observeUrl = () => {
            if (window.location.href !== lastUrl) {
                lastUrl = window.location.href;
                if (isMenuVisible) {
                    updateMenuContent();
                }
            }
            requestAnimationFrame(observeUrl);
        };
        observeUrl();
    };

    // 添加到页面
    document.body.appendChild(icon);
    document.body.appendChild(menu);
    document.head.appendChild(style);
    setupUrlListener();
})();