Greasy Fork

Greasy Fork is available in English.

X图片批量下载

可以抓取X页面图片,批量下载,添加了UI,且为图片添加了下载按钮,支持自定义命名,精确识别图片作者与发布时间

当前为 2025-11-22 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         X图片批量下载
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  可以抓取X页面图片,批量下载,添加了UI,且为图片添加了下载按钮,支持自定义命名,精确识别图片作者与发布时间
// @author       Aletia
// @author       原作者: 基础版
// @match        https://x.com/*
// @match        https://twitter.com/*
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @license      MIT
// @icon         https://abs.twimg.com/favicons/twitter.3.ico
// @connect      pbs.twimg.com
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// ==/UserScript==

/*
媒体页/media,因为脚本环境与X反爬限制,无法抓取多图推文,
但目前可以成功检测,链接会发送至消息框,为了配合此限制,多图推文添加了排除首图的功能按钮.
抓取是从当前已加载的内容中查找的,确保网络顺畅,否则加大滚动延迟.

并发数量与下载延迟不要太夸张,避免被封账号或IP,发生意外与本脚本无关.

本脚本在原作者wsdxb的基础上修改,原链接 http://greasyfork.icu/zh-CN/scripts/533499
*/


/*
MIT License

Copyright (c) 2024 基础版

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/


const DEBUG_MODE = false;
function debugLog(...args) {
    if (DEBUG_MODE) console.log('[X下载器]', ...args);
}

(function () {
    'use strict';

    // -------------------------- 配置与变量定义 --------------------------

    const CONFIG = {
        BATCH_SIZE: 1000,             // 批量抓取时的最大数量限制
        IMAGE_SCROLL_INTERVAL: 3000,  // 滚动间隔(ms),增加到3秒以等待加载
        IMAGE_MAX_SCROLL_COUNT: 100,  // 最大自动滚动次数,防止无限滚动
        SCROLL_DELAY: 1000,           // 每次滚动完成后的额外等待时间(ms)
        DOWNLOAD_DELAY: 100,          // 下载每张图片之间的延迟(ms)
        MAX_CONCURRENT_DOWNLOADS: 3,  // 并发下载任务数
        MIN_DOWNLOAD_DELAY: 100,      // 最小下载延迟限制(ms),防止封禁
        startDate: '19990101',        // 默认起始日期筛选 (YYYYMMDD)
        MAX_CONCURRENT_DOWNLOADS_LIMIT: 10, // 最大并发数的硬性上限
        SCROLL_STEP: 800,             // 每次滚动的像素距离
        NO_NEW_IMAGE_THRESHOLD: 5,    // 连续多少次滚动无新图片则停止
        DIRECT_DOWNLOAD_THRESHOLD: 3, // 当收集图片数量 <= 此值时,直接下载不打包ZIP
        // UI 紧凑度配置
        UI_SCALE: 1.2,            // 整体缩放比例 (0.7-1.0)
        FONT_SIZE_SCALE: 1.0,     // 字体大小缩放 (0.8-1.0)
        PADDING_SCALE: 0.8,       // 内边距缩放 (0.5-1.0)
        MARGIN_SCALE: 0.8,        // 外边距缩放 (0.5-1.0)
        SECTION_SPACING: 0.8,     // 区块间距缩放 (0.5-1.0)
        BUTTON_HEIGHT_SCALE: 0.8  // 按钮高度缩放 (0.7-1.0)
    };

    let cancelDownload = false;
    let isCollecting = false;
    let isPaused = false;
    let isUIOpen = false;

    const imageLinksSet = new Set();
    const imageMetadataMap = new Map();
    const tweetImageCountMap = new Map();
    const multiImageTweetUrls = new Set();

    // -------------------------- UI样式 --------------------------
    const styles = `
        .x-downloader-floating-btn {
            position: fixed;
            top: 80px;
            right: 20px;
            width: ${30 * CONFIG.UI_SCALE}px;
            height: ${30 * CONFIG.UI_SCALE}px;
            background: #1DA1F2;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: ${16 * CONFIG.FONT_SIZE_SCALE}px;
            cursor: pointer;
            z-index: 10000;
            box-shadow: 0 4px 12px rgba(0,0,0,0.5);
            border: 2px solid #2d3741;
            transition: all 0.3s ease;
        }

        .x-downloader-floating-btn:hover {
            transform: scale(1.1);
            box-shadow: 0 6px 16px rgba(0,0,0,0.6);
        }

        .x-downloader-ui {
            position: fixed;
            top: 80px;
            right: 80px;
            width: ${400 * CONFIG.UI_SCALE}px;
            max-height: 85vh;
            background: #15202b;
            border-radius: ${8 * CONFIG.UI_SCALE}px;
            box-shadow: 0 8px 24px rgba(0,0,0,0.4);
            z-index: 10001;
            display: none;
            padding: ${10 * CONFIG.PADDING_SCALE}px;
            border: 1px solid #38444d;
            color: #e7e9ea;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            font-size: ${12 * CONFIG.FONT_SIZE_SCALE}px;
            overflow-y: auto;
        }

        .x-downloader-ui.open {
            display: block;
        }

        .x-downloader-section {
            margin-bottom: ${8 * CONFIG.SECTION_SPACING}px;
            padding-bottom: ${6 * CONFIG.PADDING_SCALE}px;
            border-bottom: 1px solid #38444d;
        }

        .x-downloader-section:last-child {
            border-bottom: none;
            margin-bottom: 0;
        }

        .x-downloader-section-title {
            font-weight: 600;
            margin-bottom: ${4 * CONFIG.MARGIN_SCALE}px;
            color: #e7e9ea;
            font-size: ${12 * CONFIG.FONT_SIZE_SCALE}px;
            display: flex;
            align-items: center;
            gap: ${4 * CONFIG.MARGIN_SCALE}px;
        }

        .x-downloader-input-group {
            margin-bottom: ${4 * CONFIG.MARGIN_SCALE}px;
        }

        .x-downloader-label {
            display: block;
            margin-bottom: ${2 * CONFIG.MARGIN_SCALE}px;
            font-size: ${11 * CONFIG.FONT_SIZE_SCALE}px;
            color: #8b98a5;
        }

        .x-downloader-input, .x-downloader-select {
            width: 100%;
            padding: ${4 * CONFIG.PADDING_SCALE}px ${6 * CONFIG.PADDING_SCALE}px;
            background: #1e2732;
            border: 1px solid #38444d;
            border-radius: ${3 * CONFIG.UI_SCALE}px;
            font-size: ${11 * CONFIG.FONT_SIZE_SCALE}px;
            box-sizing: border-box;
            color: #e7e9ea;
        }

        .x-downloader-input:focus, .x-downloader-select:focus {
            outline: none;
            border-color: #1DA1F2;
        }

        .x-downloader-checkbox {
            margin-right: ${4 * CONFIG.MARGIN_SCALE}px;
            accent-color: #1DA1F2;
        }

        .x-downloader-row {
            display: flex;
            gap: ${6 * CONFIG.MARGIN_SCALE}px;
            margin-bottom: ${4 * CONFIG.MARGIN_SCALE}px;
        }

        .x-downloader-row .x-downloader-input {
            flex: 1;
        }

        .x-downloader-btn-group {
            display: flex;
            gap: ${4 * CONFIG.MARGIN_SCALE}px;
            margin-top: ${8 * CONFIG.MARGIN_SCALE}px;
        }

        .x-downloader-btn {
            flex: 1;
            padding: ${6 * CONFIG.PADDING_SCALE}px ${8 * CONFIG.PADDING_SCALE}px;
            background: #1DA1F2;
            color: white;
            border: none;
            border-radius: ${3 * CONFIG.UI_SCALE}px;
            cursor: pointer;
            font-size: ${11 * CONFIG.FONT_SIZE_SCALE}px;
            font-weight: 600;
            transition: background 0.2s;
            text-align: center;
            min-height: ${28 * CONFIG.BUTTON_HEIGHT_SCALE}px;
        }

        .x-downloader-btn:hover {
            background: #1a91da;
        }

        .x-downloader-btn:disabled {
            background: #253341;
            color: #6e767d;
            cursor: not-allowed;
        }

        .x-downloader-btn.secondary {
            background: #253341;
            color: #e7e9ea;
            text-align: center;
        }

        .x-downloader-btn.secondary:hover {
            background: #2d3741;
            color: #e7e9ea;
        }

        .x-downloader-btn.warning {
            background: #f91880;
            color: white;
            text-align: center;
        }

        .x-downloader-btn.warning:hover {
            background: #e01673;
        }

        .x-downloader-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0,0,0,0.5);
            z-index: 9999;
            display: none;
        }

        /* 文件命名规则区域 */
        .naming-pattern-container {
            background: #1e2732;
            border: 1px solid #38444d;
            border-radius: ${3 * CONFIG.UI_SCALE}px;
            padding: ${4 * CONFIG.PADDING_SCALE}px;
            margin-bottom: ${4 * CONFIG.MARGIN_SCALE}px;
        }

        .pattern-tags {
            display: flex;
            flex-wrap: wrap;
            gap: ${7 * CONFIG.MARGIN_SCALE}px;
            min-height: ${18 * CONFIG.UI_SCALE}px;
        }

        .pattern-tag {
            background: #253341;
            color: #8b98a5;
            padding: ${4 * CONFIG.PADDING_SCALE}px ${6 * CONFIG.PADDING_SCALE}px;
            border-radius: ${8 * CONFIG.UI_SCALE}px;
            font-size: ${10 * CONFIG.FONT_SIZE_SCALE}px;
            cursor: pointer;
            user-select: none;
            border: 1px solid #38444d;
            transition: all 0.2s;
        }

        .pattern-tag.active {
            background: #1DA1F2;
            color: white;
            border-color: #1DA1F2;
        }

        .pattern-tag:hover {
            background: #2d3741;
            color: #e7e9ea;
        }

        .pattern-tag.active:hover {
            background: #1a91da;
        }

        .pattern-tag.dragging {
            opacity: 0.5;
        }

        /* 时间筛选布局 */
        .date-row {
            display: flex;
            gap: ${6 * CONFIG.MARGIN_SCALE}px;
            margin-bottom: ${4 * CONFIG.MARGIN_SCALE}px;
        }

        .date-item {
            flex: 1;
        }

        /* 抓取设置布局 */
        .settings-row {
            display: flex;
            gap: ${6 * CONFIG.MARGIN_SCALE}px;
            margin-bottom: ${4 * CONFIG.MARGIN_SCALE}px;
        }

        .settings-item {
            flex: 1;
        }

        /* 文件夹设置 */
        .folder-input {
            width: 100%;
            padding: ${4 * CONFIG.PADDING_SCALE}px ${6 * CONFIG.PADDING_SCALE}px;
            background: #1e2732;
            border: 1px solid #38444d;
            border-radius: ${3 * CONFIG.UI_SCALE}px;
            font-size: ${11 * CONFIG.FONT_SIZE_SCALE}px;
            box-sizing: border-box;
            color: #e7e9ea;
        }

        .folder-input:focus {
            outline: none;
            border-color: #1DA1F2;
        }

        /* 通知消息 */
        .x-downloader-notification {
            position: fixed;
            top: 140px;
            right: 20px;
            padding: ${6 * CONFIG.PADDING_SCALE}px ${10 * CONFIG.PADDING_SCALE}px;
            background: #15202b;
            color: #e7e9ea;
            border-radius: ${4 * CONFIG.UI_SCALE}px;
            border: 1px solid #38444d;
            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
            z-index: 10003;
            font-size: ${11 * CONFIG.FONT_SIZE_SCALE}px;
            max-width: ${280 * CONFIG.UI_SCALE}px;
            display: none;
        }

        .notification-error {
            border-left: 3px solid #f91880;
        }

        .notification-success {
            border-left: 3px solid #00ba7c;
        }

        .notification-warning {
            border-left: 3px solid #f7931a;
        }

        .notification-info {
            border-left: 3px solid #1DA1F2;
        }

        /* 按钮组布局 */
        .action-buttons {
            display: flex;
            gap: ${4 * CONFIG.MARGIN_SCALE}px;
            margin-top: ${8 * CONFIG.MARGIN_SCALE}px;
        }

        .action-btn {
            flex: 1;
            padding: ${6 * CONFIG.PADDING_SCALE}px ${8 * CONFIG.PADDING_SCALE}px;
            border: none;
            border-radius: ${3 * CONFIG.UI_SCALE}px;
            cursor: pointer;
            font-size: ${11 * CONFIG.FONT_SIZE_SCALE}px;
            font-weight: 600;
            transition: background 0.2s;
            text-align: center;
            min-height: ${28 * CONFIG.BUTTON_HEIGHT_SCALE}px;
        }

        .action-btn.primary {
            background: #1DA1F2;
            color: white;
        }

        .action-btn.primary:hover {
            background: #1a91da;
        }

        .action-btn.secondary {
            background: #253341;
            color: #e7e9ea;
        }

        .action-btn.secondary:hover {
            background: #2d3741;
        }

        .action-btn.warning {
            background: #f91880;
            color: white;
        }

        .action-btn.warning:hover {
            background: #e01673;
        }

        .action-btn:disabled {
            background: #253341;
            color: #6e767d;
            cursor: not-allowed;
        }

        /* 进度条样式 */
        .progress-container {
            margin: ${8 * CONFIG.MARGIN_SCALE}px 0;
            background: #1e2732;
            border-radius: ${3 * CONFIG.UI_SCALE}px;
            padding: ${6 * CONFIG.PADDING_SCALE}px;
            display: none;
        }

        .progress-info {
            display: flex;
            justify-content: space-between;
            font-size: ${10 * CONFIG.FONT_SIZE_SCALE}px;
            margin-bottom: ${3 * CONFIG.MARGIN_SCALE}px;
            color: #8b98a5;
        }

        .progress-bar {
            width: 100%;
            height: ${4 * CONFIG.UI_SCALE}px;
            background: #38444d;
            border-radius: ${2 * CONFIG.UI_SCALE}px;
            overflow: hidden;
        }

        .progress-fill {
            height: 100%;
            background: #1DA1F2;
            border-radius: ${2 * CONFIG.UI_SCALE}px;
            transition: width 0.3s ease;
            width: 0%;
        }

        /* LOG对话框样式 - 整合到主UI中 */
        .log-section {
            margin-top: ${8 * CONFIG.MARGIN_SCALE}px;
            border-top: 1px solid #38444d;
            padding-top: ${8 * CONFIG.PADDING_SCALE}px;
        }

        .log-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: ${6 * CONFIG.MARGIN_SCALE}px;
        }

        .log-title {
            font-weight: 600;
            color: #e7e9ea;
            font-size: ${12 * CONFIG.FONT_SIZE_SCALE}px;
            display: flex;
            align-items: center;
            gap: ${4 * CONFIG.MARGIN_SCALE}px;
        }

        .log-controls {
            display: flex;
            gap: ${4 * CONFIG.MARGIN_SCALE}px;
        }

        .log-btn {
            padding: ${3 * CONFIG.PADDING_SCALE}px ${6 * CONFIG.PADDING_SCALE}px;
            background: #253341;
            color: #e7e9ea;
            border: none;
            border-radius: ${3 * CONFIG.UI_SCALE}px;
            cursor: pointer;
            font-size: ${10 * CONFIG.FONT_SIZE_SCALE}px;
            transition: background 0.2s;
        }

        .log-btn:hover {
            background: #2d3741;
        }

        .log-content {
            height: ${100 * CONFIG.UI_SCALE}px;
            background: #1e2732;
            border: 1px solid #38444d;
            border-radius: ${3 * CONFIG.UI_SCALE}px;
            padding: ${6 * CONFIG.PADDING_SCALE}px;
            overflow-y: auto;
            font-family: 'Courier New', monospace;
            font-size: ${10 * CONFIG.FONT_SIZE_SCALE}px;
            line-height: 1.3;
            white-space: pre-wrap;
            word-break: break-all;
        }

        .log-content:empty::before {
            content: "暂无日志信息";
            color: #6e767d;
            font-style: italic;
        }

        .log-link {
            color: #1DA1F2;
            text-decoration: none;
            cursor: pointer;
            margin: ${1 * CONFIG.MARGIN_SCALE}px 0;
            display: block;
        }

        .log-link:hover {
            text-decoration: underline;
            color: #1a91da;
        }

        .log-warning {
            color: #f7931a;
            font-weight: 600;
            margin-bottom: ${4 * CONFIG.MARGIN_SCALE}px;
        }
        /* 新增:绿色成功按钮样式 */
        .action-btn.success {
            background: #00ba7c;
            color: white;
        }
        .action-btn.success:hover {
            background: #00a36d;
        }
        .action-btn.success:disabled {
            background: #253341;
            color: #6e767d;
            cursor: not-allowed;
        }
    `;

    // -------------------------- 工具函数 --------------------------
    function getUsername() {
        const m = window.location.pathname.match(/^\/([^\/\?]+)/);
        return m ? m[1] : 'unknown_user';
    }

    function getDisplayName() {
        // 首先检查是否是首页
        if (window.location.pathname === '/' || window.location.pathname === '/home') {
            return 'home';
        }

        try {
            const isStatusPage = window.location.pathname.includes('/status/');

            // 同时兼容旧的 User-Name / 新版 UserName / 时间线 UserCell
            // 注意:在 /media 页面,顶部的个人信息栏通常有 [data-testid="UserName"]
            const container = document.querySelector('[data-testid="User-Name"], [data-testid="UserName"], [data-testid="UserCell"]');
            if (!container) return getUsername();

            const spans = container.querySelectorAll('span');

            for (const span of spans) {
                const text = span.textContent?.trim();
                if (!text || text.startsWith('@') || text.length <= 1) continue;

                // 关键:认证徽章所在的 span 一定含有 svg,直接跳过
                if (span.querySelector('svg')) continue;

                return text.replace(/[\\/:*?"<>|]/g, '_').replace(/\s+/g, '_');
            }
            return getUsername();
        } catch (e) {
            console.error('获取显示名称失败:', e);
            return getUsername();
        }
    }


    // 从图片元素直接提取精准的 推文ID 和 用户名,不再依赖不稳定的 DOM 结构查找
    function getTweetInfoFromElement(img) {
        const link = img.closest('a[href*="/status/"]');
        if (!link) return null;

        const href = link.getAttribute('href');
        // 匹配格式: /Username/status/TweetID
        const match = href.match(/^\/([^\/]+)\/status\/(\d+)/);

        if (match) {
            return {
                username: match[1], // 这里的 username (如 NobleQAli) 是绝对正确的
                tweetId: match[2]
            };
        }
        return null;
    }

    // 查找一组元素的"最小公共祖先"容器 (用于多图 Grid 定位)
    function findCommonContainer(elements) {
        if (!elements || elements.length === 0) return null;
        if (elements.length === 1) {
            // 单图:返回 tweetPhoto 或 videoPlayer 容器
            return elements[0].closest('[data-testid="tweetPhoto"]') ||
                elements[0].closest('[data-testid="videoPlayer"]') ||
                elements[0].parentElement;
        }

        // 多图:向上查找直到找到包含所有图片的容器
        let container = elements[0].parentElement;
        while (container && container !== document.body) {
            const allContained = elements.every(el => container.contains(el));
            if (allContained) {
                return container;
            }
            container = container.parentElement;
        }
        return elements[0].parentElement; // 兜底
    }

    // -------------------------- 增强的日期获取函数 --------------------------

    function getCurrentDate() {
        const now = new Date();
        const year = now.getFullYear();
        const month = String(now.getMonth() + 1).padStart(2, '0');
        const day = String(now.getDate()).padStart(2, '0');
        return `${year}${month}${day}`;
    }

    function getDateFromTweetId(tweetId) {
        if (!tweetId) return null;
        try {
            const id = BigInt(tweetId);
            const TWITTER_EPOCH = 1288834974657n;
            const timestamp = (id >> 22n) + TWITTER_EPOCH;
            const utcDate = new Date(Number(timestamp));

            // 修复:正确转换为本地时间
            const localDate = new Date(utcDate.getTime() - utcDate.getTimezoneOffset() * 60000);
            return localDate;
        } catch (e) {
            debugLog('解析Tweet ID失败:', e);
            return null;
        }
    }

    // -------------------------- LOG对话框功能 --------------------------

    function createLogSection() {
        const logSection = document.createElement('div');
        logSection.className = 'log-section';
        logSection.innerHTML = `
            <div class="log-header">
                <div class="log-title">📝 多图推文检测日志</div>
                <div class="log-controls">
                    <button class="log-btn" id="clearLog">清空</button>
                </div>
            </div>
            <div class="log-content" id="logContent"></div>
        `;
        return logSection;
    }

    function addToLog(message, isLink = false) {
        const logContent = document.getElementById('logContent');
        if (!logContent) return;

        const logEntry = document.createElement('div');
        const timestamp = new Date().toLocaleTimeString();

        if (isLink) {
            const link = document.createElement('a');
            link.className = 'log-link';
            link.href = message;
            link.target = '_blank';
            link.textContent = `[${timestamp}] ${message}`;
            link.title = '点击在新标签页打开';
            logEntry.appendChild(link);
        } else {
            logEntry.textContent = `[${timestamp}] ${message}`;
        }

        logContent.appendChild(logEntry);

        // 自动滚动到底部
        logContent.scrollTop = logContent.scrollHeight;

        // 根据内容量显示/隐藏滚动条
        updateLogScrollbar();
    }

    function updateLogScrollbar() {
        const logContent = document.getElementById('logContent');
        if (!logContent) return;

        // 如果内容高度超过容器高度,显示滚动条
        if (logContent.scrollHeight > logContent.clientHeight) {
            logContent.style.overflowY = 'scroll';
        } else {
            logContent.style.overflowY = 'auto';
        }
    }

    function clearLog() {
        const logContent = document.getElementById('logContent');
        if (logContent) {
            logContent.innerHTML = '';
            multiImageTweetUrls.clear(); // 同时清空存储的链接
            updateLogScrollbar();
        }
    }

    function addMultiImageTweetToLog(tweetId) {
        if (!tweetId) return;

        const username = getUsername();
        const tweetUrl = `https://x.com/${username}/status/${tweetId}`;

        // 避免重复添加
        if (multiImageTweetUrls.has(tweetUrl)) {
            return;
        }

        multiImageTweetUrls.add(tweetUrl);

        // 如果是第一个多图推文,添加警告信息
        if (multiImageTweetUrls.size === 1) {
            addToLog('受脚本环境与X反爬限制,多图推文只能抓取首图,需手动打开详情页抓取:', false);
            addToLog('检测到多图推文:', false);
        }

        addToLog(tweetUrl, true);
    }

    // -------------------------- 图片收集和筛选逻辑 --------------------------

    // 全局变量,记录已经处理过的推文ID
    const processedTweets = new Set();

    // 收集页面图片
    function getAllImages() {
        const maxBatch = parseInt(document.getElementById('batchSize')?.value) || CONFIG.BATCH_SIZE;
        let addedFromDOM = 0;
        const currentRound = window.collectionRound ? window.collectionRound + 1 : 1;
        window.collectionRound = currentRound;
        const tweetImageIndexMap = new Map();

        const primaryColumn = document.querySelector('[data-testid="primaryColumn"]');
        if (!primaryColumn) {
            debugLog(`第${currentRound}轮: 未找到主内容列,跳过`);
            return 0;
        }

        primaryColumn.querySelectorAll('img[src*="pbs.twimg.com/media/"]').forEach(img => {
            if (imageLinksSet.size >= maxBatch) return;

            const currentSrc = img.src;
            const formatMatch = currentSrc.match(/format=([a-zA-Z0-9]+)/);
            const ext = formatMatch ? formatMatch[1] : 'jpg';
            let url = currentSrc.split('?')[0] + `?format=${ext}&name=orig`;

            // 1. 图片级去重:只要链接存在,就跳过 (这是最关键的去重)
            if (imageLinksSet.has(url)) return;

            // 2. 获取精准信息
            const info = getTweetInfoFromElement(img);
            if (!info) return;

            const tweetId = info.tweetId;
            let authorUsername = info.username;
            let authorDisplayName = authorUsername;

            // 3. 时间筛选
            const postDate = getDateFromTweetId(tweetId)?.toISOString() || 'unknown';
            if (!applyFilters({ postDate })) return;

            // 4. 反向查找显示名称
            try {
                const closestArticle = img.closest('article') || img.closest('[data-testid="tweet"]');
                if (closestArticle) {
                    const nameElements = closestArticle.querySelectorAll('[data-testid="User-Name"]');
                    for (const el of nameElements) {
                        if (el.textContent.toLowerCase().includes(`@${authorUsername.toLowerCase()}`)) {
                            const text = el.textContent;
                            const parts = text.split('@');
                            if (parts.length > 0) authorDisplayName = parts[0].trim();
                            break;
                        }
                    }
                }
            } catch (e) {}

            // 4.1 如果本地没找到,尝试从页面全局头部获取(针对 Media 页面)
            if (authorDisplayName === authorUsername && authorUsername.toLowerCase() === getUsername().toLowerCase()) {
                const globalName = getDisplayName();
                if (globalName && globalName !== 'home') {
                    authorDisplayName = globalName;
                }
            }

            if (!tweetImageIndexMap.has(tweetId)) tweetImageIndexMap.set(tweetId, 0);
            const index = tweetImageIndexMap.get(tweetId);
            tweetImageIndexMap.set(tweetId, index + 1);

            imageLinksSet.add(url);
            imageMetadataMap.set(url, {
                tweetId, postDate, url, index, authorUsername, authorDisplayName
            });

            addedFromDOM++;
            if (!tweetImageCountMap.has(tweetId)) tweetImageCountMap.set(tweetId, []);
            tweetImageCountMap.get(tweetId).push(url);

            // 记录推文ID (仅用于日志或统计,不影响抓取)
            processedTweets.add(tweetId);
        });

        debugLog(`第${currentRound}轮 DOM 抓取完成,共 ${addedFromDOM} 张`);
        return addedFromDOM;
    }

    // 在所有抓取完成后执行多图检测
    async function detectMultiImageTweetsAfterScraping() {
        // 仅在/media页面进行多图检测
        if (!window.location.pathname.includes('/media')) {
            debugLog('非/media页面,跳过多图检测');
            return;
        }

        // 1. 首先提取所有"实际已收集"的推文ID
        const collectedTweetIds = new Set();
        for (const meta of imageMetadataMap.values()) {
            if (meta.tweetId) {
                collectedTweetIds.add(meta.tweetId);
            }
        }

        const tweetLinks = Array.from(document.querySelectorAll('a[href*="/status/"]'))
        .map(a => a.href.match(/\/status\/(\d+)/)?.[1])
        .filter(Boolean);

        debugLog(`开始多图检测,页面可见推文 ${tweetLinks.length} 个`);

        let multiImageCount = 0;
        let processedTweetsThisRound = new Set(); // 本轮已处理的推文

        for (const tweetId of tweetLinks) {
            // 2. 如果当前扫描到的推文ID不在"已收集列表"中,直接跳过
            if (!collectedTweetIds.has(tweetId)) {
                continue;
            }

            // 如果已经处理过该推文,跳过
            if (processedTweets.has(tweetId) || processedTweetsThisRound.has(tweetId)) {
                continue;
            }

            const already = tweetImageCountMap.get(tweetId) || [];
            // 找到该推文的所有图片链接
            const linkElements = [...document.querySelectorAll('a[href*="/status/"]')]
            .filter(a => a.href.includes(`/status/${tweetId}`));

            if (linkElements.length === 0) {
                processedTweets.add(tweetId);
                processedTweetsThisRound.add(tweetId);
                continue;
            }

            // 对每个图片链接单独检测
            let hasMultiPhotoInAnyItem = false;
            for (const linkElement of linkElements) {
                // 使用精确的列表项检测
                const svgDetectionResult = detectMultiImageByListItem(linkElement);
                if (svgDetectionResult.detected) {
                    hasMultiPhotoInAnyItem = true;
                    // 检测到多图时添加到日志
                    addMultiImageTweetToLog(tweetId);
                    break; // 只要有一个图片检测到多图,就标记整个推文为多图
                }
            }

            // 如果检测到多图推文但只抓取到一张图片,计数
            if (hasMultiPhotoInAnyItem && already.length === 1) {
                multiImageCount++;
                debugLog(`精确检测到 ${tweetId}`);
            }

            // 标记为已处理
            processedTweets.add(tweetId);
            processedTweetsThisRound.add(tweetId);
        }
    }

    // 更精确的多图检测函数 - 针对每个图片单独检测
    function detectMultiImageByListItem(linkElement) {
        if (!linkElement) {
            return { detected: false, method: "无链接元素" };
        }

        // 找到包含当前图片的列表项
        const listItem = linkElement.closest('li[role="listitem"]');
        if (!listItem) {
            return { detected: false, method: "未找到列表项" };
        }

        // 在当前列表项内检测SVG
        const svgPaths = listItem.querySelectorAll('svg path');

        for (const path of svgPaths) {
            const d = path.getAttribute('d');
            if (!d) continue;

            // 简化匹配条件1: 只检测关键路径特征
            if (d.includes('M2 8.5') && d.includes('M19.5 4')) {
                return { detected: true, method: "简化SVG路径匹配" };
            }

            // 简化匹配条件2: 检测矩形绘制命令
            if ((d.includes('M2 8.5') || d.includes('M19.5 4')) &&
                d.includes('C16.88 6') && d.includes('v11')) {
                return { detected: true, method: "关键点匹配" };
            }

        }

        return { detected: false, method: "列表项内未匹配到多图特征" };
    }

    function applyFilters(metadata) {
        if (metadata.postDate && metadata.postDate !== 'unknown') {
            const postYMD = metadata.postDate.slice(0,10).replace(/-/g,'');
            const start = (document.getElementById('startDate')?.value || '').replace(/[^\d]/g,'') || '19990101';
            const end = (document.getElementById('endDate')?.value || '').replace(/[^\d]/g,'') || new Date().toISOString().slice(0,10).replace(/-/g,'');
            if (postYMD < start || postYMD > end) return false;
        }
        return true;
    }

    // ==================== 文件命名 ====================

    function generateFileName(url, customMeta = null) {
        const meta = customMeta || imageMetadataMap.get(url);
        const settings = JSON.parse(localStorage.getItem('xDownloaderSettings') || '{}');
        const pattern = settings.fileNamePattern || [];

        const formatMatch = url.match(/format=([a-zA-Z0-9]+)/);
        const ext = formatMatch ? formatMatch[1] : 'jpg';

        if (pattern.length === 0) {
            const uniqueId = meta.tweetId || 'unknown';
            const index = meta.index !== undefined ? meta.index : 0;
            return `${uniqueId}_p${index}.${ext}`;
        }

        const parts = pattern.map(key => {
            switch (key) {
                case 'displayName':
                    // 优先使用溯源得到的作者显示名称
                    return meta.authorDisplayName || getDisplayName();
                case 'username':
                    // 优先使用溯源得到的作者用户名
                    return meta.authorUsername || getUsername();
                case 'tweetId': return meta.tweetId || 'unknown';
                case 'postDate': return meta.postDate && meta.postDate !== 'unknown' ? meta.postDate.slice(0,10).replace(/-/g,'') : '';
                case 'time': return meta.postDate && meta.postDate !== 'unknown' ? meta.postDate.slice(11,16).replace(':','') : '';
                default: return '';
            }
        }).filter(Boolean);

        let name = parts.length ? parts.join('_') : 'image';

        if (meta.index !== undefined && meta.index >= 0) {
            name += `_p${meta.index}`;
        }

        return name.replace(/[\\/:*?"<>|]/g, '_') + '.' + ext;
    }

    // ==================== 下载器 ====================

    const Downloader = (() => {
        let currentDownloads = [];

        return {
            async add(urls) {
                if (cancelDownload) return;

                // 判断数量是否触发直接下载
                if (urls.length <= CONFIG.DIRECT_DOWNLOAD_THRESHOLD) {
                    await this.downloadDirectly(urls);
                } else {
                    await this.downloadAsZip(urls);
                }
            },

            // 新增:直接下载图片(不打包ZIP)
            async downloadDirectly(urls) {
                const settings = saveSettings();
                const delay = parseInt(document.getElementById('downloadDelay')?.value) || 100;

                let done = 0;
                currentDownloads = [];

                updateProgressBar(0, urls.length, '准备直接下载...');

                for (let i = 0; i < urls.length; i++) {
                    if (cancelDownload) break;

                    const url = urls[i];
                    const filename = generateFileName(url);

                    try {
                        // 使用 XHR 获取 Blob 以确保文件名正确且能控制下载流
                        const blob = await new Promise((resolve, reject) => {
                            const request = GM_xmlhttpRequest({
                                method: "GET",
                                url: url,
                                responseType: "blob",
                                onload: r => resolve(r.response),
                                onerror: reject
                            });
                            currentDownloads.push(request);
                        });

                        if (cancelDownload) break;

                        // 调用 GM_download 保存文件
                        GM_download({
                            url: URL.createObjectURL(blob),
                            name: filename,
                            saveAs: false
                        });

                        done++;
                        updateProgressBar(done, urls.length, `直接下载中... (${done}/${urls.length})`);

                    } catch (e) {
                        console.error('直接下载失败', url, e);
                        addToLog(`下载失败: ${filename}`, false);
                    }

                    // 应用下载延迟
                    if (i < urls.length - 1) {
                        await new Promise(r => setTimeout(r, delay));
                    }
                }

                if (cancelDownload) {
                    showNotification('下载已取消', 'warning');
                    addToLog('下载已取消', false);
                    updateButtonState('idle');
                } else {
                    showNotification('图片下载完成!', 'success');
                    addToLog('所有图片下载完成!', false);
                    updateButtonState('completed');
                }

                // 延迟隐藏进度条
                setTimeout(() => {
                    const progressContainer = document.getElementById('progressContainer');
                    if (progressContainer) {
                        progressContainer.style.display = 'none';
                    }
                }, 2000);
            },

            async downloadAsZip(urls) {
                const zip = new JSZip();
                const zipName = document.getElementById('folderName')?.value.trim() || getDisplayName();
                const settings = saveSettings();
                // 打包下载使用延迟
                const delay = parseInt(document.getElementById('downloadDelay')?.value) || 100;
                const concurrent = settings.maxConcurrent;

                let done = 0;
                currentDownloads = [];

                // 显示进度条
                updateProgressBar(0, urls.length, '准备打包下载...');

                const worker = async (workerId) => {
                    // 打包下载使用延迟
                    await new Promise(resolve => setTimeout(resolve, workerId * (delay / 2)));
                    while (done < urls.length && !cancelDownload) {
                        const url = urls[done++];
                        try {
                            const blob = await new Promise((res, rej) => {
                                const request = GM_xmlhttpRequest({
                                    method: "GET",
                                    url: url,
                                    responseType: "blob",
                                    onload: r => res(r.response),
                                    onerror: rej
                                });
                                currentDownloads.push(request);
                            });

                            if (cancelDownload) break;

                            zip.file(generateFileName(url), blob);
                            updateProgressBar(done, urls.length, `打包中... (${done}/${urls.length})`);
                        } catch (e) {
                            console.error('下载失败', url);
                        }
                        // 打包下载使用延迟
                        await new Promise(r => setTimeout(r, delay));
                    }
                };
                await Promise.all(Array(concurrent).fill().map((_, i) => worker(i)));

                if (cancelDownload) {
                    showNotification('下载已取消', 'warning');
                    addToLog('下载已取消', false);
                    updateButtonState('idle');
                    return;
                }

                const blob = await zip.generateAsync({type:'blob'});
                GM_download({
                    url: URL.createObjectURL(blob),
                    name: `${zipName}_${getCurrentDate()}.zip`,
                    saveAs: true
                });
                showNotification('ZIP 下载完成!', 'success');
                addToLog('ZIP 下载完成!', false);
                updateProgressBar(urls.length, urls.length, '下载完成');

                // 下载完成后隐藏进度条
                setTimeout(() => {
                    const progressContainer = document.getElementById('progressContainer');
                    if (progressContainer) {
                        progressContainer.style.display = 'none';
                    }
                }, 2000);
                updateButtonState('completed');
            },

            cancel() {
                currentDownloads.forEach(request => {
                    try {
                        request.abort();
                    } catch (e) {
                        console.error('取消下载请求失败:', e);
                    }
                });
                currentDownloads = [];
            }
        };
    })();

    // -------------------------- 核心功能函数 --------------------------

    async function autoScrollAndCollectImages() {
        cancelDownload = false;
        imageLinksSet.clear();
        imageMetadataMap.clear();
        tweetImageCountMap.clear();
        processedTweets.clear();
        window.collectionRound = 0;

        const settings = saveSettings();
        const config = {
            BATCH_SIZE: parseInt(document.getElementById('batchSize').value) || CONFIG.BATCH_SIZE,
            IMAGE_SCROLL_INTERVAL: parseInt(document.getElementById('scrollInterval').value) || CONFIG.IMAGE_SCROLL_INTERVAL,
            IMAGE_MAX_SCROLL_COUNT: 100,
            SCROLL_DELAY: 1000,
            NO_NEW_IMAGE_THRESHOLD: 3
        };

        debugLog('当前配置:', config);

        updateButtonState('collecting');
        isCollecting = true;
        isPaused = false;

        showNotification('开始收集图片...', 'info');
        addToLog('开始收集图片...', false);

        let scrollCount = 0;
        let noNewImagesCount = 0;
        let lastImageCount = 0;
        let lastScrollHeight = 0;

        // 初始收集
        getAllImages();
        let currentCount = imageLinksSet.size;
        showNotification(`已找到 ${currentCount} 张图片`, 'info');
        addToLog(`已找到 ${currentCount} 张图片`, false);

        // 修正抓取逻辑 - 在滚动前检查数量
        while (scrollCount < config.IMAGE_MAX_SCROLL_COUNT &&
               !cancelDownload &&
               noNewImagesCount < config.NO_NEW_IMAGE_THRESHOLD) {

            // 在每次滚动前检查是否达到限制
            if (imageLinksSet.size >= config.BATCH_SIZE) {
                showNotification(`已达到收集数量限制: ${config.BATCH_SIZE},停止滚动`, 'warning');
                addToLog(`已达到收集数量限制: ${config.BATCH_SIZE},停止滚动`, false);
                break;
            }

            if (isPaused) {
                await new Promise(resolve => setTimeout(resolve, 500));
                continue;
            }

            // 渐进式滚动
            const currentScrollY = window.scrollY;
            const viewportHeight = window.innerHeight;
            const targetScrollY = currentScrollY + CONFIG.SCROLL_STEP;

            window.scrollTo(0, targetScrollY);
            await new Promise(resolve => setTimeout(resolve, config.IMAGE_SCROLL_INTERVAL));

            // 检查是否到达页面底部
            const currentScrollHeight = document.documentElement.scrollHeight;
            if (currentScrollHeight === lastScrollHeight &&
                targetScrollY + viewportHeight >= currentScrollHeight - 100) {
                noNewImagesCount++;
            } else {
                noNewImagesCount = 0;
                lastScrollHeight = currentScrollHeight;
            }

            getAllImages();
            currentCount = imageLinksSet.size;

            if (currentCount === lastImageCount) {
                noNewImagesCount++;
            } else {
                noNewImagesCount = 0;
                lastImageCount = currentCount;
            }

            scrollCount++;
            showNotification(`已收集 ${currentCount} 张图片 (滚动 ${scrollCount} 次)`, 'info');
        }

        // 修复:确保在/media页面进行多图检测
        const isMediaPage = window.location.pathname.includes('/media');
        debugLog(`当前页面: ${window.location.pathname}, 是否媒体页: ${isMediaPage}`);

        if (isMediaPage) {
            showNotification('开始检测多图推文...', 'info');
            addToLog('开始检测多图推文...', false);
            await detectMultiImageTweetsAfterScraping();

            // 添加调试信息
            const multiImageCount = multiImageTweetUrls.size;
            debugLog(`多图检测完成,检测到 ${multiImageCount} 个多图推文`);
            addToLog(`多图检测完成,检测到 ${multiImageCount} 个多图推文,已去重,重新抓取请刷新页面`, false);
        } else {
            debugLog('非媒体页面,跳过多图检测');
        }

        isCollecting = false;

        if (cancelDownload) {
            showNotification('收集已取消', 'warning');
            addToLog('收集已取消', false);
            updateButtonState('idle');
        } else {
            // 显示实际抓取数量,无论限制是多少
            const finalCount = imageLinksSet.size;
            showNotification(`收集完成! 共找到 ${finalCount} 张图片`, 'success');
            addToLog(`收集完成! 共找到 ${finalCount} 张图片`, false);
            updateButtonState('completed');
        }
    }


    async function downloadCollectedImages() {
        if (imageLinksSet.size === 0) {
            showNotification('没有可下载的图片', 'warning');
            addToLog('没有可下载的图片', false);
            return;
        }

        const imageList = Array.from(imageLinksSet);
        showNotification(`开始下载 ${imageList.length} 张图片...`, 'info');
        addToLog(`开始下载 ${imageList.length} 张图片...`, false);
        updateButtonState('downloading');

        cancelDownload = false;
        await Downloader.add(imageList);
    }

    // -------------------------- UI组件初始化 --------------------------
    function createUI() {
        // 添加样式
        const styleSheet = document.createElement('style');
        styleSheet.textContent = styles;
        document.head.appendChild(styleSheet);

        // 悬浮球
        const floatingBtn = document.createElement('div');
        floatingBtn.className = 'x-downloader-floating-btn';
        floatingBtn.innerHTML = '📷';
        floatingBtn.title = 'X图片下载器';
        document.body.appendChild(floatingBtn);

        // 通知消息
        const notification = document.createElement('div');
        notification.className = 'x-downloader-notification';
        document.body.appendChild(notification);

        // 遮罩层
        const overlay = document.createElement('div');
        overlay.className = 'x-downloader-overlay';
        document.body.appendChild(overlay);

        // 获取当前显示名称作为默认文件夹名
        const displayName = getDisplayName();

        // 主UI容器
        const uiContainer = document.createElement('div');
        uiContainer.className = 'x-downloader-ui';
        uiContainer.innerHTML = `
        <div class="x-downloader-section">
            <div class="x-downloader-section-title">📁 压缩包设置</div>
            <div class="x-downloader-input-group">
                <label class="x-downloader-label">压缩包名称</label>
                <input type="text" class="folder-input" id="folderName" value="${displayName}">
            </div>
        </div>

        <div class="x-downloader-section">
            <div class="x-downloader-section-title">📝 文件命名规则</div>
            <div class="naming-pattern-container">
                <div class="pattern-tags" id="patternTags"></div>
            </div>
        </div>

        <div class="x-downloader-section">
            <div class="x-downloader-section-title">📅 时间筛选</div>
            <div class="date-row">
                <div class="date-item">
                    <label class="x-downloader-label">开始日期 (YYYYMMDD)</label>
                    <input type="text" class="x-downloader-input" id="startDate" placeholder="${CONFIG.startDate}" pattern="\\d{8}">
                </div>
                <div class="date-item">
                    <label class="x-downloader-label">结束日期 (YYYYMMDD)</label>
                    <input type="text" class="x-downloader-input" id="endDate" placeholder="${getCurrentDate()}" pattern="\\d{8}">
                </div>
            </div>
        </div>

        <div class="x-downloader-section">
            <div class="x-downloader-section-title">⚙️ 抓取设置</div>
            <div class="settings-row">
                <div class="settings-item">
                    <label class="x-downloader-label">最大抓取数量</label>
                    <input type="text" class="x-downloader-input" id="batchSize" value="1000" pattern="\\d*">
                </div>
                <div class="settings-item">
                    <label class="x-downloader-label">滚动延迟(ms)</label>
                    <input type="text" class="x-downloader-input" id="scrollInterval" value="1500" pattern="\\d*">
                </div>
            </div>
            <div class="settings-row">
                <div class="settings-item">
                    <label class="x-downloader-label">下载延迟(ms)</label>
                    <input type="text" class="x-downloader-input" id="downloadDelay" value="100" pattern="\\d*">
                </div>
                <div class="settings-item">
                    <label class="x-downloader-label">并发下载数</label>
                    <input type="text" class="x-downloader-input" id="maxConcurrent" value="3" pattern="\\d*">
                </div>
            </div>
        </div>

        <!-- 进度条 -->
        <div class="progress-container" id="progressContainer">
            <div class="progress-info">
                <span id="progressText">准备开始</span>
                <span id="progressStats">0/0</span>
            </div>
            <div class="progress-bar">
                <div class="progress-fill" id="progressFill"></div>
            </div>
        </div>

        <div class="action-buttons">
            <button class="action-btn primary" id="startCollect">开始抓取</button>
            <button class="action-btn secondary" id="pauseCollect" disabled>暂停</button>
            <button class="action-btn warning" id="stopCollect" disabled>停止</button>
            <button class="action-btn success" id="startDownload" disabled>下载</button>
        </div>
    `;
        document.body.appendChild(uiContainer);

        // 创建LOG部分并添加到主UI
        const logSection = createLogSection();
        uiContainer.appendChild(logSection);

        // 初始化文件命名规则
        initNamingPattern();

        // 事件监听
        function addInputValidation() {
            const inputs = ['downloadDelay', 'maxConcurrent', 'batchSize', 'scrollInterval'];

            inputs.forEach(id => {
                const input = document.getElementById(id);
                if (input) {
                    // 移除旧的监听器,避免重复绑定
                    input.removeEventListener('input', validateNumber);
                    input.removeEventListener('blur', correctNumberOnBlur);

                    // input事件只用于实时验证和提示
                    input.addEventListener('input', validateNumber);

                    // blur事件用于修正值
                    input.addEventListener('blur', correctNumberOnBlur);
                }
            });
        }

        floatingBtn.addEventListener('click', toggleUI);
        overlay.addEventListener('click', closeUI);

        const startCollectBtn = document.getElementById('startCollect');
        const pauseCollectBtn = document.getElementById('pauseCollect');
        const stopCollectBtn = document.getElementById('stopCollect');
        const startDownloadBtn = document.getElementById('startDownload');

        startCollectBtn.addEventListener('click', startCollect);
        pauseCollectBtn.addEventListener('click', togglePauseCollect);
        stopCollectBtn.addEventListener('click', stopCollect);
        startDownloadBtn.addEventListener('click', startDownload);

        // 清空日志按钮事件
        document.getElementById('clearLog').addEventListener('click', clearLog);

        // 日期验证
        const startDateInput = document.getElementById('startDate');
        const endDateInput = document.getElementById('endDate');

        startDateInput.addEventListener('blur', validateDate);
        endDateInput.addEventListener('blur', validateDate);

        // 数字输入框验证
        const numberInputs = document.querySelectorAll('input[pattern="\\d*"]');
        numberInputs.forEach(input => {
            input.addEventListener('input', validateNumber);
        });

        setTimeout(() => {
            addInputValidation();
            // 初始化时验证所有输入框
            validateAllInputs();
        }, 100);
        // 加载保存的设置
        loadSettings();
    }

    // 添加验证所有输入框的函数
    function validateAllInputs() {
        const inputs = ['downloadDelay', 'maxConcurrent', 'batchSize', 'scrollInterval'];
        inputs.forEach(id => {
            const input = document.getElementById(id);
            if (input) {
                const event = new Event('input', { bubbles: true });
                input.dispatchEvent(event);
            }
        });
    }

    // 初始化文件命名规则
    function initNamingPattern() {
        const patternTags = document.getElementById('patternTags');
        if (!patternTags) return;

        const patternElements = [
            { id: 'displayName', name: '显示名称' },
            { id: 'username', name: '用户名' },
            { id: 'tweetId', name: '推文ID' },
            { id: 'postDate', name: '发布日期' },
            { id: 'time', name: '时间' },
        ];

        // 初始化所有标签
        patternElements.forEach(element => {
            const tag = document.createElement('div');
            tag.className = 'pattern-tag';
            tag.textContent = element.name;
            tag.dataset.id = element.id;
            tag.draggable = true;

            // 点击切换激活状态
            tag.addEventListener('click', () => {
                tag.classList.toggle('active');
                savePatternToConfig();
            });

            // 拖拽功能
            tag.addEventListener('dragstart', (e) => {
                e.dataTransfer.setData('text/plain', element.id);
                tag.classList.add('dragging');
            });

            tag.addEventListener('dragend', () => {
                tag.classList.remove('dragging');
            });

            tag.addEventListener('dragover', (e) => {
                e.preventDefault();
            });

            tag.addEventListener('drop', (e) => {
                e.preventDefault();
                const draggedId = e.dataTransfer.getData('text/plain');
                const target = e.target.closest('.pattern-tag');

                if (!target || target.dataset.id === draggedId) return;

                const draggedElement = document.querySelector(`.pattern-tag[data-id="${draggedId}"]`);
                const allTags = Array.from(document.querySelectorAll('.pattern-tag'));
                const targetIndex = allTags.indexOf(target);
                const draggedIndex = allTags.indexOf(draggedElement);

                if (draggedIndex > targetIndex) {
                    patternTags.insertBefore(draggedElement, target);
                } else {
                    patternTags.insertBefore(draggedElement, target.nextSibling);
                }

                savePatternToConfig();
            });

            patternTags.appendChild(tag);
        });

        // 加载保存的命名规则
        loadPatternFromConfig();
    }

    function loadPatternFromConfig() {
        const patternTags = document.getElementById('patternTags');
        const settings = JSON.parse(localStorage.getItem('xDownloaderSettings') || '{}');

        // 获取保存的排序和激活状态
        const savedOrder = settings.patternOrder || ['displayName', 'username', 'tweetId', 'postDate', 'time'];
        const savedActive = settings.fileNamePattern || ['displayName', 'postDate'];

        // 重新排列标签
        const allTags = Array.from(patternTags.children);
        const tagMap = {};
        allTags.forEach(tag => {
            tagMap[tag.dataset.id] = tag;
            tag.classList.remove('active');
        });

        // 清空容器
        patternTags.innerHTML = '';

        // 按保存的顺序添加标签
        savedOrder.forEach(id => {
            if (tagMap[id]) {
                patternTags.appendChild(tagMap[id]);
                // 激活已保存的标签
                if (savedActive.includes(id)) {
                    tagMap[id].classList.add('active');
                }
            }
        });
    }

    function savePatternToConfig() {
        const patternTags = document.getElementById('patternTags');
        const allTags = Array.from(patternTags.children);
        const patternOrder = allTags.map(tag => tag.dataset.id);
        const activeTags = patternTags.querySelectorAll('.pattern-tag.active');
        const fileNamePattern = Array.from(activeTags).map(tag => tag.dataset.id);

        // 获取当前设置
        const currentSettings = JSON.parse(localStorage.getItem('xDownloaderSettings') || '{}');

        // 更新设置
        currentSettings.patternOrder = patternOrder;
        currentSettings.fileNamePattern = fileNamePattern;

        localStorage.setItem('xDownloaderSettings', JSON.stringify(currentSettings));
    }

    function validateDate(e) {
        const input = e.target;
        const value = input.value.trim();
        const startDateInput = document.getElementById('startDate');
        const endDateInput = document.getElementById('endDate');
        const startDateValue = startDateInput.value.trim();
        const endDateValue = endDateInput.value.trim();

        // 验证日期格式
        if (value && !/^\d{8}$/.test(value)) {
            input.style.borderColor = '#f91880';
            showNotification('日期格式错误,请使用YYYYMMDD格式', 'error');
            return;
        }

        // 验证日期逻辑:结束日期不能早于开始日期
        if (startDateValue && endDateValue && startDateValue > endDateValue) {
            // 自动交换两个日期
            startDateInput.value = endDateValue;
            endDateInput.value = startDateValue;

            startDateInput.style.borderColor = '#f7931a';
            endDateInput.style.borderColor = '#f7931a';

            showNotification('开始日期晚于结束日期,已自动交换', 'warning');

            // 保存设置
            saveSettings();
        } else {
            // 清除错误样式
            input.style.borderColor = '#38444d';
            startDateInput.style.borderColor = '#38444d';
            endDateInput.style.borderColor = '#38444d';
        }
    }

    // 在日期输入框事件监听中添加验证
    function addDateValidation() {
        const startDateInput = document.getElementById('startDate');
        const endDateInput = document.getElementById('endDate');

        if (startDateInput && endDateInput) {
            // 移除旧的事件监听器,避免重复绑定
            startDateInput.removeEventListener('blur', validateDate);
            endDateInput.removeEventListener('blur', validateDate);

            // 添加新的事件监听器
            startDateInput.addEventListener('blur', validateDate);
            endDateInput.addEventListener('blur', validateDate);

            // 初始验证
            validateDate({ target: startDateInput });
        }
    }

    function validateNumber(e) {
        const input = e.target;
        const value = input.value.trim();
        const id = input.id;

        if (value && !/^\d+$/.test(value)) {
            input.style.borderColor = '#f91880';
            showNotification('请输入有效的数字', 'error');
            return;
        }

        const numValue = parseInt(value) || 0;

        // 只进行验证和提示,不立即修正值
        switch(id) {
            case 'downloadDelay':
                if (numValue > 0 && numValue < CONFIG.MIN_DOWNLOAD_DELAY) {
                    input.style.borderColor = '#f91880';
                    input.title = `最小延迟为${CONFIG.MIN_DOWNLOAD_DELAY}ms`;
                } else {
                    input.style.borderColor = '#38444d';
                    input.title = '';
                }
                break;

            case 'maxConcurrent':
                if (numValue > CONFIG.MAX_CONCURRENT_DOWNLOADS_LIMIT) {
                    input.style.borderColor = '#f91880';
                    input.title = `最大并发数为${CONFIG.MAX_CONCURRENT_DOWNLOADS_LIMIT}`;
                } else if (numValue < 1) {
                    input.style.borderColor = '#f91880';
                    input.title = '最小并发数为1';
                } else {
                    input.style.borderColor = '#38444d';
                    input.title = '';
                }
                break;

            case 'batchSize':
                if (numValue < 1) {
                    input.style.borderColor = '#f91880';
                    input.title = '最小抓取数量为1';
                } else {
                    input.style.borderColor = '#38444d';
                    input.title = '';
                }
                break;

            case 'scrollInterval':
                if (numValue < 100) {
                    input.style.borderColor = '#f91880';
                    input.title = '最小滚动间隔为100ms';
                } else {
                    input.style.borderColor = '#38444d';
                    input.title = '';
                }
                break;

            default:
                input.style.borderColor = '#38444d';
                input.title = '';
        }
    }

    function correctNumberOnBlur(e) {
        const input = e.target;
        const value = input.value.trim();
        const id = input.id;

        if (!value) return;

        const numValue = parseInt(value) || 0;
        let correctedValue = numValue;
        let showCorrectionNotification = false;

        // 验证并修正数值
        switch(id) {
            case 'downloadDelay':
                if (numValue < CONFIG.MIN_DOWNLOAD_DELAY) {
                    correctedValue = CONFIG.MIN_DOWNLOAD_DELAY;
                    showCorrectionNotification = true;
                }
                break;

            case 'maxConcurrent':
                if (numValue > CONFIG.MAX_CONCURRENT_DOWNLOADS_LIMIT) {
                    correctedValue = CONFIG.MAX_CONCURRENT_DOWNLOADS_LIMIT;
                    showCorrectionNotification = true;
                } else if (numValue < 1) {
                    correctedValue = 1;
                    showCorrectionNotification = true;
                }
                break;

            case 'batchSize':
                if (numValue < 1) {
                    correctedValue = 1;
                    showCorrectionNotification = true;
                }
                break;

            case 'scrollInterval':
                if (numValue < 100) {
                    correctedValue = 100;
                    showCorrectionNotification = true;
                }
                break;
        }

        // 如果值被修正,更新输入框并显示提示
        if (showCorrectionNotification && correctedValue !== numValue) {
            input.value = correctedValue;

            let message = '';
            switch(id) {
                case 'downloadDelay':
                    message = `下载延迟已自动调整为${correctedValue}ms`;
                    break;
                case 'maxConcurrent':
                    message = `并发下载数已自动调整为${correctedValue}`;
                    break;
                case 'batchSize':
                    message = `抓取数量已自动调整为${correctedValue}`;
                    break;
                case 'scrollInterval':
                    message = `滚动间隔已自动调整为${correctedValue}ms`;
                    break;
            }

            if (message) {
                showNotification(message, 'warning');
            }

            // 修正后保存设置
            saveSettings();
        }

        // 无论是否修正,都清除错误样式
        input.style.borderColor = '#38444d';
        input.title = '';
    }

    // -------------------------- UI相关函数 --------------------------

    function showNotification(message, type = 'info') {
        const notification = document.querySelector('.x-downloader-notification');
        if (!notification) return;

        notification.textContent = message;
        notification.className = 'x-downloader-notification';
        notification.classList.add(`notification-${type}`);
        notification.style.display = 'block';

        setTimeout(() => {
            notification.style.display = 'none';
        }, 3000);
    }

    function updateProgressBar(current, total, status = '') {
        const progressContainer = document.getElementById('progressContainer');
        const progressFill = document.getElementById('progressFill');
        const progressText = document.getElementById('progressText');
        const progressStats = document.getElementById('progressStats');

        if (!progressContainer || !progressFill) return;

        progressContainer.style.display = 'block';
        const percentage = total > 0 ? Math.round((current / total) * 100) : 0;
        progressFill.style.width = `${percentage}%`;
        progressStats.textContent = `${current}/${total}`;
        progressText.textContent = status || `${percentage}%`;

        // 完成后隐藏进度条
        if (current >= total) {
            setTimeout(() => {
                progressContainer.style.display = 'none';
            }, 2000);
        }
    }

    function protectDownloaderInputs() {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.type === 'attributes' && mutation.attributeName === 'placeholder') {
                    const target = mutation.target;
                    if (target.id === 'folderName') {
                        const displayName = getDisplayName();
                        if (target.placeholder !== displayName) {
                            setTimeout(() => {
                                target.placeholder = displayName;
                            }, 10);
                        }
                    }
                }
            });
        });

        const folderNameInput = document.getElementById('folderName');
        if (folderNameInput) {
            observer.observe(folderNameInput, {
                attributes: true,
                attributeFilter: ['placeholder']
            });
        }
    }

    function toggleUI() {
        const ui = document.querySelector('.x-downloader-ui');
        const overlay = document.querySelector('.x-downloader-overlay');

        if (isUIOpen) {
            closeUI();
        } else {
            // 每次打开UI时,重置文件夹名称为当前显示名称
            const currentDisplayName = getDisplayName();

            // 延迟设置以确保DOM已完全加载
            setTimeout(() => {
                const folderNameInput = document.getElementById('folderName');
                if (folderNameInput) {
                    folderNameInput.value = currentDisplayName;
                    folderNameInput.placeholder = currentDisplayName;
                }
            }, 100);

            ui.classList.add('open');
            overlay.style.display = 'block';
            isUIOpen = true;

            // 启动输入框保护
            setTimeout(protectDownloaderInputs, 200);
        }
    }

    function closeUI() {
        const ui = document.querySelector('.x-downloader-ui');
        const overlay = document.querySelector('.x-downloader-overlay');

        ui.classList.remove('open');
        overlay.style.display = 'none';
        isUIOpen = false;

        // 保存设置
        saveSettings();
    }

    // -------------------------- 设置管理 --------------------------
    function saveSettings() {
        const settings = {
            fileNamePattern: Array.from(document.querySelectorAll('.pattern-tag.active')).map(t => t.dataset.id),
            patternOrder: Array.from(document.querySelectorAll('.pattern-tag')).map(t => t.dataset.id),
            batchSize: parseInt(document.getElementById('batchSize').value) || 1000,
            scrollInterval: parseInt(document.getElementById('scrollInterval').value) || 1500,
            downloadDelay: Math.max(parseInt(document.getElementById('downloadDelay').value) || 100, 100),
            maxConcurrent: Math.min(parseInt(document.getElementById('maxConcurrent').value) || 3, 10),
            folderName: document.getElementById('folderName').value
        };
        localStorage.setItem('xDownloaderSettings', JSON.stringify(settings));
        return settings;
    }

    function loadSettings() {
        const s = JSON.parse(localStorage.getItem('xDownloaderSettings') || '{}');
        if (s.batchSize) document.getElementById('batchSize').value = s.batchSize;
        if (s.scrollInterval) document.getElementById('scrollInterval').value = s.scrollInterval;
        if (s.downloadDelay) document.getElementById('downloadDelay').value = s.downloadDelay;
        if (s.maxConcurrent) document.getElementById('maxConcurrent').value = s.maxConcurrent;
        if (s.folderName) document.getElementById('folderName').value = s.folderName;
        if (document.getElementById('patternTags')) loadPatternFromConfig();
    }

    // -------------------------- UI事件处理 --------------------------
    function updateButtonState(state) {
        const startBtn = document.getElementById('startCollect');
        const pauseBtn = document.getElementById('pauseCollect');
        const stopBtn = document.getElementById('stopCollect');
        const downloadBtn = document.getElementById('startDownload');

        if (!startBtn || !pauseBtn || !stopBtn || !downloadBtn) return;

        switch(state) {
            case 'idle':
                startBtn.disabled = false;
                pauseBtn.disabled = true;
                stopBtn.disabled = true;
                downloadBtn.disabled = imageLinksSet.size === 0;
                pauseBtn.textContent = '暂停';
                break;
            case 'collecting':
                startBtn.disabled = true;
                pauseBtn.disabled = false;
                stopBtn.disabled = false;
                downloadBtn.disabled = true;
                pauseBtn.textContent = '暂停';
                break;
            case 'paused':
                startBtn.disabled = true;
                pauseBtn.disabled = false;
                stopBtn.disabled = false;
                downloadBtn.disabled = true;
                pauseBtn.textContent = '继续';
                break;
            case 'completed':
                startBtn.disabled = false;
                pauseBtn.disabled = true;
                stopBtn.disabled = true;
                downloadBtn.disabled = false;
                pauseBtn.textContent = '暂停';
                break;
            case 'downloading':
                startBtn.disabled = true;
                pauseBtn.disabled = true;
                stopBtn.disabled = false;
                downloadBtn.disabled = true;
                break;
        }
    }

    async function startCollect() {
        await autoScrollAndCollectImages();
    }

    function togglePauseCollect() {
        isPaused = !isPaused;
        if (isPaused) {
            updateButtonState('paused');
            showNotification('收集已暂停', 'warning');
            addToLog('收集已暂停', false);
        } else {
            updateButtonState('collecting');
            showNotification('继续收集...', 'info');
            addToLog('继续收集...', false);
        }
    }

    function stopCollect() {
        isCollecting = false;
        isPaused = false;
        cancelDownload = true;
        Downloader.cancel();
        updateButtonState('idle');
        showNotification('收集已停止', 'warning');
        addToLog('收集已停止', false);
    }

    function startDownload() {
        downloadCollectedImages();
    }


    // 从容器获取推文ID
    function getTweetIdFromContainer(container) {
        // 多种方式获取推文ID
        const linkSelectors = [
            'a[href*="/status/"]',
            'a[data-testid*="tweet"]',
            'time[datetime]'
        ];

        for (const selector of linkSelectors) {
            const element = container.querySelector(selector);
            if (element) {
                const href = element.getAttribute('href') || '';
                const match = href.match(/\/status\/(\d+)/);
                if (match) return match[1];
            }
        }

        // 从时间戳获取
        const timeElement = container.querySelector('time');
        if (timeElement) {
            const datetime = timeElement.getAttribute('datetime');
            // 这里可以根据需要解析datetime
        }

        return null;
    }


    // 添加新的函数来处理非/media页面的下载按钮
    // 修正:移除 /media 屏蔽,允许在媒体页显示按钮(但需要配合CSS定位)
    function addDownloadButtonsToTweets() {
        // 原始代码屏蔽了/media页面: if (window.location.pathname.includes('/media')) return;
        // 现在允许执行,但要注意 /media 页面的布局是 Grid

        const images = Array.from(document.querySelectorAll('img[src*="pbs.twimg.com/media/"]'));

        // 1. 按推文ID分组图片
        const tweetGroups = new Map();

        images.forEach(img => {
            const info = getTweetInfoFromElement(img);
            if (!info) return;

            if (!tweetGroups.has(info.tweetId)) {
                tweetGroups.set(info.tweetId, []);
            }
            tweetGroups.get(info.tweetId).push(img);
        });

        // 2. 遍历每一组推文图片处理
        tweetGroups.forEach((imgList, tweetId) => {
            // 查找这一组图片的公共容器
            const targetContainer = findCommonContainer(imgList);
            if (!targetContainer) return;

            // 防止重复添加 (检查容器是否已有按钮)
            if (targetContainer.querySelector('.x-downloader-tweet-btns')) return;

            // 标记容器需要定位
            const computedStyle = window.getComputedStyle(targetContainer);
            if (computedStyle.position === 'static') {
                targetContainer.style.position = 'relative';
            }

            // 3. 创建按钮容器
            const btnContainer = document.createElement('div');
            btnContainer.className = 'x-downloader-tweet-btns';
            btnContainer.style.cssText = `
                position: absolute;
                top: 6px;
                right: 6px;
                z-index: 100;
                display: flex;
                gap: 4px;
                pointer-events: auto; /* 确保按钮可点击 */
            `;

            // 4. 根据数量添加按钮
            if (imgList.length === 1) {
                // 单图
                const downloadBtn = createDownloadButton('📥', '#1DA1F2', (e) => {
                    e.stopPropagation(); e.preventDefault();
                    downloadTweetImages(tweetId, targetContainer, false, false);
                });
                btnContainer.appendChild(downloadBtn);
            } else {
                // 多图
                const downloadAllBtn = createDownloadButton('📥全部', '#1DA1F2', (e) => {
                    e.stopPropagation(); e.preventDefault();
                    downloadTweetImages(tweetId, targetContainer, false, true);
                });
                const downloadExcludeFirstBtn = createDownloadButton('📥其他', '#f7931a', (e) => {
                    e.stopPropagation(); e.preventDefault();
                    downloadTweetImages(tweetId, targetContainer, true, true);
                });
                btnContainer.appendChild(downloadAllBtn);
                btnContainer.appendChild(downloadExcludeFirstBtn);
            }

            targetContainer.appendChild(btnContainer);
        });
    }

    // 创建下载按钮的辅助函数
    function createDownloadButton(text, color, onClick) {
        const btn = document.createElement('button');
        btn.className = 'x-downloader-tweet-btn';
        btn.textContent = text;
        btn.title = text === '📥' ? '下载原图' :
        text === '📥全部' ? '下载所有原图' :
        '下载排除首图';
        btn.style.cssText = `
            background: ${color};
            color: white;
            border: none;
            border-radius: 12px;
            padding: 4px 8px;
            font-size: 11px;
            cursor: pointer;
            opacity: 0.3;
            transition: opacity 0.2s;
            width: auto; /* 避免填满容器 */
            display: inline-block; /* 确保宽度自适应内容 */
        `;
        btn.onmouseenter = () => btn.style.opacity = '0.8';
        btn.onmouseleave = () => btn.style.opacity = '0.3';
        btn.onclick = onClick;
        return btn;
    }

    // 下载单个推文的图片
    async function downloadTweetImages(tweetId, container, excludeFirst = false, useDelay = false) {
        try {
            const downloadDelay = useDelay ?
                  (parseInt(document.getElementById('downloadDelay')?.value) || 100) : 0;

            // 重新获取容器内的所有符合该 ID 的图片
            // 注意:这里增加了过滤器,只下载属于当前 tweetId 的图片
            const allMedia = Array.from(container.querySelectorAll('img[src*="pbs.twimg.com/media/"]'));
            const tweetImages = [];

            // 用于提取作者信息的变量
            let accurateUsername = '';

            allMedia.forEach(img => {
                const info = getTweetInfoFromElement(img);
                // 严格匹配:只有 ID 匹配当前任务的图片才会被下载
                if (info && info.tweetId === tweetId) {
                    const formatMatch = img.src.match(/format=([a-zA-Z0-9]+)/);
                    const ext = formatMatch ? formatMatch[1] : 'jpg';
                    const url = img.src.split('?')[0] + `?format=${ext}&name=orig`;
                    tweetImages.push(url);

                    // 顺便获取精准的用户名 (只需获取一次)
                    if (!accurateUsername) accurateUsername = info.username;
                }
            });

            if (tweetImages.length === 0) {
                showNotification('未找到归属于此推文的图片', 'warning');
                return;
            }

            let imagesToDownload = tweetImages;
            if (excludeFirst && tweetImages.length > 1) {
                imagesToDownload = tweetImages.slice(1);
            }

            showNotification(`开始下载 ${imagesToDownload.length} 张图片...`, 'info');
            addToLog(`开始下载推文 ${tweetId} 的 ${imagesToDownload.length} 张图片`, false);

            if (imagesToDownload.length > 1) {
                updateProgressBar(0, imagesToDownload.length, '准备下载...');
            }

            const postDate = getDateFromTweetId(tweetId)?.toISOString() || 'unknown';

            // ========== 作者信息提取 ==========
            let authorDisplayName = '';

            // 1. 使用 URL 中提取的精准用户名
            const authorUsername = accurateUsername || getUsername();

            // 2. 尝试查找对应的显示名称
            // 我们在 container 及其父级中查找 User-Name 元素
            try {
                // 扩大查找范围到整个推文 article,以防 container 只是图片网格
                const article = container.closest('article') || container;
                const nameElements = article.querySelectorAll('[data-testid="User-Name"]');

                for (const el of nameElements) {
                    // 检查这个元素内部是否包含我们的精准用户名
                    if (el.textContent.toLowerCase().includes(`@${authorUsername.toLowerCase()}`)) {
                        // 找到了对应作者的标签,提取显示名称 (通常是第一部分)
                        const text = el.textContent;
                        // 简单切割逻辑:通常是 "Display Name @handle · time" 或类似
                        // 取 @ 之前的部分
                        const parts = text.split('@');
                        if (parts.length > 0) {
                            authorDisplayName = parts[0].trim();
                        }
                        break;
                    }
                }
            } catch (e) {
                console.warn('显示名称提取失败,将使用用户名代替');
            }

            // 【修复逻辑】
            // 在 /media 页面,图片 Grid 内部没有 User-Name 元素,导致上述查找失败。
            // 兜底策略:如果当前页面的 URL 属于该作者(说明我们在该作者的主页/媒体页),
            // 直接使用页面顶部的全局显示名称 (getDisplayName())
            if ((!authorDisplayName || authorDisplayName === authorUsername) &&
                authorUsername.toLowerCase() === getUsername().toLowerCase()) {
                const globalName = getDisplayName();
                // 确保获取到的不是默认值 'home' 或 'unknown_user'
                if (globalName && globalName !== 'home' && globalName !== 'unknown_user') {
                    authorDisplayName = globalName;
                }
            }

            // 最终兜底:如果没找到显示名称,就用用户名
            if (!authorDisplayName) authorDisplayName = authorUsername;

            for (let i = 0; i < imagesToDownload.length; i++) {
                const url = imagesToDownload[i];
                const index = excludeFirst ? i + 1 : i;

                const meta = {
                    tweetId,
                    postDate,
                    index: index,
                    authorUsername: authorUsername,
                    authorDisplayName: authorDisplayName
                };

                const filename = generateFileName(url, meta);

                try {
                    const blob = await new Promise((resolve, reject) => {
                        GM_xmlhttpRequest({
                            method: "GET",
                            url: url,
                            responseType: "blob",
                            onload: (response) => resolve(response.response),
                            onerror: reject
                        });
                    });

                    GM_download({
                        url: URL.createObjectURL(blob),
                        name: filename,
                        saveAs: false
                    });
                    addToLog(`下载成功: ${filename}`, false);

                    if (imagesToDownload.length > 1) {
                        updateProgressBar(i + 1, imagesToDownload.length, `下载中... (${i + 1}/${imagesToDownload.length})`);
                    }

                } catch (error) {
                    console.error(`下载失败: ${url}`, error);
                    addToLog(`下载失败: ${filename}`, false);
                }

                if (useDelay && i < imagesToDownload.length - 1) {
                    await new Promise(resolve => setTimeout(resolve, downloadDelay));
                }
            }

            if (imagesToDownload.length > 1) {
                setTimeout(() => {
                    const progressContainer = document.getElementById('progressContainer');
                    if (progressContainer) {
                        progressContainer.style.display = 'none';
                    }
                }, 1000);
            }

            showNotification(`推文图片下载完成!`, 'success');
            addToLog(`推文 ${tweetId} 图片下载完成`, false);
        } catch (error) {
            console.error('下载推文图片失败:', error);
            showNotification('下载失败', 'error');
        }
    }

    // -------------------------- 初始化 --------------------------
    (function init() {
        console.log('X图片下载器初始化...');

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', createUI);
        } else {
            createUI();
        }
        // 在UI创建完成后初始化日期验证
        setTimeout(() => {
            addDateValidation();
        }, 100);

        // 添加推文下载按钮
        setTimeout(() => {
            addDownloadButtonsToTweets();

            // 优化MutationObserver逻辑
            let processTimeout = null;

            const observer = new MutationObserver((mutations) => {
                // 检查是否有新的推文或图片被添加
                const hasNewContent = mutations.some(mutation => {
                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        return Array.from(mutation.addedNodes).some(node => {
                            if (node.nodeType === 1) {
                                // 只要有新的 article (推文) 或 img (图片) 出现,就触发检查
                                return node.matches('article') ||
                                    node.querySelector('article') ||
                                    node.matches('img') ||
                                    node.querySelector('img');
                            }
                            return false;
                        });
                    }
                    return false;
                });

                if (hasNewContent) {
                    if (processTimeout) clearTimeout(processTimeout);
                    // 防抖处理
                    processTimeout = setTimeout(() => {
                        addDownloadButtonsToTweets();
                        processTimeout = null;
                    }, 500);
                }
            });

            // 观察整个body的子节点变化和子树变化
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }, 2000);

        // 显示加载成功通知
        setTimeout(() => {
            showNotification('X图片下载器已加载', 'success');
        }, 1000);
    })();
})();