Greasy Fork

Greasy Fork is available in English.

X 博主综合操作脚本(ZIP打包版)

支持两种方式:① 普通网页扫描下载(ZIP打包) ② API下载图片/视频(ZIP打包+新版接口),带进度提示与取消功能

当前为 2025-08-27 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         X 博主综合操作脚本(ZIP打包版)
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  支持两种方式:① 普通网页扫描下载(ZIP打包) ② API下载图片/视频(ZIP打包+新版接口),带进度提示与取消功能
// @author       chatGPT + 整合自Twitter Media Downloader
// @match        https://x.com/*
// @match        https://twitter.com/*
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_download
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// ==/UserScript==

(function () {
    'use strict';

    // -------------------------- 基础配置与变量 --------------------------
    const BATCH_SIZE = 1000;
    const DOWNLOAD_PAUSE = 1000;
    const scrollInterval = 3000;
    const maxScrollCount = 10000;
    const IMAGE_SCROLL_INTERVAL = 1500;
    const IMAGE_MAX_SCROLL_COUNT = 100;

    let cancelDownload = false;
    const mediaSet = new Set();
    const imageSet = new Set();
    const statusIdSet = new Set();
    let hideTimeoutId = null;
    let lang, host, history, show_sensitive;
    const filenameTemplate = 'twitter_{user-name}(@{user-id})_{date-time}_{status-id}_{file-type}';

    // -------------------------- UI组件初始化 --------------------------
    // 进度框
    const progressBox = document.createElement('div');
    Object.assign(progressBox.style, {
        position: 'fixed',
        top: '20px',
        left: '20px',
        padding: '10px',
        backgroundColor: 'rgba(0,0,0,0.8)',
        color: '#fff',
        fontSize: '14px',
        zIndex: 9999,
        borderRadius: '8px',
        display: 'none'
    });
    document.body.appendChild(progressBox);

    // 加载提示框
    const loadingPrompt = document.createElement('div');
    Object.assign(loadingPrompt.style, {
        position: 'fixed',
        top: '50%',
        left: '50%',
        transform: 'translate(-50%, -50%)',
        padding: '20px',
        backgroundColor: 'rgba(0,0,0,0.8)',
        color: '#fff',
        fontSize: '16px',
        zIndex: 10000,
        borderRadius: '8px',
        display: 'none'
    });
    loadingPrompt.textContent = '正在加载,请不要关闭页面...';
    document.body.appendChild(loadingPrompt);

    // 进度条
    const progressBarContainer = document.createElement('div');
    Object.assign(progressBarContainer.style, {
        position: 'fixed',
        top: '55%',
        left: '50%',
        transform: 'translateX(-50%)',
        width: '300px',
        height: '20px',
        backgroundColor: '#ccc',
        zIndex: 10000,
        borderRadius: '10px',
        display: 'none'
    });
    const progressBar = document.createElement('div');
    Object.assign(progressBar.style, {
        width: '0%',
        height: '100%',
        backgroundColor: '#1DA1F2',
        borderRadius: '10px'
    });
    progressBarContainer.appendChild(progressBar);
    document.body.appendChild(progressBarContainer);

    // 下载状态通知器(来自指定脚本)
    const notifier = document.createElement('div');
    Object.assign(notifier.style, {
        display: 'none',
        position: 'fixed',
        left: '16px',
        bottom: '16px',
        color: '#000',
        background: '#fff',
        border: '1px solid #ccc',
        borderRadius: '8px',
        padding: '4px'
    });
    notifier.title = 'X Media Downloader';
    notifier.className = 'tmd-notifier';
    notifier.innerHTML = '<label>0</label>|<label>0</label>';
    document.body.appendChild(notifier);

    // -------------------------- 工具函数 --------------------------
    // 更新进度提示
    function updateProgress(txt) {
        progressBox.innerText = txt;
        progressBox.style.display = 'block';
    }

    // 获取用户名
    function getUsername() {
        const m = window.location.pathname.match(/^\/([^\/\?]+)/);
        return m ? m[1] : 'unknown_user';
    }

    // 初始化基础配置(来自指定脚本)
    async function initBaseConfig() {
        lang = getLanguage();
        host = location.hostname;
        history = await getDownloadHistory();
        show_sensitive = GM_getValue('show_sensitive', false);
        // 注入样式
        document.head.insertAdjacentHTML('beforeend', `<style>${getCSS()}${show_sensitive ? getSensitiveCSS() : ''}</style>`);
    }

    // -------------------------- 核心逻辑整合(来自指定脚本) --------------------------
    // 1. 语言配置
    function getLanguage() {
        const langMap = {
            en: {
                download: 'Download',
                completed: 'Download Completed',
                settings: 'Settings',
                dialog: {
                    title: 'Download Settings',
                    save: 'Save',
                    save_history: 'Remember download history',
                    clear_history: '(Clear)',
                    clear_confirm: 'Clear download history?',
                    show_sensitive: 'Always show sensitive content',
                    pattern: 'File Name Pattern'
                },
                enable_packaging: 'Package multiple files into a ZIP',
                packaging: 'Packaging ZIP...',
                mediaNotFound: 'MEDIA_NOT_FOUND',
                linkNotSupported: 'This tweet contains a link, which is not supported'
            },
            zh: {
                download: '下载',
                completed: '下载完成',
                settings: '设置',
                dialog: {
                    title: '下载设置',
                    save: '保存',
                    save_history: '保存下载记录',
                    clear_history: '(清除)',
                    clear_confirm: '确认要清除下载记录?',
                    show_sensitive: '自动显示敏感内容',
                    pattern: '文件名格式'
                },
                enable_packaging: '多文件打包成 ZIP',
                packaging: '正在打包ZIP...',
                mediaNotFound: '未找到媒体文件',
                linkNotSupported: '此推文包含链接,暂不支持下载'
            },
            'zh-Hant': {
                download: '下載',
                completed: '下載完成',
                settings: '設置',
                dialog: {
                    title: '下載設置',
                    save: '保存',
                    save_history: '保存下載記錄',
                    clear_history: '(清除)',
                    clear_confirm: '確認要清除下載記錄?',
                    show_sensitive: '自動顯示敏感內容',
                    pattern: '文件名規則'
                },
                enable_packaging: '多文件打包成 ZIP',
                packaging: '正在打包ZIP...',
                mediaNotFound: '未找到媒體文件',
                linkNotSupported: '此推文包含鏈接,暫不支持下載'
            }
        };
        const pageLang = document.querySelector('html').lang || navigator.language;
        return langMap[pageLang] || langMap[pageLang.split('-')[0]] || langMap.en;
    }

    // 2. CSS样式(来自指定脚本)
    function getCSS() {
        return `
            .tmd-notifier.running {display: flex; align-items: center;}
            .tmd-notifier label {display: inline-flex; align-items: center; margin: 0 8px;}
            .tmd-notifier label:before {content: " "; width: 32px; height: 16px; background-position: center; background-repeat: no-repeat;}
            .tmd-notifier label:nth-child(1):before {background-image:url("data:image/svg+xml;charset=utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2216%22 height=%2216%22 viewBox=%220 0 24 24%22><path d=%22M3,14 v5 q0,2 2,2 h14 q2,0 2,-2 v-5 M7,10 l4,4 q1,1 2,0 l4,-4 M12,3 v11%22 fill=%22none%22 stroke=%22%23666%22 stroke-width=%222%22 stroke-linecap=%22round%22 /></svg>");}
            .tmd-notifier label:nth-child(2):before {background-image:url("data:image/svg+xml;charset=utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2216%22 height=%2216%22 viewBox=%220 0 24 24%22><path d=%22M12,2 a1,1 0 0 1 0,20 a1,1 0 0 1 0,-20 M12,5 v7 h6%22 fill=%22none%22 stroke=%22%23999%22 stroke-width=%222%22 stroke-linejoin=%22round%22 stroke-linecap=%22round%22 /></svg>");}
            .tmd-notifier label:nth-child(3):before {background-image:url("data:image/svg+xml;charset=utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2216%22 height=%2216%22 viewBox=%220 0 24 24%22><path d=%22M12,0 a2,2 0 0 0 0,24 a2,2 0 0 0 0,-24%22 fill=%22%23f66%22 stroke=%22none%22 /><path d=%22M14.5,5 a1,1 0 0 0 -5,0 l0.5,9 a1,1 0 0 0 4,0 z M12,17 a2,2 0 0 0 0,5 a2,2 0 0 0 0,-5%22 fill=%22%23fff%22 stroke=%22none%22 /></svg>");}
            .tmd-btn {display: inline-block; background-color: #1DA1F2; color: #FFFFFF; padding: 0 15px; border-radius: 99px; cursor: pointer; margin-left: 10px;}
            .tmd-btn:hover {background-color: rgba(29, 161, 242, 0.9);}
            .tmd-tag {display: inline-block; background-color: #FFFFFF; color: #1DA1F2; padding: 0 10px; border-radius: 10px; border: 1px solid #1DA1F2; font-weight: bold; margin: 5px; cursor: pointer;}
            .tmd-tag:hover {background-color: rgba(29, 161, 242, 0.1);}
        `;
    }

    // 敏感内容显示CSS(来自指定脚本)
    function getSensitiveCSS() {
        return `
            li[role="listitem"]>div>div>div>div:not(:last-child) {filter: none;}
            li[role="listitem"]>div>div>div>div+div:last-child {display: none;}
        `;
    }

    // 3. 下载历史管理(来自指定脚本)
    async function getDownloadHistory() {
        let history = await GM_getValue('download_history', []);
        // 兼容旧版localStorage存储
        const oldHistory = JSON.parse(localStorage.getItem('history') || '[]');
        if (oldHistory.length > 0) {
            history = [...new Set([...history, ...oldHistory])];
            GM_setValue('download_history', history);
            localStorage.removeItem('history');
        }
        return history;
    }

    // 保存下载历史
    async function saveDownloadHistory(statusId) {
        const history = await getDownloadHistory();
        if (!history.includes(statusId)) {
            history.push(statusId);
            GM_setValue('download_history', history);
        }
    }

    // 4. 日期格式化(来自指定脚本)
    function formatDate(dateStr, format = 'YYYYMMDD-hhmmss', useLocal = false) {
        const d = new Date(dateStr);
        if (useLocal) d.setMinutes(d.getMinutes() - d.getTimezoneOffset());
        const months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'];
        const values = {
            YYYY: d.getUTCFullYear().toString(),
            YY: d.getUTCFullYear().toString().slice(-2),
            MM: (d.getUTCMonth() + 1).toString().padStart(2, '0'),
            MMM: months[d.getUTCMonth()],
            DD: d.getUTCDate().toString().padStart(2, '0'),
            hh: d.getUTCHours().toString().padStart(2, '0'),
            mm: d.getUTCMinutes().toString().padStart(2, '0'),
            ss: d.getUTCSeconds().toString().padStart(2, '0')
        };
        return format.replace(/(YYYY|YY|MM|MMM|DD|hh|mm|ss)/g, match => values[match]);
    }

    // 5. 文件名生成(来自指定脚本)
    function generateFileName(media, tweetData, index = 0) {
        const invalidChars = { '\\': '\', '/': '/', '|': '|', '<': '<', '>': '>', ':': ':', '*': '*', '?': '?', '"': '"', '\u200b': '', '\u200c': '', '\u200d': '', '\u2060': '', '\ufeff': '', '🔞': '' };
        const user = tweetData.core.user_results.result.legacy;
        const tweet = tweetData.legacy;

        // 基础信息
        const info = {
            'user-name': user.name.replace(/([\\/|*?:"\u200b-\u200d\u2060\ufeff]|🔞)/g, v => invalidChars[v]),
            'user-id': user.screen_name,
            'status-id': tweet.id_str,
            'date-time': formatDate(tweet.created_at),
            'date-time-local': formatDate(tweet.created_at, 'YYYYMMDD-hhmmss', true),
            'file-type': media.type.replace('animated_', ''),
            'file-name': media.media_url_https.split('/').pop().split(':')[0],
            'file-ext': media.type === 'photo' ? 'jpg' : 'mp4'
        };

        // 获取用户自定义模板
        const template = GM_getValue('filename', filenameTemplate);
        // 替换模板变量
        let fileName = template.replace(/\{([^{}:]+)\}/g, (_, key) => info[key] || key);
        // 多文件时添加索引
        if (index > 0) fileName += `_${index}`;
        // 补充后缀
        if (!fileName.endsWith(`.${info['file-ext']}`)) fileName += `.${info['file-ext']}`;

        return fileName;
    }

    // 6. 新版API请求(来自指定脚本,替换原有fetchJson)
    async function fetchTweetData(statusId) {
        const baseUrl = `https://${host}/i/api/graphql/2ICDjqPd81tulZcYrtpTuQ/TweetResultByRestId`;
        const variables = {
            'tweetId': statusId,
            'with_rux_injections': false,
            'includePromotedContent': true,
            'withCommunity': true,
            'withQuickPromoteEligibilityTweetFields': true,
            'withBirdwatchNotes': true,
            'withVoice': true,
            'withV2Timeline': true
        };
        const features = {
            'articles_preview_enabled': true,
            'c9s_tweet_anatomy_moderator_badge_enabled': true,
            'communities_web_enable_tweet_community_results_fetch': false,
            'creator_subscriptions_quote_tweet_preview_enabled': false,
            'creator_subscriptions_tweet_preview_api_enabled': false,
            'freedom_of_speech_not_reach_fetch_enabled': true,
            'graphql_is_translatable_rweb_tweet_is_translatable_enabled': true,
            'longform_notetweets_consumption_enabled': false,
            'longform_notetweets_inline_media_enabled': true,
            'longform_notetweets_rich_text_read_enabled': false,
            'premium_content_api_read_enabled': false,
            'profile_label_improvements_pcf_label_in_post_enabled': true,
            'responsive_web_edit_tweet_api_enabled': false,
            'responsive_web_enhance_cards_enabled': false,
            'responsive_web_graphql_exclude_directive_enabled': false,
            'responsive_web_graphql_skip_user_profile_image_extensions_enabled': false,
            'responsive_web_graphql_timeline_navigation_enabled': false,
            'responsive_web_media_download_video_enabled': false,
            'responsive_web_twitter_article_tweet_consumption_enabled': true,
            'standardized_nudges_misinfo': true,
            'tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled': true,
            'verified_phone_label_enabled': false,
            'view_counts_everywhere_api_enabled': true
        };

        // 构建请求URL
        const url = encodeURI(`${baseUrl}?variables=${JSON.stringify(variables)}&features=${JSON.stringify(features)}`);
        // 获取Cookie
        const cookies = getCookies();
        // 请求头
        const headers = {
            'authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
            'x-twitter-active-user': 'yes',
            'x-twitter-client-language': cookies.lang || 'en',
            'x-csrf-token': cookies.ct0 || ''
        };
        // Guest Token(必要时添加)
        if (cookies.ct0?.length === 32 && cookies.gt) headers['x-guest-token'] = cookies.gt;

        try {
            const response = await fetch(url, { headers });
            const data = await response.json();
            const tweetResult = data.data.tweetResult.result;
            return tweetResult.tweet || tweetResult;
        } catch (error) {
            console.error(`获取推文${statusId}数据失败:`, error);
            throw new Error(`获取推文数据失败:${error.message}`);
        }
    }

    // 获取Cookie(来自指定脚本)
    function getCookies(name) {
        const cookies = {};
        document.cookie.split(';')
            .filter(item => item.includes('='))
            .forEach(item => {
                const [key, value] = item.trim().split('=');
                cookies[key] = value;
            });
        return name ? cookies[name] : cookies;
    }

    // 7. ZIP打包下载器(整合指定脚本逻辑)
    const Downloader = (() => {
        let tasks = [], thread = 0, failed = 0, hasFailed = false;

        return {
            // 添加下载任务
            async add(tasksList, sourceBtn) {
                if (cancelDownload) return;

                // 获取用户设置:是否打包
                const enablePackaging = GM_getValue('enable_packaging', true);
                const saveHistory = GM_getValue('save_history', true);

                // 单文件直接下载,多文件打包
                if (tasksList.length === 1 && !enablePackaging) {
                    this.downloadSingle(tasksList[0], sourceBtn, saveHistory);
                } else {
                    this.downloadZip(tasksList, sourceBtn, saveHistory);
                }
            },

            // 单文件下载
            async downloadSingle(task, sourceBtn, saveHistory) {
                thread++;
                this.updateNotifier();
                updateProgress(`正在下载:${task.name}`);

                try {
                    await new Promise((resolve, reject) => {
                        GM_download({
                            url: task.url,
                            name: task.name,
                            onload: () => {
                                thread--;
                                failed--;
                                tasks = tasks.filter(t => t.url !== task.url);
                                this.updateNotifier();
                                updateProgress(`✅ 下载完成:${task.name}`);
                                if (saveHistory) saveDownloadHistory(task.statusId);
                                resolve();
                            },
                            onerror: (result) => {
                                thread--;
                                failed++;
                                tasks = tasks.filter(t => t.url !== task.url);
                                this.updateNotifier();
                                updateProgress(`❌ 下载失败:${task.name}(${result.details})`);
                                reject(new Error(result.details));
                            }
                        });
                    });
                } catch (error) {
                    console.error('单文件下载失败:', error);
                }
            },

            // 多文件ZIP打包下载
            async downloadZip(tasksList, sourceBtn, saveHistory) {
                if (cancelDownload) return;

                const zip = new JSZip();
                let completedCount = 0;
                const total = tasksList.length;
                updateProgress(`${lang.packaging}(0/${total})`);

                // 添加所有任务到队列
                tasks.push(...tasksList);
                this.updateNotifier();

                try {
                    // 并行下载文件并添加到ZIP
                    await Promise.all(tasksList.map(async (task, index) => {
                        thread++;
                        this.updateNotifier();

                        try {
                            const response = await fetch(task.url);
                            if (!response.ok) throw new Error(`HTTP错误:${response.status}`);

                            const blob = await response.blob();
                            zip.file(task.name, blob);

                            // 更新进度
                            completedCount++;
                            updateProgress(`${lang.packaging}(${completedCount}/${total})`);
                        } catch (error) {
                            failed++;
                            updateProgress(`❌ 文件${task.name}下载失败:${error.message}`);
                            console.error(`文件${task.name}处理失败:`, error);
                        } finally {
                            thread--;
                            tasks = tasks.filter(t => t.url !== task.url);
                            this.updateNotifier();
                        }
                    }));

                    // 生成ZIP并下载
                    if (cancelDownload) return;
                    updateProgress('正在生成ZIP文件...');
                    const zipBlob = await zip.generateAsync({
                        type: 'blob',
                        compression: 'STORE' // 媒体文件压缩无效,用存储模式提速
                    });

                    // 下载ZIP
                    const zipName = `${tasksList[0].name.split('_status-')[0]}_batch_${total}files.zip`;
                    const a = document.createElement('a');
                    a.href = URL.createObjectURL(zipBlob);
                    a.download = zipName;
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    URL.revokeObjectURL(a.href);

                    // 保存历史(去重)
                    if (saveHistory) {
                        const statusIds = [...new Set(tasksList.map(task => task.statusId))];
                        statusIds.forEach(id => saveDownloadHistory(id));
                    }

                    updateProgress(`✅ ZIP打包完成:${zipName}(共${total}个文件)`);
                } catch (error) {
                    updateProgress(`❌ ZIP打包失败:${error.message}`);
                    console.error('ZIP打包错误:', error);
                }
            },

            // 更新下载状态通知器
            updateNotifier() {
                if (failed > 0 && !hasFailed) {
                    hasFailed = true;
                    notifier.innerHTML += '|';
                    const clearBtn = document.createElement('label');
                    clearBtn.innerText = '清空失败';
                    clearBtn.style.color = '#f33';
                    clearBtn.onclick = () => {
                        failed = 0;
                        hasFailed = false;
                        notifier.innerHTML = '<label>0</label>|<label>0</label>';
                        this.updateNotifier();
                    };
                    notifier.appendChild(clearBtn);
                }

                // 更新数值
                notifier.children[0].innerText = thread; // 正在下载
                notifier.children[1].innerText = tasks.length - thread - failed; // 等待中
                if (failed > 0) notifier.children[2].innerText = failed; // 失败数

                // 显示/隐藏通知器
                if (thread > 0 || tasks.length > 0 || failed > 0) {
                    notifier.classList.add('running');
                } else {
                    notifier.classList.remove('running');
                }
            },

            // 取消所有任务
            cancel() {
                cancelDownload = true;
                tasks = [];
                thread = 0;
                failed = 0;
                hasFailed = false;
                this.updateNotifier();
                updateProgress('⏹️ 下载已取消');
            }
        };
    })();

    // -------------------------- 原有功能重构 --------------------------
    // 1. API下载:重构为新版逻辑(收集statusId → 获取媒体 → 打包下载)
    async function processTweets() {
        const statusIds = [];
        // 收集页面中的statusId(去重)
        document.querySelectorAll('a[href*="/status/"]').forEach(link => {
            const statusId = link.href.split('/status/').pop().split('/').shift();
            if (statusId && !statusIdSet.has(statusId) && /^\d+$/.test(statusId)) {
                statusIds.push(statusId);
                statusIdSet.add(statusId);
            }
        });

        if (statusIds.length === 0) return [];
        updateProgress(`正在解析${statusIds.length}条推文的媒体...`);

        // 批量获取推文媒体信息
        const mediaTasks = [];
        for (const statusId of statusIds) {
            if (cancelDownload) break;

            try {
                const tweetData = await fetchTweetData(statusId);
                const tweet = tweetData.legacy;
                const medias = tweet.extended_entities?.media;

                // 跳过包含链接卡片的推文
                if (tweetData.card) {
                    console.warn(`推文${statusId}包含链接卡片,跳过`);
                    continue;
                }

                // 提取媒体URL
                if (Array.isArray(medias)) {
                    medias.forEach((media, index) => {
                        let mediaUrl;
                        if (media.type === 'photo') {
                            mediaUrl = `${media.media_url_https}:orig`; // 原图
                        } else if (media.type === 'video' || media.type === 'animated_gif') {
                            // 选最高码率MP4
                            const variants = media.video_info?.variants.filter(v => v.content_type === 'video/mp4');
                            if (variants && variants.length > 0) {
                                mediaUrl = variants.sort((a, b) => (b.bitrate || 0) - (a.bitrate || 0))[0].url;
                            }
                        }

                        if (mediaUrl && !mediaSet.has(mediaUrl)) {
                            mediaSet.add(mediaUrl);
                            // 生成任务:包含URL、文件名、所属statusId
                            mediaTasks.push({
                                url: mediaUrl.split('?')[0], // 去除参数
                                name: generateFileName(media, tweetData, index),
                                statusId: statusId
                            });
                        }
                    });
                }
            } catch (error) {
                console.error(`处理推文${statusId}失败:`, error);
                continue;
            }
        }

        // 更新进度条(按已处理statusId比例)
        const progress = Math.min(Math.floor((statusIdSet.size / BATCH_SIZE) * 100), 100);
        progressBar.style.width = `${progress}%`;
        return mediaTasks;
    }

    // API下载主流程
    async function autoScrollAndDownloadAPI() {
        cancelDownload = false;
        mediaSet.clear();
        statusIdSet.clear();
        Downloader.updateNotifier();

        // 初始化UI
        updateProgress('📦 正在收集推文...');
        loadingPrompt.style.display = 'block';
        progressBarContainer.style.display = 'block';
        progressBar.style.width = '0%';

        // 初始扫描
        let mediaTasks = await processTweets();
        updateProgress(`📷 已扫描${mediaSet.size}个媒体文件`);

        // 自动滚动加载更多
        let scrollCount = 0, lastHeight = 0, stableCount = 0;
        while (!cancelDownload && scrollCount < maxScrollCount && mediaSet.size < BATCH_SIZE && stableCount < 3) {
            // 滚动到底部
            window.scrollTo(0, document.body.scrollHeight);
            await new Promise(resolve => setTimeout(resolve, scrollInterval));

            // 重新扫描
            const newTasks = await processTweets();
            mediaTasks = [...mediaTasks, ...newTasks];

            // 检测是否滚动到底(高度不变)
            const currentHeight = document.body.scrollHeight;
            if (currentHeight === lastHeight) {
                stableCount++;
            } else {
                stableCount = 0;
                lastHeight = currentHeight;
            }

            scrollCount++;
            updateProgress(`📷 已扫描${mediaSet.size}个媒体文件(滚动${scrollCount}次)`);
        }

        // 关闭加载提示
        loadingPrompt.style.display = 'none';
        progressBarContainer.style.display = 'none';

        // 执行下载
        if (cancelDownload) {
            updateProgress('⏹️ API下载已取消');
            finishAndSave(apiStartBtn);
            return;
        }

        if (mediaTasks.length === 0) {
            updateProgress(`⚠️ 未找到可下载的媒体文件`);
            finishAndSave(apiStartBtn);
            return;
        }

        // 限制批量大小
        const finalTasks = mediaTasks.slice(0, BATCH_SIZE);
        updateProgress(`🚀 开始处理${finalTasks.length}个媒体文件`);
        await Downloader.add(finalTasks, apiStartBtn);

        // 完成后清理
        finishAndSave(apiStartBtn);
    }

    // 2. 普通图片下载:重构为ZIP打包
    async function autoScrollAndDownloadImages() {
        cancelDownload = false;
        imageSet.clear();
        const username = getUsername();

        // 初始化UI
        updateProgress('📸 正在收集图片...');
        progressBox.style.display = 'block';

        // 初始扫描
        getAllImages();
        updateProgress(`📦 已找到${imageSet.size}张图片`);

        // 自动滚动加载
        let scrollCount = 0, lastHeight = 0;
        while (scrollCount < IMAGE_MAX_SCROLL_COUNT && !cancelDownload) {
            window.scrollTo(0, document.body.scrollHeight);
            await new Promise(resolve => setTimeout(resolve, IMAGE_SCROLL_INTERVAL));

            getAllImages();
            const currentHeight = document.body.scrollHeight;
            updateProgress(`📦 已找到${imageSet.size}张图片(滚动${scrollCount+1}次)`);

            // 检测到底部
            if (currentHeight === lastHeight) break;
            lastHeight = currentHeight;
            scrollCount++;
        }

        // 取消处理
        if (cancelDownload) {
            updateProgress('⏹️ 普通下载已取消');
            finishAndSave(startBtn);
            return;
        }

        // 生成下载任务
        const imageList = Array.from(imageSet);
        if (imageList.length === 0) {
            updateProgress('⚠️ 未找到可下载的图片');
            finishAndSave(startBtn);
            return;
        }

        // 构建任务列表
        const tasks = imageList.map((url, index) => ({
            url: url,
            name: `${username}_img_${formatDate(new Date())}_${index+1}.jpg`,
            statusId: `img_batch_${Date.now()}` // 图片批量标记
        }));

        // 执行下载(打包)
        updateProgress(`🚀 开始下载${tasks.length}张图片`);
        await Downloader.add(tasks, startBtn);

        // 完成清理
        finishAndSave(startBtn);
    }

    // 收集普通图片URL(原有逻辑保留)
    function getAllImages() {
        document.querySelectorAll('img[src*="twimg.com/media"], img[src*="pbs.twimg.com/amplify_video_thumb"]').forEach(img => {
            // 替换为原图URL
            const url = img.src.replace(/&name=\w+/, '') + '&name=orig';
            imageSet.add(url);
        });
    }

    // 下载完成后清理
    function finishAndSave(btn) {
        btn.disabled = false;
        cancelBtn.style.display = 'none';
        // 5秒后隐藏进度框
        hideTimeoutId = setTimeout(() => {
            progressBox.style.display = 'none';
        }, 5000);
    }

    // -------------------------- 设置面板(来自指定脚本) --------------------------
    async function openSettings() {
        // 创建遮罩层
        const mask = document.createElement('div');
        Object.assign(mask.style, {
            position: 'fixed',
            left: 0,
            top: 0,
            width: '100%',
            height: '100%',
            backgroundColor: 'rgba(0,0,0,0.5)',
            zIndex: 10001,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center'
        });

        // 创建设置面板
        const panel = document.createElement('div');
        Object.assign(panel.style, {
            width: '600px',
            maxWidth: '90vw',
            backgroundColor: '#fff',
            borderRadius: '10px',
            padding: '20px',
            color: '#000'
        });

        // 标题栏
        const titleBar = document.createElement('div');
        titleBar.style.display = 'flex';
        titleBar.style.justifyContent = 'space-between';
        titleBar.style.alignItems = 'center';

        const title = document.createElement('h3');
        title.innerText = lang.dialog.title;
        title.style.margin = 0;

        const closeBtn = document.createElement('button');
        closeBtn.innerText = '×';
        closeBtn.style.border = 'none';
        closeBtn.style.background = 'none';
        closeBtn.style.fontSize = '20px';
        closeBtn.style.cursor = 'pointer';
        closeBtn.style.color = '#666';
        closeBtn.onclick = () => mask.remove();

        titleBar.appendChild(title);
        titleBar.appendChild(closeBtn);
        panel.appendChild(titleBar);

        // 配置项区域
        const configArea = document.createElement('div');
        configArea.style.marginTop = '15px';

        // 1. 保存下载记录
        const historyConfig = document.createElement('div');
        historyConfig.style.marginBottom = '15px';
        historyConfig.style.padding = '10px';
        historyConfig.style.border = '1px solid #eee';
        historyConfig.style.borderRadius = '5px';

        const historyLabel = document.createElement('label');
        historyLabel.style.display = 'flex';
        historyLabel.style.alignItems = 'center';

        const historyCheckbox = document.createElement('input');
        historyCheckbox.type = 'checkbox';
        historyCheckbox.checked = GM_getValue('save_history', true);
        historyCheckbox.style.marginRight = '10px';
        historyCheckbox.onchange = () => GM_setValue('save_history', historyCheckbox.checked);

        historyLabel.innerText = lang.dialog.save_history;
        historyLabel.insertBefore(historyCheckbox, historyLabel.firstChild);

        // 清除历史按钮
        const clearBtn = document.createElement('span');
        clearBtn.innerText = lang.dialog.clear_history;
        clearBtn.style.color = '#1DA1F2';
        clearBtn.style.marginLeft = '15px';
        clearBtn.style.cursor = 'pointer';
        clearBtn.onclick = async () => {
            if (confirm(lang.dialog.clear_confirm)) {
                GM_setValue('download_history', []);
                history = [];
                alert(lang.dialog.clear_history + '成功');
            }
        };

        historyLabel.appendChild(clearBtn);
        historyConfig.appendChild(historyLabel);
        configArea.appendChild(historyConfig);

        // 2. 显示敏感内容
        const sensitiveConfig = document.createElement('div');
        sensitiveConfig.style.marginBottom = '15px';
        sensitiveConfig.style.padding = '10px';
        sensitiveConfig.style.border = '1px solid #eee';
        sensitiveConfig.style.borderRadius = '5px';

        const sensitiveLabel = document.createElement('label');
        sensitiveLabel.style.display = 'flex';
        sensitiveLabel.style.alignItems = 'center';

        const sensitiveCheckbox = document.createElement('input');
        sensitiveCheckbox.type = 'checkbox';
        sensitiveCheckbox.checked = GM_getValue('show_sensitive', false);
        sensitiveCheckbox.style.marginRight = '10px';
        sensitiveCheckbox.onchange = () => {
            GM_setValue('show_sensitive', sensitiveCheckbox.checked);
            show_sensitive = sensitiveCheckbox.checked;
            // 重新注入样式
            document.head.insertAdjacentHTML('beforeend', `<style>${show_sensitive ? getSensitiveCSS() : ''}</style>`);
        };

        sensitiveLabel.innerText = lang.dialog.show_sensitive;
        sensitiveLabel.insertBefore(sensitiveCheckbox, sensitiveLabel.firstChild);
        sensitiveConfig.appendChild(sensitiveLabel);
        configArea.appendChild(sensitiveConfig);

        // 3. 启用ZIP打包
        const zipConfig = document.createElement('div');
        zipConfig.style.marginBottom = '15px';
        zipConfig.style.padding = '10px';
        zipConfig.style.border = '1px solid #eee';
        zipConfig.style.borderRadius = '5px';

        const zipLabel = document.createElement('label');
        zipLabel.style.display = 'flex';
        zipLabel.style.alignItems = 'center';

        const zipCheckbox = document.createElement('input');
        zipCheckbox.type = 'checkbox';
        zipCheckbox.checked = GM_getValue('enable_packaging', true);
        zipCheckbox.style.marginRight = '10px';
        zipCheckbox.onchange = () => GM_setValue('enable_packaging', zipCheckbox.checked);

        zipLabel.innerText = lang.enable_packaging;
        zipLabel.insertBefore(zipCheckbox, zipLabel.firstChild);
        zipConfig.appendChild(zipLabel);
        configArea.appendChild(zipConfig);

        // 4. 文件名格式
        const filenameConfig = document.createElement('div');
        filenameConfig.style.marginBottom = '15px';
        filenameConfig.style.padding = '10px';
        filenameConfig.style.border = '1px solid #eee';
        filenameConfig.style.borderRadius = '5px';

        const filenameTitle = document.createElement('div');
        filenameTitle.innerText = lang.dialog.pattern;
        filenameTitle.style.marginBottom = '10px';
        filenameTitle.style.fontWeight = 'bold';

        const filenameInput = document.createElement('textarea');
        filenameInput.value = GM_getValue('filename', filenameTemplate);
        filenameInput.style.width = '100%';
        filenameInput.style.minHeight = '80px';
        filenameInput.style.padding = '8px';
        filenameInput.style.boxSizing = 'border-box';
        filenameInput.style.border = '1px solid #ccc';
        filenameInput.style.borderRadius = '5px';
        filenameInput.onchange = () => GM_setValue('filename', filenameInput.value);

        // 变量提示
        const varTips = document.createElement('div');
        varTips.style.marginTop = '10px';
        varTips.style.display = 'flex';
        varTips.style.flexWrap = 'wrap';
        varTips.style.gap = '8px';

        const varList = [
            { key: '{user-name}', tip: '用户名' },
            { key: '{user-id}', tip: '@后的用户名' },
            { key: '{status-id}', tip: '推文ID' },
            { key: '{date-time}', tip: 'UTC发布时间' },
            { key: '{date-time-local}', tip: '本地时间' },
            { key: '{file-type}', tip: '媒体类型(photo/video)' },
            { key: '{file-name}', tip: '原始文件名' }
        ];

        varList.forEach(item => {
            const tag = document.createElement('span');
            tag.className = 'tmd-tag';
            tag.innerText = item.key;
            tag.title = item.tip;
            tag.onclick = () => {
                const start = filenameInput.selectionStart;
                const end = filenameInput.selectionEnd;
                filenameInput.value = filenameInput.value.substring(0, start) + item.key + filenameInput.value.substring(end);
                filenameInput.selectionStart = filenameInput.selectionEnd = start + item.key.length;
            };
            varTips.appendChild(tag);
        });

        filenameConfig.appendChild(filenameTitle);
        filenameConfig.appendChild(filenameInput);
        filenameConfig.appendChild(varTips);
        configArea.appendChild(filenameConfig);

        // 保存按钮
        const saveBtn = document.createElement('div');
        saveBtn.className = 'tmd-btn';
        saveBtn.innerText = lang.dialog.save;
        saveBtn.style.marginLeft = 'auto';
        saveBtn.style.display = 'block';
        saveBtn.onclick = () => mask.remove();

        configArea.appendChild(saveBtn);
        panel.appendChild(configArea);
        mask.appendChild(panel);
        document.body.appendChild(mask);
    }

    // -------------------------- 按钮初始化 --------------------------
    // 普通下载按钮
    const startBtn = document.createElement('button');
    startBtn.innerText = '普通图片下载(ZIP)';
    Object.assign(startBtn.style, {
        position: 'fixed',
        top: '20px',
        right: '20px',
        zIndex: 10000,
        padding: '12px 20px',
        backgroundColor: '#1DA1F2',
        color: '#fff',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer',
        fontSize: '14px',
        marginBottom: '10px'
    });
    startBtn.onclick = () => {
        clearTimeout(hideTimeoutId);
        startBtn.disabled = true;
        cancelBtn.style.display = 'block';
        autoScrollAndDownloadImages();
    };

    // API下载按钮
    const apiStartBtn = document.createElement('button');
    apiStartBtn.innerText = 'API下载(ZIP)';
    Object.assign(apiStartBtn.style, {
        position: 'fixed',
        top: '70px',
        right: '20px',
        zIndex: 10000,
        padding: '12px 20px',
        backgroundColor: '#1DA1F2',
        color: '#fff',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer',
        fontSize: '14px',
        marginBottom: '10px'
    });
    apiStartBtn.onclick = () => {
        clearTimeout(hideTimeoutId);
        apiStartBtn.disabled = true;
        cancelBtn.style.display = 'block';
        autoScrollAndDownloadAPI();
    };

    // 取消按钮
    const cancelBtn = document.createElement('button');
    cancelBtn.innerText = '❌ 取消';
    Object.assign(cancelBtn.style, {
        position: 'fixed',
        top: '120px',
        right: '20px',
        zIndex: 10000,
        padding: '12px 20px',
        backgroundColor: '#ff4d4f',
        color: '#fff',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer',
        display: 'none',
        fontSize: '14px'
    });
    cancelBtn.onclick = () => {
        cancelDownload = true;
        Downloader.cancel();
        cancelBtn.innerText = '⏳ 停止中...';
        // 启用原按钮
        startBtn.disabled = false;
        apiStartBtn.disabled = false;
    };

    // 设置按钮
    const settingBtn = document.createElement('button');
    settingBtn.innerText = '⚙️ 设置';
    Object.assign(settingBtn.style, {
        position: 'fixed',
        top: '170px',
        right: '20px',
        zIndex: 10000,
        padding: '12px 20px',
        backgroundColor: '#666',
        color: '#fff',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer',
        fontSize: '14px'
    });
    settingBtn.onclick = openSettings;

    // 添加按钮到页面
    document.body.appendChild(startBtn);
    //document.body.appendChild(apiStartBtn);
    document.body.appendChild(cancelBtn);
    //document.body.appendChild(settingBtn);

    // -------------------------- 初始化 --------------------------
    (async () => {
        await initBaseConfig();
        updateProgress('准备就绪:点击按钮开始下载(支持ZIP打包)');
        // 3秒后自动隐藏初始提示
        hideTimeoutId = setTimeout(() => {
            progressBox.style.display = 'none';
        }, 3000);
    })();
})();