123FastLink

基于@Bao-qing (2025-01-29), @微風子 (1.3.1)版本的分支,补充额外功能并修复bug

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         123FastLink
// @namespace    http://tampermonkey.net/
// @version      1.3.1-mod-1.0.7
// @description  基于@Bao-qing (2025-01-29), @微風子 (1.3.1)版本的分支,补充额外功能并修复bug
// @author       Cursor
// @match        *://*.123pan.com/*
// @match        *://*.123pan.cn/*
// @match        *://*.123865.com/*
// @match        *://*.123684.com/*
// @match        *://*.123912.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=123pan.com
// @license      MIT
// @grant        GM_setClipboard
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function() {
    'use strict';

    // --- Constants and Configuration ---
    const SCRIPT_NAME = "123FastLink";
    const SCRIPT_VERSION = "1.3.1-mod-1.0.7";
    const debugMode = false; // Add debug mode flag
    const LEGACY_FOLDER_LINK_PREFIX_V1 = "123FSLinkV1$";
    const COMMON_PATH_LINK_PREFIX_V1 = "123FLCPV1$";
    const LEGACY_FOLDER_LINK_PREFIX_V2 = "123FSLinkV2$";
    const COMMON_PATH_LINK_PREFIX_V2 = "123FLCPV2$";
    const COMMON_PATH_DELIMITER = "%";
    const BASE62_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

    const API_PATHS = {
        UPLOAD_REQUEST: "/b/api/file/upload_request",
        LIST_NEW: "/b/api/file/list/new",
        FILE_INFO: "/b/api/file/info",
        SHARE_LIST: "/b/api/share/get",
        FOLDER_DETAIL: "/b/api/file/detail"
    };

    const DOM_SELECTORS = {
        TARGET_BUTTON_AREA: '.home-operator-button-group',
        FILE_ROW_SELECTOR: ".ant-table-row.ant-table-row-level-0.editable-row",
        FILE_CHECKBOX_SELECTOR: "input[type='checkbox']"
    };

    const RETRY_AND_DELAY_CONFIG = {
        RATE_LIMIT_ITEM_RETRY_DELAY_MS: 5000,
        RATE_LIMIT_MAX_ITEM_RETRIES: 2,
        RATE_LIMIT_GLOBAL_PAUSE_TRIGGER_FAILURES: 3,
        RATE_LIMIT_GLOBAL_PAUSE_DURATION_MS: 30000,
        GENERAL_API_RETRY_DELAY_MS: 3000,
        GENERAL_API_MAX_RETRIES: 20,
        PROACTIVE_DELAY_MS: 200
    };

    const FILTER_CONFIG = {
        STORAGE_KEY: 'fastlink_filter_settings',
        DEFAULT_FILTERS: [
            { ext: 'nfo', name: '电影信息文件', emoji: '📝', enabled: true },
            { ext: 'jpg', name: '图片文件', emoji: '🖼️', enabled: true },
            { ext: 'jpeg', name: '图片文件', emoji: '🖼️', enabled: false },
            { ext: 'png', name: '图片文件', emoji: '🖼️', enabled: true },
            { ext: 'gif', name: '动图文件', emoji: '🎞️', enabled: false },
            { ext: 'bmp', name: '图片文件', emoji: '🖼️', enabled: false },
            { ext: 'webp', name: '图片文件', emoji: '🖼️', enabled: false },
            { ext: 'tif', name: '图片文件', emoji: '🖼️', enabled: false },
            { ext: 'tiff', name: '图片文件', emoji: '🖼️', enabled: false },
            { ext: 'txt', name: '文本文件', emoji: '📄', enabled: false },
            { ext: 'srt', name: '字幕文件', emoji: '💬', enabled: false },
            { ext: 'ass', name: '字幕文件', emoji: '💬', enabled: false },
            { ext: 'ssa', name: '字幕文件', emoji: '💬', enabled: false },
            { ext: 'vtt', name: '字幕文件', emoji: '💬', enabled: false },
            { ext: 'sub', name: '字幕文件', emoji: '💬', enabled: false },
            { ext: 'idx', name: '字幕索引', emoji: '🔍', enabled: false },
            { ext: 'xml', name: 'XML文件', emoji: '🔧', enabled: false },
            { ext: 'html', name: '网页文件', emoji: '🌐', enabled: false },
            { ext: 'htm', name: '网页文件', emoji: '🌐', enabled: false },
            { ext: 'url', name: '网址链接', emoji: '🔗', enabled: false },
            { ext: 'lnk', name: '快捷方式', emoji: '🔗', enabled: false },
            { ext: 'pdf', name: 'PDF文档', emoji: '📑', enabled: false },
            { ext: 'doc', name: 'Word文档', emoji: '📘', enabled: false },
            { ext: 'docx', name: 'Word文档', emoji: '📘', enabled: false },
            { ext: 'xls', name: 'Excel表格', emoji: '📊', enabled: false },
            { ext: 'xlsx', name: 'Excel表格', emoji: '📊', enabled: false },
            { ext: 'ppt', name: 'PPT演示', emoji: '📽️', enabled: false },
            { ext: 'pptx', name: 'PPT演示', emoji: '📽️', enabled: false },
            { ext: 'md', name: 'Markdown文件', emoji: '📝', enabled: false },
            { ext: 'torrent', name: '种子文件', emoji: '🧲', enabled: false },
        ],
        DEFAULT_FILTER_OPTIONS: {
            filterOnShareEnabled: false,
            filterOnTransferEnabled: false,
        }
    };

    const API_ERROR_CODES = {
        STORAGE_SPACE_EXCEEDED: {
            code: 5055,
            message: "云盘空间超出限制",
            shouldNotRetry: true
        }
        // Add more error codes here as needed
    };

    // Add settings manager
    const settingsManager = {
        settings: {
            debugMode: false,
            useFolderNameForJson: true,
            appendDateToJson: true,
            usesBase62EtagsInExport: true,
            fastlinkButtonBg: '#fff',
            fastlinkButtonText: '#333',
            fastlinkButtonRandom: false
        },
        init: function() {
            // Load settings from storage
            const savedSettings = localStorage.getItem('123FastLink_settings');
            if (savedSettings) {
                try {
                    this.settings = JSON.parse(savedSettings);
                    if (!this.settings.fastlinkButtonBg) this.settings.fastlinkButtonBg = '#fff';
                    if (!this.settings.fastlinkButtonText) this.settings.fastlinkButtonText = '#333';
                    if (typeof this.settings.fastlinkButtonRandom !== 'boolean') this.settings.fastlinkButtonRandom = false;
                } catch (e) {
                    console.error(`[${SCRIPT_NAME}] Failed to load settings:`, e);
                }
            }
        },
        save: function() {
            localStorage.setItem('123FastLink_settings', JSON.stringify(this.settings));
        },
        get: function(key) {
            return this.settings[key];
        },
        set: function(key, value) {
            this.settings[key] = value;
            this.save();
        },
        isDebugMode: function() {
            return this.get('debugMode');
        }
    };

    const apiTestManager = {
        getApiPathOptions: function() {
            return Object.entries(API_PATHS).map(([key, path]) => ({
                value: path,
                label: `${key} (${path})`
            }));
        },
        getRequiredParams: function(apiPath) {
            // Define required parameters for each API path
            const paramMap = {
                [API_PATHS.FOLDER_DETAIL]: ['fileID'],
                [API_PATHS.FILE_INFO]: ['fileIdList'],
                [API_PATHS.LIST_NEW]: [
                    'driveId',
                    'limit',
                    'next',
                    'orderBy',
                    'orderDirection',
                    'trashed',
                    'SearchData',
                    'OnlyLookAbnormalFile',
                    'event',
                    'operateType',
                    'inDirectSpace',
                    'parentFileId',
                    'Page'
                ],
                [API_PATHS.SHARE_LIST]: ['shareKey'],
                [API_PATHS.UPLOAD]: ['fileName', 'parentFileId', 'size']
            };
            return paramMap[apiPath] || [];
        },
        getDefaultParams: function(apiPath) {
            const defaultParams = {
                [API_PATHS.LIST_NEW]: {
                    driveId: 0,
                    limit: 100,
                    next: 0,
                    orderBy: "file_name",
                    orderDirection: "asc",
                    trashed: false,
                    SearchData: "",
                    OnlyLookAbnormalFile: 0,
                    event: "homeListFile",
                    operateType: 4,
                    inDirectSpace: false,
                    parentFileId: 0,
                    Page: 1
                }
            };
            return defaultParams[apiPath] || {};
        },
        async testApi(apiPath, params, retryCount = null, method = 'POST') {
            try {
                // Create a function that returns a Promise for the API call
                const apiCall = async () => {
                    const response = await apiHelper.sendRequest(method, apiPath, params);
                    if (!response) {
                        throw new Error('服务器返回空响应');
                    }
                    if (response.code !== 0) {
                        throw new Error(`API错误: ${response.message || '未知错误'} (代码: ${response.code})`);
                    }
                    return response;
                };

                let response;
                if (retryCount === null || retryCount === 0) {
                    // No retry, just make the API call once
                    response = await apiCall();
                } else {
                    // Implement our own retry logic
                    let attempts = 0;
                    let lastError = null;

                    while (attempts <= retryCount) {
                        try {
                            response = await apiCall();
                            break; // Success, exit the retry loop
                        } catch (error) {
                            lastError = error;
                            attempts++;
                            if (attempts > retryCount) {
                                throw lastError; // Max retries reached, throw the last error
                            }
                            // Wait before retrying
                            await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.GENERAL_API_RETRY_DELAY_MS));
                        }
                    }
                }

                return {
                    success: true,
                    data: response
                };
            } catch (error) {
                console.error(`[${SCRIPT_NAME}] API测试失败:`, error);
                return {
                    success: false,
                    error: error.message || '未知错误'
                };
            }
        }
    };

    const filterManager = {
        filters: [],
        filterOnShareEnabled: false,
        filterOnTransferEnabled: false,

        init: function() { this.loadSettings(); },
        loadSettings: function() {
            try {
                const savedSettings = GM_getValue(FILTER_CONFIG.STORAGE_KEY);
                if (savedSettings) {
                    const parsedSettings = JSON.parse(savedSettings);
                    if (Array.isArray(parsedSettings.filters)) this.filters = parsedSettings.filters;
                    else { this.filters = parsedSettings; this.filterOnShareEnabled = FILTER_CONFIG.DEFAULT_FILTER_OPTIONS.filterOnShareEnabled; this.filterOnTransferEnabled = FILTER_CONFIG.DEFAULT_FILTER_OPTIONS.filterOnTransferEnabled; }
                    if (typeof parsedSettings.filterOnShareEnabled === 'boolean') this.filterOnShareEnabled = parsedSettings.filterOnShareEnabled;
                    if (typeof parsedSettings.filterOnTransferEnabled === 'boolean') this.filterOnTransferEnabled = parsedSettings.filterOnTransferEnabled;
                    console.log(`[${SCRIPT_NAME}] 已加载过滤器设置`);
                } else this.resetToDefaults();
            } catch (e) { console.error(`[${SCRIPT_NAME}] 加载过滤器设置失败:`, e); this.resetToDefaults(); }
        },
        saveSettings: function() {
            try {
                GM_setValue(FILTER_CONFIG.STORAGE_KEY, JSON.stringify({ filters: this.filters, filterOnShareEnabled: this.filterOnShareEnabled, filterOnTransferEnabled: this.filterOnTransferEnabled }));
                return true;
            } catch (e) { console.error(`[${SCRIPT_NAME}] 保存过滤器设置失败:`, e); return false; }
        },
        resetToDefaults: function() { this.filters = JSON.parse(JSON.stringify(FILTER_CONFIG.DEFAULT_FILTERS)); this.filterOnShareEnabled = FILTER_CONFIG.DEFAULT_FILTER_OPTIONS.filterOnShareEnabled; this.filterOnTransferEnabled = FILTER_CONFIG.DEFAULT_FILTER_OPTIONS.filterOnTransferEnabled; console.log(`[${SCRIPT_NAME}] 已重置为默认过滤器设置`); },
        shouldFilterFile: function(fileName, isShareOperation = true) {
            if ((isShareOperation && !this.filterOnShareEnabled) || (!isShareOperation && !this.filterOnTransferEnabled)) return false;
            if (!fileName) return false;
            const lastDotIndex = fileName.lastIndexOf('.');
            if (lastDotIndex === -1) return false;
            const extension = fileName.substring(lastDotIndex + 1).toLowerCase();
            const filter = this.filters.find(f => f.ext.toLowerCase() === extension);
            return filter && filter.enabled;
        },
        getFilteredCount: function() { return this.filters.filter(f => f.enabled).length; },
        setAllFilters: function(enabled) { this.filters.forEach(filter => filter.enabled = enabled); },

        buildFilterModalContent: function() {
            let html = `
                <div class="filter-global-switches">
                    <div class="filter-switch-item">
                        <input type="checkbox" id="fl-filter-share-toggle" class="filter-toggle-checkbox" ${this.filterOnShareEnabled ? 'checked' : ''}>
                        <label for="fl-filter-share-toggle"><span class="filter-emoji">🔗</span><span class="filter-name">生成分享链接时启用过滤</span></label>
                    </div>
                    <div class="filter-switch-item">
                        <input type="checkbox" id="fl-filter-transfer-toggle" class="filter-toggle-checkbox" ${this.filterOnTransferEnabled ? 'checked' : ''}>
                        <label for="fl-filter-transfer-toggle"><span class="filter-emoji">📥</span><span class="filter-name">转存链接/文件时启用过滤</span></label>
                    </div>
                </div>
                <hr class="filter-divider">
                <div class="filter-description">
                    <p>管理要过滤的文件类型。启用过滤后,相应类型的文件将不会包含在生成的链接或转存操作中。</p>
                </div>
                <div class="filter-select-style-container">
                    <div id="fl-selected-filter-tags" class="filter-selected-tags"></div>
                    <input type="text" id="fl-filter-search-input" class="filter-search-input" placeholder="输入扩展名 (如: jpg) 或名称添加/搜索...">
                    <div id="fl-filter-dropdown" class="filter-dropdown"></div>
                </div>
                <div class="filter-controls" style="margin-top: 15px;">
                    <button id="fl-filter-select-all" class="filter-btn">✅ 全选</button>
                    <button id="fl-filter-select-none" class="filter-btn">❌ 全不选</button>
                    <button id="fl-filter-reset" class="filter-btn">🔄 恢复默认</button>
                </div>`;
            return html;
        },
        renderFilterItems: function() {
            const modal = uiManager.getModalElement();
            if (!modal) return;
            const selectedTagsContainer = modal.querySelector('#fl-selected-filter-tags');
            const dropdown = modal.querySelector('#fl-filter-dropdown');
            const searchInput = modal.querySelector('#fl-filter-search-input');
            if (!selectedTagsContainer || !dropdown) return;

            selectedTagsContainer.innerHTML = '';
            dropdown.innerHTML = '';
            const searchTerm = searchInput ? searchInput.value.trim().toLowerCase() : "";

            this.filters.forEach((filter, index) => {
                if (filter.enabled) {
                    const tag = document.createElement('div');
                    tag.className = 'filter-tag';
                    tag.dataset.index = index;
                    tag.innerHTML = `<span class="filter-emoji">${filter.emoji}</span><span class="filter-tag-text">.${filter.ext}</span><span class="filter-tag-name">(${filter.name})</span><span class="filter-tag-remove">×</span>`;
                    tag.querySelector('.filter-tag-remove').addEventListener('click', () => { this.filters[index].enabled = false; this.renderFilterItems(); });
                    selectedTagsContainer.appendChild(tag);
                } else {
                    const filterText = `.${filter.ext} ${filter.name}`.toLowerCase();
                    if (searchTerm && !filter.ext.toLowerCase().includes(searchTerm) && !filterText.includes(searchTerm)) return;
                    const item = document.createElement('div');
                    item.className = 'filter-dropdown-item';
                    item.dataset.index = index;
                    item.innerHTML = `<span class="filter-emoji">${filter.emoji}</span><span class="filter-ext">.${filter.ext}</span><span class="filter-name">${filter.name}</span>`;
                    item.addEventListener('click', () => { this.filters[index].enabled = true; if (searchInput) searchInput.value = ''; this.renderFilterItems(); });
                    dropdown.appendChild(item);
                }
            });
            dropdown.style.display = dropdown.children.length > 0 && (document.activeElement === searchInput || dropdown.matches(':hover')) ? 'block' : 'none';
        },
        attachFilterEvents: function() {
            const modal = uiManager.getModalElement();
            if (!modal) return;
            this.renderFilterItems();
            const searchInput = modal.querySelector('#fl-filter-search-input');
            const dropdown = modal.querySelector('#fl-filter-dropdown');

            if (searchInput && dropdown) {
                searchInput.addEventListener('input', () => this.renderFilterItems());
                searchInput.addEventListener('focus', () => { if (dropdown.children.length > 0) dropdown.style.display = 'block'; });
                searchInput.addEventListener('blur', () => setTimeout(() => { if (!dropdown.matches(':hover')) dropdown.style.display = 'none'; }, 200));
                searchInput.addEventListener('keydown', (e) => {
                    if (e.key === 'Enter' && searchInput.value.trim() !== '') {
                        e.preventDefault();
                        const term = searchInput.value.trim().toLowerCase().replace(/^\\./, ''); // Remove leading dot
                        if (!term || !/^[a-z0-9_]+$/.test(term)) { // Basic validation for new extension
                            uiManager.showAlert("无效的扩展名格式。请只使用字母、数字和下划线。", 1500);
                            return;
                        }
                        const matchedIndex = this.filters.findIndex(f => f.ext.toLowerCase() === term);
                        if (matchedIndex !== -1) { // Existing filter
                            if (!this.filters[matchedIndex].enabled) {
                                this.filters[matchedIndex].enabled = true;
                                searchInput.value = '';
                                this.renderFilterItems();
                            } else {
                                uiManager.showAlert(`扩展名 ".${term}" 已经启用。`, 1500);
                            }
                        } else { // New filter
                            this.filters.push({ ext: term, name: '自定义类型', emoji: '✨', enabled: true });
                            // Sort filters by extension name to keep the list organized
                            this.filters.sort((a, b) => a.ext.localeCompare(b.ext));
                            searchInput.value = '';
                            this.renderFilterItems();
                            uiManager.showAlert(`已添加并启用自定义过滤器 ".${term}"。`, 1500);
                        }
                    }
                });
                dropdown.addEventListener('mouseenter', () => dropdown.dataset.hover = "true");
                dropdown.addEventListener('mouseleave', () => { delete dropdown.dataset.hover; if (document.activeElement !== searchInput) dropdown.style.display = 'none'; });
            }
            const shareToggle = modal.querySelector('#fl-filter-share-toggle');
            if (shareToggle) shareToggle.addEventListener('change', () => { this.filterOnShareEnabled = shareToggle.checked; });
            const transferToggle = modal.querySelector('#fl-filter-transfer-toggle');
            if (transferToggle) transferToggle.addEventListener('change', () => { this.filterOnTransferEnabled = transferToggle.checked; });
            modal.querySelector('#fl-filter-select-all')?.addEventListener('click', () => { this.setAllFilters(true); this.renderFilterItems(); });
            modal.querySelector('#fl-filter-select-none')?.addEventListener('click', () => { this.setAllFilters(false); this.renderFilterItems(); });
            modal.querySelector('#fl-filter-reset')?.addEventListener('click', () => {
                this.resetToDefaults();
                if (shareToggle) shareToggle.checked = this.filterOnShareEnabled;
                if (transferToggle) transferToggle.checked = this.filterOnTransferEnabled;
                this.renderFilterItems();
            });
        }
    };

    const apiHelper = {
        buildURL: (host, path, queryParams = {}) => { const queryString = new URLSearchParams(queryParams).toString(); return `${host}${path}${queryString ? '?' + queryString : ''}`; },
        sendRequest: async function(method, path, queryParams = {}, body = null, isPublicCall = false) {
            const config = { host: 'https://' + window.location.host, authToken: localStorage['authorToken'], loginUuid: localStorage['LoginUuid'], appVersion: '3', referer: document.location.href, };
            const headers = { 'Content-Type': 'application/json;charset=UTF-8', 'platform': 'web', 'App-Version': config.appVersion, 'Origin': config.host, 'Referer': config.referer, };
            if (!isPublicCall) { if (config.authToken) headers['Authorization'] = 'Bearer ' + config.authToken; if (config.loginUuid) headers['LoginUuid'] = config.loginUuid; }
            try {
                const urlToFetch = this.buildURL(config.host, path, queryParams);
                const response = await fetch(urlToFetch, { method, headers, body: body ? JSON.stringify(body) : null, credentials: 'include' });
                const responseText = await response.text();
                let responseData;
                try { responseData = JSON.parse(responseText); } catch (e) { if (!response.ok) throw new Error(`❗ HTTP ${response.status}: ${responseText || response.statusText}`); throw new Error(`❗ 响应解析JSON失败: ${e.message}`); }
                if (responseData.code !== 0) { const message = responseData.message || 'API业务逻辑错误'; const apiError = new Error(`❗ ${message}`); if (typeof message === 'string' && (message.includes("频繁") || message.includes("操作过快") || message.includes("rate limit") || message.includes("too many requests"))) apiError.isRateLimit = true; throw apiError; }
                return responseData;
            } catch (error) { if (!error.isRateLimit && !error.message?.startsWith("UserStopped")) { /* Log non-rate-limit, non-user-stopped errors */ } throw error; }
        },
        createFolder: async function(parentId, folderName) { return coreLogic._executeApiWithRetries(() => this._createFolderInternal(parentId, folderName), `创建文件夹: ${folderName}`, coreLogic.currentOperationRateLimitStatus); },
        _createFolderInternal: async function(parentId, folderName) { if (parentId === undefined || parentId === null || isNaN(parseInt(parentId))) { throw new Error(`创建文件夹 "${folderName}" 失败:父文件夹ID无效 (${parentId})。`); } const requestBody = { driveId: 0, etag: "", fileName: folderName, parentFileId: parseInt(parentId, 10), size: 0, type: 1, NotReuse: true, RequestSource: null, duplicate: 1, event: "newCreateFolder", operateType: 1 }; const responseData = await this.sendRequest("POST", API_PATHS.UPLOAD_REQUEST, {}, requestBody); if (responseData?.data?.Info?.FileId !== undefined) return responseData.data.Info; throw new Error('创建文件夹失败或API响应缺少FileId'); },
        listDirectoryContents: async function(parentId, limit = 100) { return coreLogic._executeApiWithRetries(() => this._listDirectoryContentsInternal(parentId, limit), `列出目录ID: ${parentId}`, coreLogic.currentOperationRateLimitStatus); },
        _listDirectoryContentsInternal: async function(parentId, limit = 100) {
            if (parentId === undefined || parentId === null || isNaN(parseInt(parentId))) {
                throw new Error(`无效的文件夹ID: ${parentId},无法列出内容。`);
            }
            let allItems = [];
            let nextMarker = "0";
            let currentPage = 1;
            let totalItems = 0;
            
            do {
                await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS));
                const queryParams = {
                    driveId: 0,
                    limit: limit,
                    next: 0,
                    orderBy: "file_name",
                    orderDirection: "asc",
                    trashed: false,
                    SearchData: "",
                    OnlyLookAbnormalFile: 0,
                    event: "homeListFile",
                    operateType: 4,
                    inDirectSpace: false,
                    parentFileId: parseInt(parentId, 10),
                    Page: currentPage
                };
                
                const responseData = await this.sendRequest("GET", API_PATHS.LIST_NEW, queryParams);
                if (responseData?.data?.InfoList) {
                    if (responseData.data.InfoList.length === 0) break;
                    const newItems = responseData.data.InfoList.map(item => ({
                        FileID: parseInt(item.FileId, 10) || NaN,
                        FileName: item.FileName || "Unknown",
                        Type: parseInt(item.Type, 10) || 0,
                        Size: parseInt(item.Size, 10) || 0,
                        Etag: item.Etag || "",
                        ParentFileID: parseInt(item.ParentFileId, 10)
                    }));
                    allItems = allItems.concat(newItems);
                    totalItems += responseData.data.InfoList.length;
                    nextMarker = responseData.data.Next;
                    currentPage++;
                } else {
                    if (currentPage === 1 && !responseData?.data?.InfoList && responseData.message && responseData.code !== 0) {
                        throw new Error(`API错误: ${responseData.message}`);
                    }
                    nextMarker = "-1";
                }
            } while (nextMarker !== "-1");
            
            console.log(`[${SCRIPT_NAME}] Retrieved ${totalItems} items (${allItems.length} unique) from directory ${parentId}`);
            return allItems;
        },
        getFileInfo: async function(idList) { return coreLogic._executeApiWithRetries(() => this._getFileInfoInternal(idList), `获取文件信息: ${idList.join(',')}`, coreLogic.currentOperationRateLimitStatus); },
        _getFileInfoInternal: async function(idList) { if (!idList || idList.length === 0) return { data: { infoList: [] } }; const requestBody = { fileIdList: idList.map(id => ({ fileId: String(id) })) }; const responseData = await this.sendRequest("POST", API_PATHS.FILE_INFO, {}, requestBody); if (responseData?.data?.infoList) { responseData.data.infoList = responseData.data.infoList.map(info => ({ ...info, FileID: parseInt(info.FileId || info.FileID, 10) || NaN, FileName: info.Name || info.FileName || "Unknown", Type: parseInt(info.Type || info.type, 10) || 0, Size: parseInt(info.Size || info.size, 10) || 0, Etag: info.Etag || info.etag || "" })); } return responseData; },
        rapidUpload: async function(etag, size, fileName, parentId) { return coreLogic._executeApiWithRetries(() => this._rapidUploadInternal(etag, size, fileName, parentId), `秒传: ${fileName}`, coreLogic.currentOperationRateLimitStatus); },
        _rapidUploadInternal: async function(etag, size, fileName, parentId) { if (parentId === undefined || parentId === null || isNaN(parseInt(parentId))) { throw new Error(`秒传文件 "${fileName}" 失败:父文件夹ID无效 (${parentId})。`); } const requestBody = { driveId: 0, etag: etag, fileName: fileName, parentFileId: parseInt(parentId, 10), size: parseInt(size, 10), type: 0, NotReuse: false, RequestSource: null, duplicate: 1, event: "rapidUpload", operateType: 1 }; const responseData = await this.sendRequest("POST", API_PATHS.UPLOAD_REQUEST, {}, requestBody); if (responseData?.data?.Info?.FileId !== undefined) return responseData.data.Info; throw new Error(responseData.message || '秒传文件失败或API响应异常'); },
        listSharedDirectoryContents: async function(parentId, shareKey, sharePwd, limit = 100) { return coreLogic._executeApiWithRetries( () => this._listSharedDirectoryContentsInternal(parentId, shareKey, sharePwd, limit), `列出分享目录ID: ${parentId} (ShareKey: ${shareKey.substring(0,4)}...)`, coreLogic.currentOperationRateLimitStatus, true ); },
        _listSharedDirectoryContentsInternal: async function(parentId, shareKey, sharePwd, limit = 100) {
            if (parentId === undefined || parentId === null || isNaN(parseInt(parentId))) throw new Error(`无效的分享文件夹ID: ${parentId},无法列出内容。`);
            if (!shareKey) throw new Error("ShareKey 不能为空。");
            let allItems = []; let nextMarker = "0"; let currentPage = 1;
            do {
                const queryParams = { limit: limit, next: 0, orderBy: "file_name", orderDirection: "asc", parentFileId: parseInt(parentId, 10), Page: currentPage, shareKey: shareKey, };
                if (sharePwd) queryParams.SharePwd = sharePwd;
                const responseData = await this.sendRequest("GET", API_PATHS.SHARE_LIST, queryParams, null, true);
                if (responseData?.data?.InfoList) {
                    if (responseData.data.InfoList.length === 0) break;
                    const newItems = responseData.data.InfoList.map(item => ({ FileID: parseInt(item.FileId, 10) || NaN, FileName: item.FileName || "Unknown", Type: parseInt(item.Type, 10) || 0, Size: parseInt(item.Size, 10) || 0, Etag: item.Etag || "", ParentFileID: parseInt(item.ParentFileId, 10) }));
                    allItems = allItems.concat(newItems); nextMarker = responseData.data.Next; currentPage++;
                } else { if (currentPage === 1 && !responseData?.data?.InfoList && responseData.message && responseData.code !== 0) throw new Error(`API错误: ${responseData.message}`); nextMarker = "-1"; }
            } while (nextMarker !== "-1");
            return allItems;
        },
        getFolderDetail: async function(folderId) { 
            return coreLogic._executeApiWithRetries(
                () => this._getFolderDetailInternal(folderId),
                `获取文件夹详情: ${folderId}`,
                coreLogic.currentOperationRateLimitStatus
            );
        },
        _getFolderDetailInternal: async function(folderId) {
            if (!folderId || isNaN(parseInt(folderId))) {
                throw new Error(`无效的文件夹ID: ${folderId}`);
            }
            const queryParams = { fileID: parseInt(folderId, 10) };
            const responseData = await this.sendRequest("GET", API_PATHS.FOLDER_DETAIL, queryParams);
            if (responseData?.code === 0 && responseData?.data) {
                return responseData.data;
            }
            throw new Error(responseData?.message || '获取文件夹详情失败');
        },
    };

    const processStateManager = { 
        _userRequestedStop: false,
        _isPaused: false,
        _modalStopButtonId: 'fl-modal-stop-btn',
        _modalPauseButtonId: 'fl-modal-pause-btn',
        // Keep track of last known progress to update mini bar instantly if needed
        _lastProgressData: { processed: 0, total: 0, successes: 0, failures: 0, currentFileName: "", extraStatus: "" }, 
        reset: function() { 
            this._userRequestedStop = false;
            this._isPaused = false;
            const btn = document.getElementById(this._modalStopButtonId); 
            if(btn){btn.textContent = "🛑 停止"; btn.disabled = false;}
            const pauseBtn = document.getElementById(this._modalPauseButtonId);
            if(pauseBtn){pauseBtn.textContent = "⏸️ 暂停"; pauseBtn.disabled = true;}
            // Reset mini progress title too
            if (uiManager.miniProgressElement) {
                const miniTitle = uiManager.miniProgressElement.querySelector('.fastlink-mini-progress-title span');
                if (miniTitle) miniTitle.textContent = "⚙️ 处理中...";
            }
        },
        requestStop: function() { 
            this._userRequestedStop = true; 
            const btn = document.getElementById(this._modalStopButtonId); 
            if(btn){btn.textContent = "正在停止..."; btn.disabled = true;}
            const pauseBtn = document.getElementById(this._modalPauseButtonId);
            if(pauseBtn){pauseBtn.disabled = true;}
            const minimizeBtn = document.getElementById('fl-m-minimize');
            if(minimizeBtn) minimizeBtn.disabled = true;
            console.log(`[${SCRIPT_NAME}] User requested stop.`);
            // Update mini progress title if active
            if (uiManager.isMiniProgressActive && uiManager.miniProgressElement) {
                const miniTitle = uiManager.miniProgressElement.querySelector('.fastlink-mini-progress-title span');
                if (miniTitle) miniTitle.textContent = "🛑 正在停止...";
            }
        },
        togglePause: function() {
            this._isPaused = !this._isPaused;
            const pauseBtn = document.getElementById(this._modalPauseButtonId);
            if(pauseBtn) {
                pauseBtn.textContent = this._isPaused ? "▶️ 继续" : "⏸️ 暂停";
            }
            // Update mini progress title if active
            if (uiManager.isMiniProgressActive && uiManager.miniProgressElement) {
                const miniTitle = uiManager.miniProgressElement.querySelector('.fastlink-mini-progress-title span');
                if (miniTitle) miniTitle.textContent = this._isPaused ? "⏸️ 已暂停" : "⚙️ 处理中...";
            }
            return this._isPaused;
        },
        isPaused: function() { return this._isPaused; },
        isStopRequested: function() { return this._userRequestedStop; },
        getStopButtonId: function() { return this._modalStopButtonId; },
        getPauseButtonId: function() { return this._modalPauseButtonId; },
        updateProgressUINow: function() { // Added to directly call update with last known data
            this.updateProgressUI(
                this._lastProgressData.processed,
                this._lastProgressData.total,
                this._lastProgressData.successes,
                this._lastProgressData.failures,
                this._lastProgressData.currentFileName,
                this._lastProgressData.extraStatus
            );
        },
        updateProgressUI: function(processed, total, successes, failures, currentFileName, extraStatus = "") { 
            // Store last data
            this._lastProgressData = { processed, total, successes, failures, currentFileName, extraStatus };

            const bar = document.querySelector('.fastlink-progress-bar'); 
            if (bar) bar.style.width = `${total > 0 ? Math.round((processed / total) * 100) : 0}%`; 
            const statTxt = document.querySelector('.fastlink-status p:first-child'); 
            if (statTxt) statTxt.textContent = `处理中: ${processed} / ${total} 项 (预估)`; 
            const sucCnt = document.querySelector('.fastlink-stats .success-count'); 
            if (sucCnt) sucCnt.textContent = `✅ 成功:${successes}`; 
            const failCnt = document.querySelector('.fastlink-stats .failed-count'); 
            if (failCnt) failCnt.textContent = `❌ 失败:${failures}`; 
            const curFile = document.querySelector('.fastlink-current-file .file-name'); 
            if (curFile) curFile.textContent = currentFileName ? `📄 ${currentFileName}` : "准备中..."; 
            const extraEl = document.querySelector('.fastlink-status .extra-status-message'); 
            if (extraEl) { extraEl.textContent = extraStatus; extraEl.style.display = extraStatus ? 'block' : 'none';}

            // Update mini progress bar if active
            if (uiManager.isMiniProgressActive && uiManager.miniProgressElement) {
                const miniBar = uiManager.miniProgressElement.querySelector('.fastlink-mini-progress-bar');
                if (miniBar) miniBar.style.width = `${total > 0 ? Math.round((processed / total) * 100) : 0}%`;

                const miniFile = uiManager.miniProgressElement.querySelector('.fastlink-mini-progress-file');
                if (miniFile) miniFile.textContent = currentFileName ? (currentFileName.length > 30 ? currentFileName.substring(0, 27) + "..." : currentFileName) : "准备中...";

                const miniStatus = uiManager.miniProgressElement.querySelector('.fastlink-mini-progress-status');
                if (miniStatus) miniStatus.textContent = `${processed}/${total} (✅${successes} ❌${failures})`;
                
                const miniTitle = uiManager.miniProgressElement.querySelector('.fastlink-mini-progress-title span');
                if (miniTitle) {
                    if (this._userRequestedStop) {
                        miniTitle.textContent = (processed < total) ? "🛑 正在停止..." : "🛑 已停止";
                    } else if (processed >= total && total > 0) {
                         miniTitle.textContent = "✅ 处理完成";
                    } else {
                        miniTitle.textContent = "⚙️ 处理中...";
                    }
                }
            }
        }, 
        appendInfoMessage: function(message) {
            const logArea = document.querySelector('.fastlink-info-messages');
            if (logArea) {
                // Remove oldest messages if we have more than 100
                while (logArea.children.length >= 100) {
                    logArea.removeChild(logArea.firstChild);
                }                
                const p = document.createElement('p');
                p.className = 'info-message';
                p.innerHTML = message;
                logArea.appendChild(p);
                logArea.scrollTop = logArea.scrollHeight;
            }
        },

        appendErrorMessage: function(message) {
            const isDebugMode = settingsManager.isDebugMode();
            const logArea = isDebugMode ? document.querySelector('.fastlink-error-messages') : document.querySelector('.fastlink-info-messages');
            if (logArea) {
                if (!isDebugMode) { // in debug mode, keep all messages
                    while (logArea.children.length >= 100) {
                        logArea.removeChild(logArea.firstChild);
                    }
                }
                const p = document.createElement('p');
                p.className =  'error-message';
                p.innerHTML = message;
                logArea.appendChild(p);
                logArea.scrollTop = logArea.scrollHeight;
             }
         },
    };

    const coreLogic = {
        currentOperationRateLimitStatus: { consecutiveRateLimitFailures: 0, totalRetries: { general: 0, rateLimit: 0 } },
        _executeApiWithRetries: async function(apiFunctionExecutor, itemNameForLog, rateLimitStatusRef, isPublicCallForSendRequest = false) {
            let generalErrorRetries = 0;
            while (generalErrorRetries <= RETRY_AND_DELAY_CONFIG.GENERAL_API_MAX_RETRIES) {
                if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                let rateLimitRetriesForCurrentGeneralAttempt = 0;
                while (rateLimitRetriesForCurrentGeneralAttempt <= RETRY_AND_DELAY_CONFIG.RATE_LIMIT_MAX_ITEM_RETRIES) {
                    if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                    try {
                        const result = await apiFunctionExecutor();
                        rateLimitStatusRef.consecutiveRateLimitFailures = 0;
                        if (generalErrorRetries > 0 || rateLimitRetriesForCurrentGeneralAttempt > 0) {
                            processStateManager.appendInfoMessage(`ℹ️ ${itemNameForLog}: Succeeded after ${generalErrorRetries} general retries and ${rateLimitRetriesForCurrentGeneralAttempt} rate limit retries`);
                            rateLimitStatusRef.totalRetries.general += generalErrorRetries;
                            rateLimitStatusRef.totalRetries.rateLimit += rateLimitRetriesForCurrentGeneralAttempt;
                        }
                        return result;
                    } catch (error) {
                        // Check if this is a known error code that should not be retried
                        const knownError = Object.values(API_ERROR_CODES).find(err => error.message.includes(err.message));
                        if (knownError && knownError.shouldNotRetry) {
                            processStateManager.appendErrorMessage(`❌ ${itemNameForLog}: ${error.message} (无需重试)`);
                            throw error;
                        }

                        if (processStateManager.isStopRequested()) throw error;
                        if (error.isRateLimit) {
                            rateLimitStatusRef.consecutiveRateLimitFailures++;
                            const rlRetryAttemptDisplay = rateLimitRetriesForCurrentGeneralAttempt + 1;
                            const currentFileEl = document.querySelector('.fastlink-current-file .file-name');
                            if(currentFileEl) processStateManager.appendErrorMessage(`⏳ ${currentFileEl.textContent || itemNameForLog}: 操作频繁 (RL ${rlRetryAttemptDisplay}/${RETRY_AND_DELAY_CONFIG.RATE_LIMIT_MAX_ITEM_RETRIES + 1})`);
                            if (rateLimitRetriesForCurrentGeneralAttempt >= RETRY_AND_DELAY_CONFIG.RATE_LIMIT_MAX_ITEM_RETRIES) { processStateManager.appendErrorMessage(`❌ ${itemNameForLog}: 已达当前常规尝试的最大API限流重试次数。`); throw error; }
                            rateLimitRetriesForCurrentGeneralAttempt++;
                            if (rateLimitStatusRef.consecutiveRateLimitFailures >= RETRY_AND_DELAY_CONFIG.RATE_LIMIT_GLOBAL_PAUSE_TRIGGER_FAILURES) {
                                processStateManager.appendErrorMessage(`[全局暂停] API持续频繁,暂停 ${RETRY_AND_DELAY_CONFIG.RATE_LIMIT_GLOBAL_PAUSE_DURATION_MS / 1000} 秒...`);
                                const extraStatusEl = document.querySelector('.fastlink-status .extra-status-message');
                                if(extraStatusEl) extraStatusEl.textContent = `全局暂停中... ${RETRY_AND_DELAY_CONFIG.RATE_LIMIT_GLOBAL_PAUSE_DURATION_MS / 1000}s`;
                                await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.RATE_LIMIT_GLOBAL_PAUSE_DURATION_MS));
                                if(extraStatusEl) extraStatusEl.textContent = "";
                                rateLimitStatusRef.consecutiveRateLimitFailures = 0; rateLimitRetriesForCurrentGeneralAttempt = 0;
                            } else { await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.RATE_LIMIT_ITEM_RETRY_DELAY_MS)); }
                        } else {
                            const genRetryAttemptDisplay = generalErrorRetries + 1;
                            processStateManager.appendErrorMessage(`❌ ${itemNameForLog}: ${error.message} (常规重试 ${genRetryAttemptDisplay}/${RETRY_AND_DELAY_CONFIG.GENERAL_API_MAX_RETRIES + 1})`);
                            generalErrorRetries++; if (generalErrorRetries > RETRY_AND_DELAY_CONFIG.GENERAL_API_MAX_RETRIES) throw error;
                            await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.GENERAL_API_RETRY_DELAY_MS)); break;
                        }
                    }
                }
            }
            rateLimitStatusRef.totalRetries.general += generalErrorRetries;
            rateLimitStatusRef.totalRetries.rateLimit += rateLimitRetriesForCurrentGeneralAttempt;
            throw new Error(`[${SCRIPT_NAME}] All API retries failed: ${itemNameForLog} (Total: ${rateLimitStatusRef.totalRetries.general} general, ${rateLimitStatusRef.totalRetries.rateLimit} rate limit retries)`);
        },
        getSelectedFileIds: () => Array.from(document.querySelectorAll(DOM_SELECTORS.FILE_ROW_SELECTOR)).filter(row => (row.querySelector(DOM_SELECTORS.FILE_CHECKBOX_SELECTOR) || {}).checked).map(row => String(row.getAttribute('data-row-key'))).filter(id => id != null),
        getCurrentDirectoryId: () => {
            const url = window.location.href;
            const homeFilePathMatch = url.match(/[?&]homeFilePath=([^&]*)/);
            if (homeFilePathMatch) { let filePathIds = homeFilePathMatch[1]; if (filePathIds && filePathIds !== "") { if (filePathIds.includes(',')) { const idsArray = filePathIds.split(','); return idsArray[idsArray.length - 1]; } else { return filePathIds; } } else { return "0"; } }
            const regexes = [ /fid=(\d+)/, /#\/list\/folder\/(\d+)/, /\/drive\/(?:folder\/)?(\d+)/, /\/s\/[a-zA-Z0-9_-]+\/(\d+)/, /(?:\/|^)(\d+)(?=[\/?#]|$)/ ];
            for (const regex of regexes) { const match = url.match(regex); if (match && match[1]) { if (match[1] === "0") { if (regex.source === String(/\/drive\/(?:folder\/)?(\d+)/) && url.includes("/drive/0")) return "0"; } return match[1]; } }
            const lowerUrl = url.toLowerCase(); if (lowerUrl.includes("/drive/0") || lowerUrl.endsWith("/drive") || lowerUrl.endsWith("/drive/") || lowerUrl.match(/^https?:\/\/[^\/]+\/?([#?].*)?$/) || lowerUrl.endsWith(".123pan.com") || lowerUrl.endsWith(".123pan.cn") || lowerUrl.endsWith(".123pan.com/") || lowerUrl.endsWith(".123pan.cn/")) return "0";
            try { const pathname = new URL(url).pathname; if (pathname === '/' || pathname.toLowerCase() === '/drive/' || pathname.toLowerCase() === '/index.html') return "0"; } catch(e) { /*ignore*/ }
            return "0";
        },
        _findLongestCommonPrefix: function(paths) {
            if (!paths || paths.length === 0) return ""; if (paths.length === 1 && paths[0].includes('/')) { const lastSlash = paths[0].lastIndexOf('/'); if (lastSlash > -1) return paths[0].substring(0, lastSlash + 1); return ""; } if (paths.length === 1 && !paths[0].includes('/')) return "";
            const sortedPaths = [...paths].sort(); const firstPath = sortedPaths[0]; const lastPath = sortedPaths[sortedPaths.length - 1]; let i = 0; while (i < firstPath.length && firstPath.charAt(i) === lastPath.charAt(i)) i++; let prefix = firstPath.substring(0, i);
            if (prefix.includes('/')) prefix = prefix.substring(0, prefix.lastIndexOf('/') + 1); else { if (!paths.every(p => p === prefix || p.startsWith(prefix + "/"))) return "";}
            return (prefix.length > 1 && prefix.endsWith('/')) ? prefix : "";
        },

        _generateLinkProcess: async function(itemFetcherAsyncFn, operationTitleForUI, isBatchMode = false) {
            processStateManager.reset();
            this.currentOperationRateLimitStatus.consecutiveRateLimitFailures = 0;
            let allFileEntriesData = [];
            let processedAnyFolder = false; // This will be set by the itemFetcher callback
            let totalDiscoveredItemsForProgress = 0;
            let itemsProcessedForProgress = 0;
            let successes = 0, failures = 0;
            let jsonDataForExport = null;
            const startTime = Date.now();

            uiManager.showModal(operationTitleForUI, `
                <div class="fastlink-progress-container"><div class="fastlink-progress-bar" style="width: 0%"></div></div>
                <div class="fastlink-status-container">
                    <div class="fastlink-info-status">
                        <p>🔍 正在分析项目...</p>
                        <div class="fastlink-info-messages"></div>
                    </div>
                    <div class="fastlink-error-status">
                        <div class="fastlink-error-messages"></div>
                    </div>
                    <p class="extra-status-message" style="color: #ff7f50; display: none;"></p>
                </div>
                <div class="fastlink-stats"><span class="success-count">✅ 成功:0</span><span class="failed-count">❌ 失败:0</span></div>
                <div class="fastlink-current-file"><p class="file-name">准备开始...</p></div>`, 'progress_stoppable', false);

            try {
                const result = await itemFetcherAsyncFn(
                    (itemData) => { allFileEntriesData.push(itemData); },
                    (isFolder) => { if(isFolder) processedAnyFolder = true; },
                    (progressUpdate) => {
                        if (progressUpdate.total !== undefined) totalDiscoveredItemsForProgress = progressUpdate.total;
                        if (progressUpdate.processed !== undefined) itemsProcessedForProgress = progressUpdate.processed;
                        if (progressUpdate.successCount !== undefined) successes = progressUpdate.successCount;
                        if (progressUpdate.failureCount !== undefined) failures = progressUpdate.failureCount;
                        processStateManager.updateProgressUI(itemsProcessedForProgress, totalDiscoveredItemsForProgress, successes, failures, progressUpdate.currentFile, progressUpdate.extraStatus);
                    }
                );
                // Ensure final counts are taken from the result of the fetcher
                totalDiscoveredItemsForProgress = result.totalDiscoveredItemsForProgress;
                itemsProcessedForProgress = result.itemsProcessedForProgress;
                successes = result.successes;
                failures = result.failures;

            } catch (e) {
                if (e.message === "UserStopped") processStateManager.appendInfoMessage("🛑 用户已停止操作。");
                else { processStateManager.appendErrorMessage(`SYSTEM ERROR: ${e.message}`); console.error("Error during generation:", e); }
            }

            processStateManager.updateProgressUI(itemsProcessedForProgress, totalDiscoveredItemsForProgress, successes, failures, "处理完成", "");

            const totalTime = Math.round((Date.now() - startTime) / 1000);
            console.log(`[${SCRIPT_NAME}] File links processing took ${totalTime}s for ${allFileEntriesData.length} entries`);

            if (allFileEntriesData.length > 0) {
                let link = "";
                const allPaths = allFileEntriesData.map(entry => entry.fullPath);
                const commonPrefix = this._findLongestCommonPrefix(allPaths);
                let useV2Format = settingsManager.get('usesBase62EtagsInExport');
                let profileStartTime = performance.now();
                const processedEntries = allFileEntriesData.map(entry => {
                    const etagConversion = hexToOptimizedEtag(entry.etag);
                    return { 
                        ...entry, 
                        processedEtag: useV2Format ? etagConversion.optimized : entry.etag 
                    };
                });
                let profileEndTime = performance.now();
                console.log(`[${SCRIPT_NAME}] File entry etag conversion took ${(profileEndTime - profileStartTime).toFixed(2)}ms for ${allFileEntriesData.length} entries`);

                if (commonPrefix && (processedAnyFolder || allPaths.some(p => p.includes('/')))) { const fileStrings = processedEntries.map(entry => `${useV2Format ? entry.processedEtag : entry.etag}#${entry.size}#${entry.fullPath.substring(commonPrefix.length)}`); link = (useV2Format ? COMMON_PATH_LINK_PREFIX_V2 : COMMON_PATH_LINK_PREFIX_V1) + commonPrefix + COMMON_PATH_DELIMITER + fileStrings.join('$');
                } else { const fileStrings = processedEntries.map(entry => `${useV2Format ? entry.processedEtag : entry.etag}#${entry.size}#${entry.fullPath}`); link = fileStrings.join('$'); if (processedAnyFolder || allPaths.some(p => p.includes('/'))) link = (useV2Format ? LEGACY_FOLDER_LINK_PREFIX_V2 : LEGACY_FOLDER_LINK_PREFIX_V1) + link; else if (useV2Format && !link.startsWith(LEGACY_FOLDER_LINK_PREFIX_V2) && !link.startsWith(COMMON_PATH_LINK_PREFIX_V2)) link = LEGACY_FOLDER_LINK_PREFIX_V2 + link; }

                const commonPathForExport = (commonPrefix && (processedAnyFolder || allPaths.some(p => p.includes('/')))) ? commonPrefix : "";
                const totalSize = allFileEntriesData.reduce((acc, entry) => acc + (Number(entry.size) || 0), 0);
                const formattedTotalSize = formatBytes(totalSize);
                profileStartTime = performance.now()
                jsonDataForExport = { 
                    scriptVersion: SCRIPT_VERSION, 
                    exportVersion: "1.0", 
                    usesBase62EtagsInExport: useV2Format, 
                    commonPath: commonPathForExport, 
                    totalFilesCount: allFileEntriesData.length, 
                    totalSize: totalSize,
                    formattedTotalSize: formattedTotalSize,
                    files: allFileEntriesData.map(entry => ({ 
                        path: commonPathForExport ? entry.fullPath.substring(commonPathForExport.length) : entry.fullPath, 
                        size: String(entry.size), 
                        etag: useV2Format ? hexToOptimizedEtag(entry.etag).optimized : entry.etag 
                    })) 
                };
                profileEndTime = performance.now();
                console.log(`[${SCRIPT_NAME}] Preparing json data for export took ${(profileEndTime - profileStartTime).toFixed(2)}ms for ${allFileEntriesData.length} entries`);

                if (isBatchMode) {
                    uiManager.hideModal();
                } else {
                    if (processStateManager.isStopRequested()) processStateManager.appendInfoMessage(`⚠️ 操作已停止。以下是已处理 ${allFileEntriesData.length} 项的部分链接/数据。`);
                    if (useV2Format) processStateManager.appendInfoMessage('💡 使用V2链接格式 (Base62 ETags) 生成。', true); else processStateManager.appendInfoMessage('ℹ️ 使用V1链接格式 (标准 ETags) 生成。', true);

                    let titleMessage = failures > 0 && successes > 0 ? "🎯 部分成功" : (successes > 0 ? "🎉 生成成功" : "🤔 无有效数据");
                    if (processStateManager.isStopRequested()) titleMessage = "🔴 操作已停止 (部分数据)";

                    let linkText = allFileEntriesData.length < 100 ? link : `成功生成了 ${allFileEntriesData.length} 条链接!\n${link.substring(0, 100)}${link.length > 100 ? '...' : ''}`;

                    // Get error messages if any
                    const errorMessagesDiv = document.querySelector('.fastlink-error-messages');
                    let errorMessagesHtml = '';
                    if (errorMessagesDiv && errorMessagesDiv.children.length > 0 && settingsManager.isDebugMode()) {
                        errorMessagesHtml = `<div class="fastlink-error-summary" style="margin-top: 15px; text-align: left; max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; font-size: 0.9em;">
                            <h4 style="margin: 0 0 10px 0;">⚠️ 错误信息:</h4>
                            ${Array.from(errorMessagesDiv.children).map(p => `<p style="margin: 3px 0; color: #d9534f;">${p.innerHTML}</p>`).join('')}
                        </div>`;
                    }

                    const summary = `<div class="fastlink-result"><h3>${titleMessage}</h3><p>📄 已处理项目 (用于链接/JSON): ${allFileEntriesData.length} 个</p><p>✅ 成功提取元数据: ${successes} 个</p><p>❌ 失败/跳过项目: ${failures} 个</p><p>💾 已处理项目总大小: ${formattedTotalSize}</p><p>⏱️ 耗时: ${totalTime} 秒</p><textarea class="fastlink-link-text" readonly>${linkText}</textarea></div>${errorMessagesHtml}`;
                    uiManager.showModal( processStateManager.isStopRequested() ? "🔴 秒传链接 (部分生成)" : "🎉 秒传链接已生成", summary, 'showLink', true, link, jsonDataForExport );
                }                
                return { link, jsonData: jsonDataForExport };
            }
            return { link: "", jsonData: null };
        },

        generateShareLink: async function() {
            const selectedItemIds = this.getSelectedFileIds();
            if (!selectedItemIds.length) { uiManager.showAlert("请先勾选要分享的文件或文件夹。"); return ""; }

            return this._generateLinkProcess(async (addDataCb, markFolderCb, progressCb) => {
                let totalDiscovered = selectedItemIds.length;
                let processedCount = 0;
                let successCount = 0;
                let failureCount = 0;

                async function processSingleItem(itemInput, currentRelativePath) { // itemInput can be itemId or itemObject
                    if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                    
                    // Add pause check
                    while (processStateManager.isPaused()) {
                        await new Promise(r => setTimeout(r, 100)); // Wait 100ms before checking again
                    }
                    
                    let itemDetails;
                    let itemIdForDisplay = typeof itemInput === 'object' && itemInput !== null && itemInput.FileID ? itemInput.FileID : itemInput;
                    const baseItemName = `${currentRelativePath || '根目录'}/${itemIdForDisplay}`;
                    progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: baseItemName, extraStatus: "获取信息..." });

                    if (typeof itemInput === 'object' && itemInput !== null && itemInput.FileID !== undefined && itemInput.FileName !== undefined && itemInput.Type !== undefined) {
                        // Use provided itemDetails if it's a complete object
                        itemDetails = itemInput;
                    } else {
                        // Otherwise, fetch itemDetails
                        try {
                            const itemInfoResponse = await apiHelper.getFileInfo([String(itemInput)]); // itemInput is an ID here
                            if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                            if (!itemInfoResponse?.data?.infoList?.length) throw new Error(`项目 ${itemInput} 信息未找到`);
                            itemDetails = itemInfoResponse.data.infoList[0];
                        } catch (e) {
                            if (processStateManager.isStopRequested()) throw e;
                            failureCount++; processedCount++;
                            processStateManager.appendErrorMessage(`❌ 获取项目 "${baseItemName}" 详情最终失败: ${e.message}`);
                            progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: baseItemName, extraStatus: "获取信息失败" });
                            return;
                        }
                    }

                    if (isNaN(itemDetails.FileID)) { failureCount++; processedCount++; processStateManager.appendErrorMessage(`❌ 项目 "${itemDetails.FileName || itemId}" FileID无效`); progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: baseItemName }); return; }
                    const cleanName = (itemDetails.FileName || "Unknown").replace(/[#$%\/]/g, "_").replace(new RegExp(COMMON_PATH_DELIMITER.replace(/[.*+?^${}()|[\\\]\\\\]/g, '\\\\$&'), 'g'), '_');
                    const itemDisplayPath = `${currentRelativePath ? currentRelativePath + '/' : ''}${cleanName}`;
                    const formattedSize = formatBytes(Number(itemDetails.Size) || 0);
                    processedCount++;
                    progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: `${itemDisplayPath} (${formattedSize})` });

                    if (itemDetails.Type === 0) { // File
                        if (itemDetails.Etag && itemDetails.Size !== undefined) {
                            if (filterManager.shouldFilterFile(cleanName, true)) { 
                                if (settingsManager.isDebugMode()) processStateManager.appendInfoMessage(`⏭️ 已过滤: ${itemDisplayPath} (${formattedSize})`); 
                            }
                            else { 
                                addDataCb({ etag: itemDetails.Etag, size: itemDetails.Size, fullPath: itemDisplayPath }); 
                                successCount++; 
                                if (settingsManager.isDebugMode()) processStateManager.appendInfoMessage(`✔️ 文件: ${itemDisplayPath} (${formattedSize})`); 
                            }
                        } else { 
                            failureCount++; 
                            let ed = !itemDetails.Etag ? "缺少Etag" : "缺少大小"; 
                            processStateManager.appendErrorMessage(`❌ 文件 "${itemDisplayPath}" (${formattedSize}) ${ed}`); 
                        }
                    } else if (itemDetails.Type === 1) { // Folder
                        markFolderCb(true);
                        processStateManager.appendInfoMessage(`📁 扫描文件夹: ${itemDisplayPath}`);
                        progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: itemDisplayPath, extraStatus: "列出内容..." });
                        try {
                            const contents = await apiHelper.listDirectoryContents(itemDetails.FileID);
                            if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                            totalDiscovered += contents.length;
                            for (const contentItem of contents) {
                                if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                                if (isNaN(contentItem.FileID)) { failureCount++; totalDiscovered = Math.max(1, totalDiscovered -1); processStateManager.appendErrorMessage(`❌ 文件夹 "${itemDisplayPath}" 内发现无效项目ID`); continue; }
                                await processSingleItem(contentItem, itemDisplayPath); // Pass the whole contentItem
                                await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS / 10));
                            }
                        } catch (e) { if (processStateManager.isStopRequested()) throw e; processStateManager.appendErrorMessage(`❌ 处理文件夹 "${itemDisplayPath}" 内容最终失败: ${e.message}`); progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: itemDisplayPath, extraStatus: "列出内容失败" }); }
                    }
                    await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS / 5));
                }

                progressCb({ processed: 0, total: totalDiscovered, successCount: 0, failureCount: 0, currentFile: "准备开始..." });
                for (let i = 0; i < selectedItemIds.length; i++) { if (processStateManager.isStopRequested()) break; await processSingleItem(selectedItemIds[i], ""); }
                return { totalDiscoveredItemsForProgress: Math.max(totalDiscovered, processedCount), itemsProcessedForProgress: processedCount, successes: successCount, failures: failureCount };
            }, "生成秒传链接");
        },

        generateLinkFromPublicShare: async function(shareKey, sharePwd, startParentFileId = "0", isBatchMode = false) {
            if (!shareKey?.trim()) { uiManager.showAlert("分享Key不能为空。"); return "";}
            if (isNaN(parseInt(startParentFileId))) { uiManager.showAlert("起始文件夹ID必须是数字。"); return ""; }

            return this._generateLinkProcess(async (addDataCb, markFolderCb, progressCb) => {
                let totalDiscovered = 1;
                let processedCount = 0;
                let successCount = 0;
                let failureCount = 0;

                async function _fetchSharedItemsRecursive(currentSharedParentId, currentRelativePath) {
                    if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                    
                    // Add pause check
                    while (processStateManager.isPaused()) {
                        await new Promise(r => setTimeout(r, 100)); // Wait 100ms before checking again
                    }
                    
                    const baseItemNameForUI = `${currentRelativePath || '分享根目录'}/ID:${currentSharedParentId}`;
                    progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: baseItemNameForUI, extraStatus: "获取分享内容..." });

                    let contents;
                    try {
                        contents = await apiHelper.listSharedDirectoryContents(currentSharedParentId, shareKey, sharePwd);
                        if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                    } catch (e) {
                        if (processStateManager.isStopRequested()) throw e;
                        failureCount++; processedCount++;
                        processStateManager.appendErrorMessage(`❌ 获取分享目录 "${baseItemNameForUI}" 内容失败: ${e.message}`);
                        progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: baseItemNameForUI, extraStatus: "获取分享内容失败" });
                        return;
                    }

                    if (processedCount === 0 && currentSharedParentId === startParentFileId) totalDiscovered = contents.length > 0 ? contents.length : 1;
                    else totalDiscovered += contents.length;
                    processedCount++;

                    for (const item of contents) {
                        if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                        if (isNaN(item.FileID)) { failureCount++; totalDiscovered = Math.max(1, totalDiscovered-1); processStateManager.appendErrorMessage(`❌ 分享内发现无效项目ID: ${item.FileName}`); continue; }

                        const cleanName = (item.FileName || "Unknown").replace(/[#$%\/]/g, "_").replace(new RegExp(COMMON_PATH_DELIMITER.replace(/[.*+?^${}()|[\\\]\\\\]/g, '\\\\$&'), 'g'), '_');
                        const itemDisplayPath = `${currentRelativePath ? currentRelativePath + '/' : ''}${cleanName}`;
                        const formattedSize = formatBytes(Number(item.Size) || 0);

                        // Test case for test_failure.txt
                        if (cleanName === "test_failure.txt") {
                            processStateManager.appendErrorMessage(`❌ 测试失败文件: ${itemDisplayPath}`);
                            failureCount++;
                            continue;
                        }

                        let itemProcessedThisLoop = false; // Flag to ensure processedCount is incremented correctly for files

                        if (item.Type === 0) { // File
                            progressCb({ processed: processedCount + (itemProcessedThisLoop ? 0 : 1), total: totalDiscovered, successCount, failureCount, currentFile: `${itemDisplayPath} (${formattedSize})` });
                            if (item.Etag && item.Size !== undefined) {
                                if (filterManager.shouldFilterFile(cleanName, true)) { processStateManager.appendInfoMessage(`⏭️ 已过滤: ${itemDisplayPath} (${formattedSize})`); }
                                else { addDataCb({ etag: item.Etag, size: item.Size, fullPath: itemDisplayPath }); successCount++; if (settingsManager.isDebugMode()) processStateManager.appendInfoMessage(`✔️ 文件 (分享): ${itemDisplayPath} (${formattedSize})`);}
                            } else { failureCount++; let ed = !item.Etag ? "缺少Etag" : "缺少大小"; processStateManager.appendErrorMessage(`❌ 分享文件 "${itemDisplayPath}" (${formattedSize}) ${ed}`); }
                            if(!itemProcessedThisLoop) { processedCount++; itemProcessedThisLoop = true;}
                        } else if (item.Type === 1) { // Folder
                             progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: itemDisplayPath }); // Update UI for folder before recursive call
                            markFolderCb(true);
                            processStateManager.appendInfoMessage(`📁 扫描分享文件夹: ${itemDisplayPath}`);
                            await _fetchSharedItemsRecursive(item.FileID, itemDisplayPath);
                        }
                        await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS / 20));
                    }
                }
                progressCb({ processed: 0, total: totalDiscovered, successCount: 0, failureCount: 0, currentFile: "准备开始从分享链接生成..." });
                await _fetchSharedItemsRecursive(startParentFileId, "");
                return { totalDiscoveredItemsForProgress: Math.max(totalDiscovered, processedCount), itemsProcessedForProgress: processedCount, successes: successCount, failures: failureCount };
            }, `从分享链接生成 (Key: ${shareKey.substring(0,8)}...)`, isBatchMode);
        },

        parseShareLink: (shareLink) => {
            let commonBasePath = ""; let isCommonPathFormat = false; let isV2EtagFormat = false;
            if (shareLink.startsWith(COMMON_PATH_LINK_PREFIX_V2)) { isCommonPathFormat = true; isV2EtagFormat = true; shareLink = shareLink.substring(COMMON_PATH_LINK_PREFIX_V2.length); }
            else if (shareLink.startsWith(COMMON_PATH_LINK_PREFIX_V1)) { isCommonPathFormat = true; shareLink = shareLink.substring(COMMON_PATH_LINK_PREFIX_V1.length); }
            if (isCommonPathFormat) { const delimiterPos = shareLink.indexOf(COMMON_PATH_DELIMITER); if (delimiterPos > -1) { commonBasePath = shareLink.substring(0, delimiterPos); shareLink = shareLink.substring(delimiterPos + 1); } else { console.error("Malformed common path link: delimiter not found."); isCommonPathFormat = false; } }
            else { if (shareLink.startsWith(LEGACY_FOLDER_LINK_PREFIX_V2)) { isV2EtagFormat = true; shareLink = shareLink.substring(LEGACY_FOLDER_LINK_PREFIX_V2.length); } else if (shareLink.startsWith(LEGACY_FOLDER_LINK_PREFIX_V1)) { shareLink = shareLink.substring(LEGACY_FOLDER_LINK_PREFIX_V1.length); } }
            return shareLink.split('$').map(sLink => { const parts = sLink.split('#'); if (parts.length >= 3) { let etag = parts[0]; try { etag = optimizedEtagToHex(parts[0], isV2EtagFormat); } catch (e) { console.error(`[${SCRIPT_NAME}] Error decoding ETag: ${parts[0]}, ${e.message}`); return null; } let filePath = parts.slice(2).join('#'); if (isCommonPathFormat && commonBasePath) filePath = commonBasePath + filePath; return { etag: etag, size: parts[1], fileName: filePath }; } return null; }).filter(i => i);
        },
        transferFromShareLink: async function(shareLink, targetFolderPath = "", startIndex = null, endIndex = null) {
            if (!shareLink?.trim()) { uiManager.showAlert("链接为空"); return; } const filesToProcess = this.parseShareLink(shareLink); if (!filesToProcess.length) { uiManager.showAlert("无法解析链接或链接中无有效文件信息"); return; }
            const isFolderStructureHint = shareLink.startsWith(LEGACY_FOLDER_LINK_PREFIX_V1) || shareLink.startsWith(COMMON_PATH_LINK_PREFIX_V1) || shareLink.startsWith(LEGACY_FOLDER_LINK_PREFIX_V2) || shareLink.startsWith(COMMON_PATH_LINK_PREFIX_V2) || filesToProcess.some(f => f.fileName.includes('/'));
            await this._executeActualFileTransfer(filesToProcess, isFolderStructureHint, "链接转存", [], targetFolderPath, startIndex, endIndex);
        },
        transferImportedJsonData: async function(jsonData, targetFolderPath = "", startIndex = null, endIndex = null) {
            if (!jsonData || typeof jsonData !== 'object') { uiManager.showAlert("JSON数据无效"); return; } const { scriptVersion, exportVersion, usesBase62EtagsInExport, commonPath, files } = jsonData; if (!files || !Array.isArray(files) || files.length === 0) { uiManager.showAlert("JSON文件中没有有效的文件条目。"); return; }
            processStateManager.appendInfoMessage(`[导入] JSON包含 ${files.length} 个条目。公共路径: '${commonPath || "(无)"}', Base62 ETags (声明): ${usesBase62EtagsInExport === undefined ? '未声明' : usesBase62EtagsInExport}`); let preprocessingFailedItems = [];
            const filesToProcess = files.map(fileFromJson => { if (!fileFromJson || typeof fileFromJson.path !== 'string' || !fileFromJson.size || !fileFromJson.etag) { const errorMsg = "条目无效 (缺少 path, size, or etag)"; preprocessingFailedItems.push({ fileName: (fileFromJson||{}).path || "未知文件(数据缺失)", error: errorMsg, originalEntry: fileFromJson||{} }); return null; } let finalEtag; try { let attemptDecode = usesBase62EtagsInExport; if (usesBase62EtagsInExport === undefined) { const isLikelyHex = /^[0-9a-fA-F]+$/.test(fileFromJson.etag); if (isLikelyHex && fileFromJson.etag.length === 32) attemptDecode = false; else if (!isLikelyHex || fileFromJson.etag.length < 32) attemptDecode = true; else attemptDecode = false; processStateManager.appendInfoMessage(`[导入推断] 文件 '${fileFromJson.path.substring(0,30)}...' ETag '${fileFromJson.etag.substring(0,10)}...', usesBase62EtagsInExport未声明,推断为: ${attemptDecode}`); } finalEtag = attemptDecode ? optimizedEtagToHex(fileFromJson.etag, true) : fileFromJson.etag; } catch (e) { const errorMsg = `ETag解码失败 (${fileFromJson.etag}): ${e.message}`; processStateManager.appendErrorMessage(`❌ ${errorMsg} 文件: ${fileFromJson.path}`); preprocessingFailedItems.push({ fileName: fileFromJson.path, error: errorMsg, originalEntry: fileFromJson }); return null; } const fullFileName = commonPath ? commonPath + fileFromJson.path : fileFromJson.path; return { etag: finalEtag, size: String(fileFromJson.size), fileName: fullFileName, originalEntry: fileFromJson }; }).filter(f => f !== null);
            if (preprocessingFailedItems.length > 0) processStateManager.appendErrorMessage(`[导入注意] ${preprocessingFailedItems.length} 个条目在预处理阶段失败,将不会被尝试转存。`);
            if (!filesToProcess.length && preprocessingFailedItems.length > 0) { uiManager.showModal("⚠️ JSON导入预处理失败",`所有 ${preprocessingFailedItems.length} 个文件条目在导入预处理阶段即发生错误,无法继续转存。<br><div id="fastlink-permanent-failures-log" style="display: block; margin-top: 10px; text-align: left; max-height: 150px; overflow-y: auto; border: 1px solid #ddd; padding: 5px; font-size: 0.85em;"><h4>预处理失败项目:</h4><div id="fastlink-failures-list">${preprocessingFailedItems.map(pf => `<p style="margin:2px 0;">📄 <span style="font-weight:bold;">${pf.fileName}</span>: <span style="color:red;">${pf.error}</span></p>`).join('')}</div></div>`, 'info_with_buttons', true, null, null, preprocessingFailedItems); return; }
            else if (!filesToProcess.length) { uiManager.showAlert("JSON文件中解析后无有效文件可转存(所有条目均无效或解码失败)。"); return; }
            const isFolderStructureHint = !!commonPath || filesToProcess.some(f => f.fileName.includes('/')); await this._executeActualFileTransfer(filesToProcess, isFolderStructureHint, "文件导入", preprocessingFailedItems, targetFolderPath, startIndex, endIndex);
        },
        _executeActualFileTransfer: async function(filesToProcess, isFolderStructureHint, operationTitle = "转存", initialPreprocessingFailures = [], targetFolderPath = "", startIndex = null, endIndex = null) {
            processStateManager.reset(); 
            this.currentOperationRateLimitStatus.consecutiveRateLimitFailures = 0; 
            let permanentlyFailedItems = [...initialPreprocessingFailures]; 
            let totalSuccessfullyTransferredSize = 0;

            // Apply range filter if specified
            console.log(`before: startIndex: ${startIndex}, endIndex: ${endIndex}`); 
            if (startIndex == null && endIndex !== null)
            {
                startIndex = 1;
            } else if (startIndex !== null && endIndex == null)
            {
                endIndex = originalLength;
            }
            console.log(`after: startIndex: ${startIndex}, endIndex: ${endIndex}`); 
            if (startIndex !== null && endIndex !== null) {
                const originalLength = filesToProcess.length;
                if (startIndex > originalLength) {
                    uiManager.showAlert(`起始序号超出范围:总文件数为 ${originalLength},但起始序号为 ${startIndex}`);
                    return;
                }
                if (endIndex > originalLength) {
                    uiManager.showAlert(`结束序号超出范围:总文件数为 ${originalLength},但结束序号为 ${endIndex}`);
                    return;
                }
                filesToProcess = filesToProcess.slice(startIndex - 1, endIndex);
                processStateManager.appendInfoMessage(`ℹ️ 已应用文件范围过滤: ${startIndex}-${endIndex} (共 ${filesToProcess.length} 个文件)`);
                if (filesToProcess.length === 0) {
                    uiManager.showAlert(`指定范围内没有文件可处理 (范围: ${startIndex}-${endIndex}, 总文件数: ${originalLength})`);
                    return;
                }
            }

            let rootDirId = this.getCurrentDirectoryId(); 
            if (rootDirId === null || isNaN(parseInt(rootDirId))) { uiManager.showAlert("无法确定当前目标目录ID。将尝试转存到根目录。"); rootDirId = "0"; } 
            rootDirId = parseInt(rootDirId);
            let userSpecifiedFolderPath = targetFolderPath ? targetFolderPath.trim() : ""; 
            let finalRootDirId = rootDirId;
            const pathPrefixInput = document.getElementById('fl-path-prefix');
            const pathPrefix = pathPrefixInput ? pathPrefixInput.value.trim() : "";
            if (pathPrefix) { processStateManager.appendInfoMessage(`ℹ️ 已启用路径前缀过滤: ${pathPrefix}`); filesToProcess = filesToProcess.filter(file => file.fileName.startsWith(pathPrefix)); if (filesToProcess.length === 0) { uiManager.showAlert("没有找到以指定路径前缀开头的文件。"); return; } }

            const initialModalTitle = `⚙️ ${operationTitle}状态 (${filesToProcess.length} 项)`;
            // Initial modal content with placeholder for folder selector (might be removed if no folder path input)
            let modalContent = `
                <div class="fastlink-progress-container"><div class="fastlink-progress-bar" style="width: 0%"></div></div>
                <div class="fastlink-status-container">
                    <div class="fastlink-info-status">
                        <p>🚀 准备${operationTitle} ${filesToProcess.length} 个文件到目录ID ${rootDirId}${userSpecifiedFolderPath ? " 的 " + userSpecifiedFolderPath + " 文件夹中" : ""}</p>
                        <div class="fastlink-info-messages"></div>
                    </div>
                    <div class="fastlink-error-status">
                        <p>⚠️ 错误信息</p>
                        <div class="fastlink-error-messages"></div>
                    </div>
                    <p class="extra-status-message" style="color: #ff7f50; display: none;"></p>
                </div>
                <div class="fastlink-stats"><span class="success-count">✅ 成功:0</span><span class="failed-count">❌ 失败:0</span></div>
                <div class="fastlink-current-file"><p class="file-name">准备开始...</p></div>
                <div id="fastlink-permanent-failures-log" style="display: none; margin-top: 10px; text-align: left; max-height: 100px; overflow-y: auto; border: 1px solid #ddd; padding: 5px; font-size: 0.85em;"><h4>永久失败项目:</h4><div id="fastlink-failures-list"></div></div>`;

            // Folder selector is only part of the 'inputLink' or 'inputPublicShare' initial modal, not the progress modal directly *during* transfer.
            // However, the logic to create userSpecifiedFolderPath runs *before* the loop.
            // For this modal, we don't need to show the input again.
            uiManager.showModal(initialModalTitle, modalContent, 'progress_stoppable', false);

            let successes = 0, failures = 0; const folderCache = {}; const startTime = Date.now();

            if (userSpecifiedFolderPath) {
                try {
                    processStateManager.updateProgressUI(0, filesToProcess.length, successes, failures, `创建目标文件夹: ${userSpecifiedFolderPath}`, "");
                    const dirContents = await apiHelper.listDirectoryContents(rootDirId, 500); // Check against current dir
                    if (processStateManager.isStopRequested()) { uiManager.showAlert("操作已取消"); return; }

                    const pathParts = userSpecifiedFolderPath.split('/');
                    let parentIdForUserPath = rootDirId; // User path is relative to current dir
                    let currentPathForUser = "";

                    for (let i = 0; i < pathParts.length; i++) {
                        const folderName = pathParts[i].trim(); if (!folderName) continue;
                        currentPathForUser = currentPathForUser ? `${currentPathForUser}/${folderName}` : folderName;
                        if (folderCache[currentPathForUser]) { parentIdForUserPath = folderCache[currentPathForUser]; continue; }

                        const existingFolder = dirContents.find(item => item.Type === 1 && item.FileName === folderName && item.ParentFileID == parentIdForUserPath); // More specific check
                        if (existingFolder && !isNaN(existingFolder.FileID)) {
                            parentIdForUserPath = existingFolder.FileID;
                            processStateManager.appendInfoMessage(`ℹ️ 文件夹已存在: ${folderName} (ID: ${parentIdForUserPath})`);
                        } else {
                            processStateManager.appendInfoMessage(`📁 创建文件夹: ${folderName} (在ID: ${parentIdForUserPath})`);
                            const newFolder = await apiHelper.createFolder(parentIdForUserPath, folderName);
                            if (processStateManager.isStopRequested()) { uiManager.showAlert("操作已取消"); return; }
                            if (newFolder && !isNaN(parseInt(newFolder.FileId))) { parentIdForUserPath = parseInt(newFolder.FileId); processStateManager.appendInfoMessage(`✅ 文件夹创建成功: ${folderName} (ID: ${parentIdForUserPath})`); }
                            else { throw new Error(`创建文件夹返回的ID无效: ${JSON.stringify(newFolder)}`); }
                        }
                        folderCache[currentPathForUser] = parentIdForUserPath;
                    }
                    finalRootDirId = parentIdForUserPath;
                    processStateManager.appendInfoMessage(`✅ 目标文件夹就绪: ${userSpecifiedFolderPath} (ID: ${finalRootDirId})`);
                } catch (error) {
                    processStateManager.appendErrorMessage(`❌ 创建目标文件夹 "${userSpecifiedFolderPath}" 失败: ${error.message}`);
                    console.error(`[${SCRIPT_NAME}] 创建目标文件夹错误:`, error);
                    uiManager.showAlert(`创建目标文件夹失败: ${error.message},将尝试转存到当前目录 (ID: ${rootDirId})`);
                    finalRootDirId = rootDirId; // Fallback
                }
            }

            for (let i = 0; i < filesToProcess.length; i++) {
                if (processStateManager.isStopRequested()) break;
                
                // Add pause check
                while (processStateManager.isPaused()) {
                    await new Promise(r => setTimeout(r, 100)); // Wait 100ms before checking again
                }

                const file = filesToProcess[i];
                const originalFileNameForLog = file.fileName || "未知文件";
                const formattedFileSize = file.size ? formatBytes(Number(file.size)) : "未知大小";

                if (!file || !file.fileName || !file.etag || !file.size) { failures++; processStateManager.appendErrorMessage(`❌ 跳过无效文件数据 (索引 ${i}): ${originalFileNameForLog}`); permanentlyFailedItems.push({ ...file, fileName: originalFileNameForLog, error: "无效文件数据" }); processStateManager.updateProgressUI(i + 1, filesToProcess.length, successes, failures, `无效数据 (${formattedFileSize})`); continue; }
                if (filterManager.shouldFilterFile(file.fileName, false)) { processStateManager.appendInfoMessage(`⏭️ 已过滤: ${file.fileName} (${formattedFileSize})`); processStateManager.updateProgressUI(i + 1, filesToProcess.length, successes, failures, `已过滤: ${file.fileName} (${formattedFileSize})`); continue; }

                processStateManager.updateProgressUI(i, filesToProcess.length, successes, failures, `${file.fileName} (${formattedFileSize})`, "");
                let effectiveParentId = finalRootDirId; // Start with the (potentially user-specified) target folder
                let actualFileName = file.fileName;

                try {
                    if (file.fileName.includes('/')) {
                        const pathParts = file.fileName.split('/');
                        actualFileName = pathParts.pop();
                        if (!actualFileName && pathParts.length > 0 && file.fileName.endsWith('/')) { processStateManager.appendErrorMessage(`⚠️ 文件路径 "${file.fileName}" (${formattedFileSize}) 可能表示目录,跳过。`); failures++; permanentlyFailedItems.push({ ...file, error: "路径表示目录" }); continue; }

                        let parentIdForLinkPath = finalRootDirId; // **FIXED**: Link paths are relative to finalRootDirId
                        let currentCumulativeLinkPath = "";

                        for (let j = 0; j < pathParts.length; j++) {
                            if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                            const part = pathParts[j]; if (!part) continue;
                            currentCumulativeLinkPath = j === 0 ? part : `${currentCumulativeLinkPath}/${part}`;
                            processStateManager.updateProgressUI(i, filesToProcess.length, successes, failures, `${file.fileName} (${formattedFileSize})`, `检查/创建路径: ${currentCumulativeLinkPath}`);

                            const cacheKeyForLinkPath = `link:${currentCumulativeLinkPath}`; // Namespace cache for link paths
                            if (folderCache[cacheKeyForLinkPath]) {
                                parentIdForLinkPath = folderCache[cacheKeyForLinkPath];
                            } else {
                                const dirContents = await apiHelper.listDirectoryContents(parentIdForLinkPath, 500);
                                if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                                const foundFolder = dirContents.find(it => it.Type === 1 && it.FileName === part && it.ParentFileID == parentIdForLinkPath);

                                if (foundFolder && !isNaN(foundFolder.FileID)) {
                                    parentIdForLinkPath = foundFolder.FileID;
                                } else {
                                    processStateManager.updateProgressUI(i, filesToProcess.length, successes, failures, `${file.fileName} (${formattedFileSize})`, `创建文件夹: ${currentCumulativeLinkPath}`);
                                    const createdFolder = await apiHelper.createFolder(parentIdForLinkPath, part);
                                    if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                                    parentIdForLinkPath = parseInt(createdFolder.FileId);
                                }
                                folderCache[cacheKeyForLinkPath] = parentIdForLinkPath;
                                await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS / 5));
                            }
                        }
                        effectiveParentId = parentIdForLinkPath;
                    }

                    if (isNaN(effectiveParentId) || effectiveParentId < 0) throw new Error(`路径创建失败或父ID无效 (${effectiveParentId}) for ${file.fileName} (${formattedFileSize})`);
                    if (!actualFileName) throw new Error(`文件名无效 for ${file.fileName} (${formattedFileSize})`);

                    processStateManager.updateProgressUI(i, filesToProcess.length, successes, failures, `${actualFileName} (${formattedFileSize})`, `秒传到ID: ${effectiveParentId}`);
                    await apiHelper.rapidUpload(file.etag, file.size, actualFileName, effectiveParentId);
                    if (processStateManager.isStopRequested()) throw new Error("UserStopped"); successes++; totalSuccessfullyTransferredSize += Number(file.size); processStateManager.appendInfoMessage(`✔️ 文件: ${file.fileName} (${formattedFileSize})`);
                } catch (e) { if (processStateManager.isStopRequested()) break; failures++; processStateManager.appendErrorMessage(`❌ 文件 "${actualFileName}" (${formattedFileSize}) (原始: ${originalFileNameForLog}) 失败: ${e.message}`); permanentlyFailedItems.push({ ...file, fileName: originalFileNameForLog, error: e.message }); processStateManager.updateProgressUI(i + 1, filesToProcess.length, successes, failures, `${actualFileName} (${formattedFileSize})`, "操作失败"); }
                await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS / 5));
            }
            processStateManager.updateProgressUI(filesToProcess.length, filesToProcess.length, successes, failures, "处理完成", "");
            const totalTime = Math.round((Date.now() - startTime) / 1000); let resultEmoji = successes > 0 && permanentlyFailedItems.length === 0 ? '🎉' : (successes > 0 ? '🎯' : '😢'); if (processStateManager.isStopRequested()) resultEmoji = '🔴';
            let finalUserMessage = processStateManager.isStopRequested() ? "操作已由用户停止" : `${operationTitle}完成`; if (!processStateManager.isStopRequested() && permanentlyFailedItems.length > 0) finalUserMessage = `${operationTitle}部分完成或预处理失败,共 ${permanentlyFailedItems.length} 个文件有问题。`;
            const formattedTotalSuccessfullyTransferredSize = formatBytes(totalSuccessfullyTransferredSize);
            let summary = `<div class="fastlink-result"><h3>${resultEmoji} ${finalUserMessage}</h3><p>✅ 成功转存: ${successes} 个文件</p><p>💾 成功转存总大小: ${formattedTotalSuccessfullyTransferredSize}</p><p>❌ 转存尝试失败: ${failures} 个文件</p><p>📋 总计问题文件 (含预处理): ${permanentlyFailedItems.length} 个</p><p>⏱️ 耗时: ${totalTime} 秒</p>${!processStateManager.isStopRequested() && successes > 0 ? '<p>📢 请手动刷新页面查看已成功转存的结果</p>' : ''}</div>`;
            if (permanentlyFailedItems.length > 0) {
                summary += `<div class="fastlink-failures-summary" style="margin-top: 15px; text-align: left; max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; font-size: 0.9em;">
                    <h4 style="margin: 0 0 10px 0;">❌ 失败项目详情:</h4>
                    ${permanentlyFailedItems.map((item, index) => `
                        <div class="failure-item" style="margin-bottom: 8px; padding: 5px; background: ${index % 2 === 0 ? '#f9f9f9' : '#fff'};">
                            <p style="margin: 0;"><strong>文件名:</strong> ${item.fileName || '未知文件'}</p>
                            <p style="margin: 3px 0 0 0; color: #ff4444;"><strong>错误:</strong> ${item.error || '未知错误'}</p>
                        </div>
                    `).join('')}
                </div>`;
            }
            uiManager.updateModalContent(summary);
            if (permanentlyFailedItems.length > 0 && !processStateManager.isStopRequested()) {
                const failuresLogDiv = document.getElementById('fastlink-failures-list'); const permanentFailuresDiv = document.getElementById('fastlink-permanent-failures-log');
                if (failuresLogDiv && permanentFailuresDiv) { failuresLogDiv.innerHTML = ''; permanentlyFailedItems.forEach(pf => { const p = document.createElement('p'); p.style.margin = '2px 0'; p.innerHTML = `📄 <span style="font-weight:bold;">${pf.fileName}</span>: <span style="color:red;">${pf.error || '未知错误'}</span>`; failuresLogDiv.appendChild(p); }); permanentFailuresDiv.style.display = 'block'; }
                const modalInstance = uiManager.getModalElement();
                if (modalInstance) {
                    let buttonsDiv = modalInstance.querySelector('.fastlink-modal-buttons'); if(!buttonsDiv) { buttonsDiv = document.createElement('div'); buttonsDiv.className = 'fastlink-modal-buttons'; modalInstance.querySelector(`#${uiManager.MODAL_CONTENT_ID}`)?.appendChild(buttonsDiv); } buttonsDiv.innerHTML = '';
                    const retryBtn = document.createElement('button'); retryBtn.id = 'fl-m-retry-failed'; retryBtn.className = 'confirm-btn'; retryBtn.textContent = `🔁 重试失败项 (${permanentlyFailedItems.length})`; retryBtn.onclick = () => { this._executeActualFileTransfer(permanentlyFailedItems, isFolderStructureHint, operationTitle + " - 重试", [], targetFolderPath, startIndex, endIndex); }; buttonsDiv.appendChild(retryBtn);
                    const copyLogBtn = document.createElement('button'); copyLogBtn.id = 'fl-m-copy-failed-log'; copyLogBtn.className = 'copy-btn'; copyLogBtn.style.marginLeft = '10px'; copyLogBtn.textContent = '复制问题日志'; copyLogBtn.onclick = () => { const logText = permanentlyFailedItems.map(pf => `文件: ${pf.fileName || (pf.originalEntry&&pf.originalEntry.path)||'未知路径'}\n${(pf.originalEntry&&pf.originalEntry.etag)?('原始ETag: '+pf.originalEntry.etag+'\n'):(pf.etag?'处理后ETag: '+pf.etag+'\n':'')}${(pf.originalEntry&&pf.originalEntry.size)?('大小: '+pf.originalEntry.size+'\n'):(pf.size?'大小: '+pf.size+'\n':'')}错误: ${pf.error||'未知错误'}`).join('\n\n'); GM_setClipboard(logText); uiManager.showAlert("问题文件日志已复制到剪贴板!", 1500); }; buttonsDiv.appendChild(copyLogBtn);
                    const closeBtnModal = document.createElement('button'); closeBtnModal.id = 'fl-m-final-close'; closeBtnModal.className = 'cancel-btn'; closeBtnModal.textContent = '关闭'; closeBtnModal.style.marginLeft = '10px'; closeBtnModal.onclick = () => uiManager.hideModal(); buttonsDiv.appendChild(closeBtnModal);
                }
                 uiManager.enableModalCloseButton(false); // Use custom close button
            } else {
                 uiManager.enableModalCloseButton(true); // Enable original close button
            }
        }
    };

    const uiManager = {
        modalElement: null, dropdownMenuElement: null, STYLE_ID: 'fastlink-dynamic-styles', MODAL_CONTENT_ID: 'fastlink-modal-content-area',
        activeModalOperationType: null, modalHideCallback: null,
        miniProgressElement: null, isMiniProgressActive: false, // Added for mini progress

        _downloadToFile: function(content, filename, contentType) { const a = document.createElement('a'); const blob = new Blob([content], { type: contentType }); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); URL.revokeObjectURL(a.href); },
        applyStyles: function() {
            const style = document.createElement('style');
            style.textContent = `
                .fastlink-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 10000; width: 500px; max-height: 80vh; display: flex; flex-direction: column; }
                .fastlink-modal-title { font-size: 18px; font-weight: bold; margin-bottom: 15px; text-align: center; }
                .fastlink-modal-content { flex: 1; overflow-y: auto; margin-bottom: 15px; position: relative; }
                .fastlink-modal-buttons { display: flex; justify-content: center; gap: 10px; }
                .fastlink-modal-buttons button { padding: 8px 15px; border: none; border-radius: 4px; cursor: pointer; }
                .fastlink-modal-buttons button:disabled { opacity: 0.5; cursor: not-allowed; }
                .fastlink-modal-buttons .confirm-btn { background: #4CAF50; color: white; }
                .fastlink-modal-buttons .cancel-btn { background: #f44336; color: white; }
                .fastlink-modal-buttons .copy-btn { background: #2196F3; color: white; }
                .fastlink-modal-buttons .export-btn { background: #FF9800; color: white; }
                .fastlink-modal-buttons .stop-btn { background: #f44336; color: white; }
                .fastlink-modal-buttons .pause-btn { background: #FF9800; color: white; }
                .fastlink-modal-buttons .minimize-btn { background: #9E9E9E; color: white; }
                .fastlink-modal-buttons .close-btn { background: #9E9E9E; color: white; }
                .fastlink-modal-input{width:calc(100% - 16px);padding:8px;margin-bottom:10px;border:1px solid #ccc;border-radius:4px}
                .fastlink-modal-textarea{width:calc(100% - 16px);padding:8px;margin-bottom:10px;border:1px solid #ccc;border-radius:4px;min-height:200px;font-family:monospace;white-space:pre;resize:vertical}
                .fastlink-file-input-container{margin-top:10px;margin-bottom:5px;text-align:left}
                .fastlink-file-input-container label{margin-right:5px;font-size:0.9em;}
                .fastlink-file-input-container input[type="file"]{font-size:0.9em;max-width:250px;}
                .fastlink-progress-container{width:100%;height:10px;background-color:#f0f0f0;border-radius:5px;margin:10px 0 15px;overflow:hidden}
                .fastlink-progress-bar{height:100%;background-color:#1890ff;transition:width .3s ease}
                .fastlink-status{text-align:left;margin-bottom:10px;max-height:150px;overflow-y:auto;border:1px solid #eee;padding:0px;font-size:.9em;position:relative}
                .fastlink-status p:first-child{position:sticky;top:0;background:white;z-index:1;margin:0;padding:3px 0;border-bottom:1px solid #eee}
                .fastlink-status p{margin:3px 0;line-height:1.3}
                .fastlink-stats{display:flex;justify-content:space-between;margin:10px 0;border-top:1px solid #eee;border-bottom:1px solid #eee;padding:5px 0}
                .fastlink-current-file{width:90%;height:81px;margin:0 auto;overflow-y:auto;word-break:break-all;background-color:#f9f9f9;padding:8px;border-radius:4px;border:1px solid #ddd;font-family:monospace;font-size:0.9em;line-height:1.4}
                .error-message{color:#d9534f;font-size:.9em}
                .info-message{color:#28a745;font-size:.9em}
                .fastlink-result{text-align:center}
                .fastlink-result h3{font-size:18px;margin:5px 0 15px}
                .fastlink-result p{margin:8px 0}
                .fastlink-link-text{width:90%;height:81px;margin:0 auto;overflow-y:auto;word-break:break-all;background-color:#f9f9f9;padding:8px;border-radius:4px;border:1px solid #ddd;font-family:monospace;font-size:0.9em;line-height:1.4}
                #fastlink-dropdown-menu-container{position:absolute;background:#fff;border:1px solid #e4e7ed;padding:0;box-shadow:0 4px 12px rgba(0,0,0,.12);margin-top:5px;z-index:10002 !important;max-height:calc(100vh - 80px);overflow-y:auto;top:100%;left:0;min-width:180px;font-size:14px;border-radius:8px;}
                #fastlink-dropdown-menu-container ul.ant-dropdown-menu{padding: 6px 0;}
                #fastlink-dropdown-menu-container li.ant-dropdown-menu-item{font-size:14px;line-height:22px;padding: 6px 20px;white-space:nowrap;}
                #fastlink-dropdown-menu-container li.ant-dropdown-menu-item:hover{background:#f5f5f5;}
                #fastlink-dropdown-menu-container li[role="separator"]{height:1px;background:#f0f0f0;margin:4px 0;padding:0;}
                .fastlink-drag-drop-area{border:2px dashed #ccc;padding:10px;transition: border-color .3s ease;}
                .fastlink-drag-drop-area.drag-over-active{border-color:#007bff; background-color: #f8f9fa;}
                .filter-controls{display:flex;justify-content:space-between;margin-bottom:15px;}
                .filter-btn{padding:5px 10px;border:1px solid #ddd;border-radius:4px;background:#f8f9f8;cursor:pointer;font-size:0.9em;}
                .filter-btn:hover{background:#e9ecef;}
                .filter-description{margin-bottom:15px;text-align:left;font-size:0.9em;}
                .filter-list{max-height:250px;overflow-y:auto;border:1px solid #eee;padding:5px;text-align:left;margin-bottom:15px;}
                .filter-item{display:flex;align-items:center;padding:5px 0;border-bottom:1px solid #f5f5f5;}
                .filter-item:last-child{border-bottom:none;}
                .filter-checkbox{margin-right:10px;}
                .filter-emoji{margin-right:5px;}
                .filter-ext{font-weight:bold;margin-right:8px;}
                .filter-name{color:#666;font-size:0.9em;}
                .fastlink-modal.filter-dialog{max-height:90vh;display:flex;flex-direction:column;}
                .fastlink-modal.filter-dialog .fastlink-modal-content{flex:1;overflow-y:auto;max-height:calc(90vh - 120px);}
                .filter-global-switches{margin-bottom:15px;text-align:left;}
                .filter-switch-item{display:flex;align-items:center;margin-bottom:8px;}
                .filter-toggle-checkbox{margin-right:10px;}
                .filter-divider{margin:15px 0;border:0;border-top:1px solid #eee;}
                .filter-select-style-container { position: relative; margin-bottom: 15px; }
                .filter-selected-tags { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px; min-height: 38px; margin-bottom: -1px; }
                .filter-tag { display: inline-flex; align-items: center; background-color: #e6f7ff; border: 1px solid #91d5ff; border-radius: 4px; padding: 3px 8px; font-size: 0.9em; cursor: default; }
                .filter-tag .filter-emoji { margin-right: 4px; } .filter-tag .filter-tag-text { font-weight: bold; } .filter-tag .filter-tag-name { color: #555; margin-left: 4px; font-size: 0.9em; }
                .filter-tag-remove { margin-left: 8px; cursor: pointer; font-weight: bold; color: #555; } .filter-tag-remove:hover { color: #000; }
                .filter-search-input { width: 100%; padding: 8px 10px; border: 1px solid #d9d9d9; border-radius: 0 0 4px 4px; box-sizing: border-box; font-size: 0.95em; }
                .filter-selected-tags + .filter-search-input { border-top-left-radius: 0; border-top-right-radius: 0; }
                .filter-search-input:focus { outline: none; border-color: #40a9ff; box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); }
                .filter-dropdown { position: absolute; top: 100%; left: 0; right: 0; background-color: #fff; border: 1px solid #d9d9d9; border-top: none; max-height: 200px; overflow-y: auto; z-index: 1001; display: none; border-radius: 0 0 4px 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
                .filter-dropdown-item { display: flex; align-items: center; padding: 8px 12px; cursor: pointer; font-size: 0.9em; }
                .filter-dropdown-item:hover { background-color: #f5f5f5; } .filter-dropdown-item .filter-emoji { margin-right: 6px; } .filter-dropdown-item .filter-ext { font-weight: bold; margin-right: 6px; } .filter-dropdown-item .filter-name { color: #555; }
                .fastlink-modal.filter-dialog .fastlink-modal-content { max-height: calc(90vh - 160px); }
                .folder-selector-container{margin-top:10px;text-align:left;}.folder-selector-label{display:block;margin-bottom:5px;font-size:0.9em;}.folder-selector-input-container{position:relative;}.folder-selector-input{width:100%;padding:8px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box;}.folder-selector-dropdown{position:absolute;width:100%;max-height:200px;overflow-y:auto;background:#fff;border:1px solid #ccc;border-top:none;border-radius:0 0 4px 4px;z-index:1000;display:none;}.folder-selector-dropdown.active{display:block;}.folder-item{display:flex;align-items:center;padding:8px 10px;cursor:pointer;}.folder-item:hover{background:#f5f5f5;}.folder-item-checkbox{margin-right:10px;}.folder-item-icon{margin-right:8px;color:#1890ff;}.folder-item-name{flex-grow:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.folder-tag-container{display:flex;flex-wrap:wrap;gap:5px;margin-top:5px;min-height:30px;border:1px solid #eee;padding:5px;border-radius:4px;}.folder-tag{display:flex;align-items:center;background:#e6f7ff;border-radius:2px;padding:2px 8px;border:1px solid #91d5ff;}.folder-tag-text{margin-right:5px;}.folder-tag-remove{cursor:pointer;color:#999;font-weight:bold;font-size:14px;}.folder-tag-remove:hover{color:#666;}
                .fastlink-mini-progress{position:fixed;bottom:15px;right:15px;width:280px;background-color:#fff;border:1px solid #ccc;border-radius:6px;box-shadow:0 2px 10px rgba(0,0,0,.2);z-index:10005;padding:10px;font-size:0.85em;display:none;flex-direction:column;}
                .fastlink-mini-progress-title{font-weight:bold;margin-bottom:5px;display:flex;justify-content:space-between;align-items:center;}
                .fastlink-mini-progress-bar-container{width:100%;height:8px;background-color:#e9ecef;border-radius:4px;overflow:hidden;margin-bottom:5px;}
                .fastlink-mini-progress-bar{height:100%;background-color:#007bff;transition:width .2s ease;}
                .fastlink-mini-progress-file{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:3px;color:#555;}
                .fastlink-mini-progress-status{font-size:0.9em;color:#333;}
                .fastlink-mini-progress-restore-btn{font-size:0.8em;padding:3px 8px;background-color:#6c757d;color:white;border:none;border-radius:3px;cursor:pointer;align-self:flex-start;margin-top:5px;}
                .fastlink-mini-progress-restore-btn:hover{background-color:#5a6268;}
                .settings-dialog .settings-group { margin-bottom: 15px; }
                .settings-dialog .settings-group h4 { margin: 0 0 10px 0; color: #333; }
                .settings-dialog .settings-item { display: flex; align-items: center; margin-bottom: 8px; }
                .settings-dialog .settings-item label { margin-left: 8px; cursor: pointer; }
                .settings-dialog .settings-description { font-size: 0.85em; color: #666; margin-left: 24px; margin-top: 2px; }
                
                /* API Test Styles */
                .api-test-container {
                    margin-top: 15px;
                    padding: 15px;
                    background: #f5f5f5;
                    border-radius: 8px;
                    display:none;
                }
                
                .api-test-row {
                    margin-bottom: 10px;
                    display: flex;
                    align-items: center;
                    gap: 10px;
                }
                
                .api-test-row label {
                    min-width: 100px;
                    font-weight: 500;
                }
                
                .api-path-select {
                    flex: 1;
                    padding: 8px;
                    border: 1px solid #ddd;
                    border-radius: 4px;
                    background: white;
                }
                
                .api-param-input {
                    flex: 1;
                    padding: 8px;
                    border: 1px solid #ddd;
                    border-radius: 4px;
                }
                
                .api-test-button {
                    padding: 8px 16px;
                    background: #1890ff;
                    color: white;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                    font-weight: 500;
                }
                
                .api-test-button:hover {
                    background: #40a9ff;
                }
                
                .api-test-button:disabled {
                    background: #d9d9d9;
                    cursor: not-allowed;
                }
                
                .api-test-result {
                    margin-top: 15px;
                    padding: 10px;
                    border-radius: 4px;
                }
                
                .api-test-success {
                    background: #f6ffed;
                    border: 1px solid #b7eb8f;
                }
                
                .api-test-error {
                    background: #fff2f0;
                    border: 1px solid #ffccc7;
                }
                
                .api-test-loading {
                    color: #666;
                    font-style: italic;
                }
                
                .api-test-result pre {
                    margin: 10px 0 0;
                    padding: 10px;
                    background: white;
                    border-radius: 4px;
                    overflow-x: auto;
                    font-family: monospace;
                    font-size: 12px;
                    line-height: 1.5;
                }
                .fastlink-status-container {
                    text-align: left;
                    margin-bottom: 10px;
                    border: 1px solid #eee;
                    padding: 0px;
                    font-size: .9em;
                    position: relative;
                }
                .fastlink-info-status, .fastlink-error-status {
                    max-height: 100px;
                    overflow-y: auto;
                    border-bottom: 1px solid #eee;
                }
                .fastlink-info-status p:first-child, .fastlink-error-status p:first-child {
                    position: sticky;
                    top: 0;
                    background: white;
                    z-index: 1;
                    margin: 0;
                    padding: 3px 0;
                    border-bottom: 1px solid #eee;
                }
                .fastlink-info-messages, .fastlink-error-messages {
                    padding: 5px;
                }
                .fastlink-info-messages p, .fastlink-error-messages p {
                    margin: 3px 0;
                    line-height: 1.3;
                }
                .error-message {
                    color: #d9534f;
                    font-size: .9em;
                }
                .info-message {
                    color: #28a745;
                    font-size: .9em;
                }
                .fastlink-modal-version { position: absolute; bottom: 5px; right: 10px; font-size: 0.7em; color: #999; }
                .fastlink-menu-btn {
                    height: 32px;
                    font-size: 14px;
                    padding: 0 15px;
                    display: inline-flex;
                    align-items: center;
                    gap: 6px;
                    border-radius: 6px;
                    border: 1px solid #d9d9d9;
                    box-shadow: none;
                    outline: none;
                    transition: box-shadow .2s, border-color .2s;
                    white-space: nowrap;
                }
                .fastlink-menu-btn:focus, .fastlink-menu-btn:active {
                    border-color: #4096ff;
                    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
                    outline: none;
                }
                .home-operator-button-group .ant-btn, .home-operator-button-group .fastlink-menu-btn {
                    margin-right: 8px;
                }
            `;
            style.textContent += `
                .fastlink-info-popup {
                    position: fixed;
                    bottom: 20px;
                    left: 50%;
                    transform: translateX(-50%);
                    background-color: #333;
                    color: white;
                    padding: 10px 20px;
                    border-radius: 5px;
                    z-index: 99999;
                    opacity: 1;
                    transition: opacity 0.5s ease-out;
                }
                .fastlink-info-popup-fadeout {
                    opacity: 0;
                }
                .fastlink-btn-preset-selected {
                    box-shadow: 0 0 0 2px #4096ff;
                    border: 2px solid #4096ff !important;
                    z-index: 1;
                }
            `;
            document.head.appendChild(style);
        },
        initMiniProgress: function() { // Added for mini progress
            if (this.miniProgressElement) return;
            this.miniProgressElement = document.createElement('div');
            this.miniProgressElement.className = 'fastlink-mini-progress';
            this.miniProgressElement.innerHTML = `
                <div class="fastlink-mini-progress-title">
                    <span>⚙️ 处理中...</span>
                    <button class="fastlink-mini-progress-restore-btn">恢复</button>
                </div>
                <div class="fastlink-mini-progress-bar-container"><div class="fastlink-mini-progress-bar" style="width: 0%;"></div></div>
                <div class="fastlink-mini-progress-file">准备中...</div>
                <div class="fastlink-mini-progress-status">0/0</div>
            `;
            document.body.appendChild(this.miniProgressElement);
            this.miniProgressElement.querySelector('.fastlink-mini-progress-restore-btn').addEventListener('click', () => {
                this.hideMiniProgress();
                if (this.modalElement && this.activeModalOperationType === 'progress_stoppable') {
                    this.modalElement.style.display = 'flex';
                }
            });
        },
        showMiniProgress: function() { // Added for mini progress
            if (this.miniProgressElement) {
                this.miniProgressElement.style.display = 'flex';
                this.isMiniProgressActive = true;
            }
        },
        hideMiniProgress: function() { // Added for mini progress
            if (this.miniProgressElement) {
                this.miniProgressElement.style.display = 'none';
                this.isMiniProgressActive = false;
            }
        },
        createDropdownButton: function() {
            const existingButtons = document.querySelectorAll('.fastlink-main-button-container'); 
            existingButtons.forEach(btn => btn.remove()); 
            const targetElement = document.querySelector(DOM_SELECTORS.TARGET_BUTTON_AREA); 
            if (targetElement && targetElement.parentNode) { 
                const buttonContainer = document.createElement('div'); 
                buttonContainer.className = 'fastlink-main-button-container';
                buttonContainer.style.display = 'inline-block';
                buttonContainer.style.marginLeft = '4px';
                buttonContainer.style.position = 'relative';
                // Use a button styled like Ant Design, with dynamic background color
                let btnBg = settingsManager.get('fastlinkButtonBg') || '#fff';
                let btnText = settingsManager.get('fastlinkButtonText') || '#333';
                if (settingsManager.get('fastlinkButtonRandom')) {
                    // Get all preset color pairs except random
                    const presetColors = [
                        {bg:'#1677ff', fg:'#fff'},
                        {bg:'#52c41a', fg:'#fff'},
                        {bg:'#faad14', fg:'#fff'},
                        {bg:'#f5222d', fg:'#fff'},
                        {bg:'#595959', fg:'#fff'},
                        {bg:'#fff', fg:'#1677ff'},
                        {bg:'#fff', fg:'#333'},
                        {bg:'#722ed1', fg:'#fff'},
                        {bg:'#13c2c2', fg:'#fff'},
                        {bg:'#eb2f96', fg:'#fff'},
                        {bg:'#fadb14', fg:'#222'},
                        {bg:'#36cfc9', fg:'#fff'}
                    ];
                    const randomPreset = presetColors[Math.floor(Math.random() * presetColors.length)];
                    btnBg = randomPreset.bg;
                    btnText = randomPreset.fg;
                    // Save the chosen random color for this session
                    settingsManager.set('fastlinkButtonBg', btnBg);
                    settingsManager.set('fastlinkButtonText', btnText);
                }
                buttonContainer.innerHTML = `<button type="button" class="ant-btn ant-btn-default ant-btn-color-default ant-btn-variant-outlined mfy-button fastlink-menu-btn" style="background: ${btnBg}; color: ${btnText};"><span role="img" aria-label="menu" class="anticon anticon-menu"><svg viewBox="64 64 896 896" focusable="false" data-icon="menu" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M120 300h720v60H120zm0 180h720v60H120zm0 180h720v60H120z"></path></svg></span><span>秒传</span></button>`; 
                const dropdownMenu = document.createElement('div'); 
                dropdownMenu.id = 'fastlink-dropdown-menu-container'; 
                dropdownMenu.style.display = 'none';
                dropdownMenu.style.position = 'absolute';
                dropdownMenu.style.top = 'calc(100% + 5px)';
                dropdownMenu.style.right = '0';
                dropdownMenu.style.zIndex = '1000';
                dropdownMenu.style.minWidth = '180px';
                dropdownMenu.style.fontSize = '14px';
                dropdownMenu.style.borderRadius = '8px';
                dropdownMenu.style.boxShadow = '0 4px 12px rgba(0,0,0,0.12)';
                dropdownMenu.style.background = '#fff';
                dropdownMenu.style.border = '1px solid #e4e7ed';
                dropdownMenu.innerHTML = `
                    <ul class="ant-dropdown-menu ant-dropdown-menu-root ant-dropdown-menu-vertical ant-dropdown-menu-light" role="menu" tabindex="0" data-menu-list="true" style="border-radius: 10px;">
                        <li id="fastlink-generateShare" class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" style="padding: 5px 12px;">🔗 生成链接 (选中项)</li>
                        <li id="fastlink-generateFromPublicShare" class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" style="padding: 5px 12px;">🌐 从分享链接生成</li>
                        <li id="fastlink-generateFromPublicShareBatch" class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" style="padding: 5px 12px;">📦 从分享链接批量生成</li>
                        <li class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="separator" style="border-top: 1px solid #eee; margin: 3px 0; padding: 0;"></li>
                        <li id="fastlink-receiveDirect" class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" style="padding: 5px 12px;">📥 链接/文件转存</li>                    
                        <li class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="separator" style="border-top: 1px solid #eee; margin: 3px 0; padding: 0;"></li>
                        <li id="fastlink-filterSettings" class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" style="padding: 5px 12px;">🔍 元数据过滤设置</li>
                        <li id="fastlink-convert123Share" class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" style="padding: 5px 12px;">🔄 转换.123share文件</li>
                        <li id="fastlink-settings" class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" style="padding: 5px 12px;">⚙️ 设置</li>
                    </ul>`;
                this.dropdownMenuElement = dropdownMenu;
                // Only open menu when clicking the button, not the whole container
                const menuBtn = buttonContainer.querySelector('.fastlink-menu-btn');
                menuBtn.addEventListener('click', (e) => { e.stopPropagation(); dropdownMenu.style.display = dropdownMenu.style.display === 'none' ? 'block' : 'none'; });
                document.addEventListener('click', (e) => { if (this.dropdownMenuElement && !buttonContainer.contains(e.target) && !this.dropdownMenuElement.contains(e.target)) { if (this.dropdownMenuElement.style.display !== 'none') this.dropdownMenuElement.style.display = 'none'; } });
                dropdownMenu.querySelector('#fastlink-generateShare').addEventListener('click', async (e) => { e.stopPropagation(); dropdownMenu.style.display = 'none'; await coreLogic.generateShareLink(); });
                dropdownMenu.querySelector('#fastlink-generateFromPublicShare').addEventListener('click', (e) => { e.stopPropagation(); dropdownMenu.style.display = 'none'; this.showModal("🌐 从分享链接中生成链接", "", 'inputPublicShare'); });
                dropdownMenu.querySelector('#fastlink-generateFromPublicShareBatch').addEventListener('click', (e) => { e.stopPropagation(); dropdownMenu.style.display = 'none'; this.showModal("🌐 从分享链接批量生成链接", "", 'inputPublicShareBatch'); });
                dropdownMenu.querySelector('#fastlink-receiveDirect').addEventListener('click', (e) => { e.stopPropagation(); dropdownMenu.style.display = 'none'; this.showModal("📥 文件转存/粘贴链接", "", 'inputLink'); });
                dropdownMenu.querySelector('#fastlink-filterSettings').addEventListener('click', (e) => { e.stopPropagation(); dropdownMenu.style.display = 'none'; this.showModal("🔍 元数据过滤设置", "", 'filterSettings'); });
                dropdownMenu.querySelector('#fastlink-convert123Share').addEventListener('click', (e) => {
                    e.stopPropagation();
                    dropdownMenu.style.display = 'none';
                    const input = document.createElement('input');
                    input.type = 'file';
                    input.accept = '.123share,.json';
                    input.onchange = async (event) => {
                        const file = event.target.files[0];
                        if (file) {
                            try {
                                const content = await new Promise((resolve, reject) => {
                                    const reader = new FileReader();
                                    reader.onload = (e) => resolve(e.target.result);
                                    reader.onerror = (e) => reject(e);
                                    reader.readAsText(file);
                                });
                                if (file.name.endsWith('.123share')) {
                                    const jsonData = convert123ShareToJson(content);
                                    if (jsonData) {
                                        this._downloadToFile(JSON.stringify(jsonData, null, 2), file.name.replace('.123share', '.json'), 'application/json');
                                        this.showAlert("转换成功!JSON文件已开始下载。");
                                    } else {
                                        this.showError("转换失败:无法解析.123share文件。");
                                    }
                                } else if (file.name.endsWith('.json')) {
                                    // Convert JSON to .123share
                                    try {
                                        const jsonData = JSON.parse(content);
                                        const shareContent = convertJsonTo123Share(jsonData);
                                        if (shareContent) {
                                            this._downloadToFile(shareContent, file.name.replace('.json', '.123share'), 'text/plain');
                                            this.showAlert("转换成功!.123share文件已开始下载。");
                                        } else {
                                            this.showError("转换失败:无法生成.123share文件。");
                                        }
                                    } catch (e) {
                                        this.showError(`转换失败:${e.message}`);
                                    }
                                } else {
                                    this.showError("请选择.123share或.json文件。");
                                }
                            } catch (e) {
                                console.error(`[${SCRIPT_NAME}] Error processing file:`, e);
                                this.showError(`处理文件时发生错误: ${e.message}`);
                            }
                        }
                    };
                    input.click();
                });
                dropdownMenu.querySelector('#fastlink-settings').addEventListener('click', (e) => {
                    e.stopPropagation();
                    dropdownMenu.style.display = 'none';
                    this.showSettingsDialog();
                });
                // Insert after the upload button using its class selector
                let inserted = false;
                const uploadBtn = targetElement.querySelector('.upload-button');
                if (uploadBtn && uploadBtn.parentNode) {
                    uploadBtn.parentNode.insertBefore(buttonContainer, uploadBtn.nextSibling);
                    inserted = true;
                }
                if (!inserted) {
                    targetElement.appendChild(buttonContainer);
                }
                // Always append the dropdownMenu to the buttonContainer
                buttonContainer.appendChild(dropdownMenu);
                console.log(`[${SCRIPT_NAME}] 秒传按钮已添加。`); 
                return true; 
            } else { 
                console.warn(`[${SCRIPT_NAME}] 目标按钮区域 '${DOM_SELECTORS.TARGET_BUTTON_AREA}' 未找到。`); 
                return false; 
            }
        },
        showModal: function(title, content, type = 'info', closable = true, pureLinkForClipboard = null, jsonDataForExport = null, preprocessingFailuresForLog = null) {
            const isOperationalModal = (t) => ['progress_stoppable', 'inputLink', 'inputPublicShare', 'filterSettings', 'showLink'].includes(t);

            if (this.modalElement && this.activeModalOperationType && this.activeModalOperationType !== type && isOperationalModal(this.activeModalOperationType) && isOperationalModal(type) ) {
                console.log(`[${SCRIPT_NAME}] Hiding active modal ('${this.activeModalOperationType}') for new modal ('${type}').`);
                if (this.modalHideCallback) { this.modalHideCallback(); this.modalHideCallback = null; }
                this.modalElement.style.display = 'none';
            } else if (this.modalElement && type !== 'info' && type !== 'error' && this.activeModalOperationType !== type) { // Avoid hiding info/error for another of same, ensure different op type
                this.hideModal();
            }

            if (this.modalElement && this.modalElement.style.display === 'none' && this.activeModalOperationType === type && isOperationalModal(type)) {
                this.modalElement.style.display = 'flex';
                const titleEl = this.modalElement.querySelector('.fastlink-modal-title');
                if (titleEl) titleEl.textContent = title;
                 // If it's a progress_stoppable modal being reshown, ensure buttons are in correct state
                if (type === 'progress_stoppable') {
                    const stopBtnInstance = this.modalElement.querySelector(`#${processStateManager.getStopButtonId()}`);
                    const cancelBtnInstance = this.modalElement.querySelector('#fl-m-cancel.close-btn');
                    if (stopBtnInstance) {
                        stopBtnInstance.textContent = processStateManager.isStopRequested() ? "正在停止..." : "🛑 停止";
                        stopBtnInstance.disabled = processStateManager.isStopRequested();
                    }
                    if (cancelBtnInstance) {
                        cancelBtnInstance.textContent = processStateManager.isStopRequested() ? "关闭" : "隐藏";
                        cancelBtnInstance.disabled = !processStateManager.isStopRequested() && type === 'progress_stoppable' && !this.modalElement.querySelector(`#${processStateManager.getStopButtonId()}`)?.disabled ; // Disable hide if stop is active
                    }
                }
                return;
            } else if (this.modalElement && this.modalElement.style.display === 'none' && this.activeModalOperationType !== type) {
                this.hideModal();
            }

            this.modalElement = document.createElement('div'); this.modalElement.className = 'fastlink-modal';
            if (type === 'filterSettings') this.modalElement.className += ' filter-dialog';

            let htmlContent = `<div class="fastlink-modal-title">${title}</div><div id="${this.MODAL_CONTENT_ID}" class="fastlink-modal-content">`;
            if (type === 'inputLink') { htmlContent += `<div id="fl-m-drop-area" class="fastlink-drag-drop-area"><textarea id="fl-m-link-input" class="fastlink-modal-input" placeholder="🔗 粘贴秒传链接 或 📂 将文件拖放到此处..." style="min-height: 60px;">${content|| ''}</textarea><div id="fl-m-file-drop-status" style="font-size:0.9em; color:#28a745; margin-top:5px; margin-bottom:5px; min-height:1.2em;"></div><div class="fastlink-file-input-container"><label for="fl-m-file-input">或通过选择文件导入:</label><input type="file" id="fl-m-file-input" accept=".json,.123share" class="fastlink-modal-file-input"></div></div><div class="folder-selector-container"><label for="fl-folder-selector" class="folder-selector-label">目标文件夹路径 (可选):</label><div class="folder-selector-input-container"><input type="text" id="fl-folder-selector" class="folder-selector-input" placeholder="输入目标文件夹路径,如: 电影/漫威"><div id="fl-folder-dropdown" class="folder-selector-dropdown"></div></div><div id="fl-selected-folders" class="folder-tag-container"></div></div><div class="folder-selector-container"><label for="fl-path-prefix" class="folder-selector-label">文件路径前缀过滤 (可选):</label><div class="folder-selector-input-container"><input type="text" id="fl-path-prefix" class="folder-selector-input" placeholder="输入路径前缀,如: 电影/漫威/2023"></div></div><div class="folder-selector-container"><label class="folder-selector-label">文件范围 (可选):</label><div class="folder-selector-input-container" style="display: flex; gap: 10px;"><input type="number" id="fl-start-index" class="folder-selector-input" placeholder="起始序号" min="1" style="width: 100px;"><input type="number" id="fl-end-index" class="folder-selector-input" placeholder="结束序号" min="1" style="width: 100px;"></div></div>`; }
            else if (type === 'inputPublicShare') { htmlContent += `<input type="text" id="fl-m-public-share-key" class="fastlink-modal-input" placeholder="🔑 分享Key 或 完整分享链接"><input type="text" id="fl-m-public-share-pwd" class="fastlink-modal-input" placeholder="🔒 提取码 (如有)"><input type="text" id="fl-m-public-share-fid" class="fastlink-modal-input" value="0" placeholder="📁 起始文件夹ID (默认0为根目录)">`; }
            else if (type === 'inputPublicShareBatch') { htmlContent += `<textarea id="fl-m-public-share-batch-input" class="fastlink-modal-textarea" placeholder="🔗 请输入多个分享链接,每行一个&#10;支持格式:&#10;1. 完整分享链接 (如 https://www.123pan.com/s/xxx)&#10;2. 分享Key (如 xxx)&#10;3. 带提取码的链接 (如 https://www.123pan.com/s/xxx?pwd=yyy)&#10;4. 带提取码的文本 (如 xxx 提取码:yyy)"></textarea>`; }
            else if (type === 'filterSettings') { htmlContent += filterManager.buildFilterModalContent(); }
            else htmlContent += content;
            htmlContent += `</div><div class="fastlink-modal-buttons">`;
            if (type === 'inputLink') { htmlContent += `<button id="fl-m-confirm" class="confirm-btn">➡️ 转存</button><button id="fl-m-cancel" class="cancel-btn">取消</button>`; }
            else if (type === 'inputPublicShare') { htmlContent += `<button id="fl-m-generate-public" class="confirm-btn">✨ 生成</button><button id="fl-m-cancel" class="cancel-btn">取消</button>`; }
            else if (type === 'inputPublicShareBatch') { htmlContent += `<button id="fl-m-generate-public-batch" class="confirm-btn">✨ 批量生成</button><button id="fl-m-cancel" class="cancel-btn">取消</button>`; }
            else if (type === 'filterSettings') { htmlContent += `<button id="fl-m-save-filters" class="confirm-btn">💾 保存设置</button><button id="fl-m-cancel" class="cancel-btn">取消</button>`; }
            else if (type === 'showLink') { if (pureLinkForClipboard || jsonDataForExport) { htmlContent += `<button id="fl-m-copy" class="copy-btn">📋 复制链接</button>`; if (jsonDataForExport) htmlContent += `<button id="fl-m-export-json" class="export-btn">📄 导出为 JSON</button>`; } htmlContent += `<button id="fl-m-cancel" class="close-btn">关闭</button>`; }
            else if (type === 'progress_stoppable') { htmlContent += `<button id="${processStateManager.getStopButtonId()}" class="stop-btn">🛑 停止</button><button id="${processStateManager.getPauseButtonId()}" class="pause-btn" style="margin-left: 5px;">⏸️ 暂停</button><button id="fl-m-minimize" class="minimize-btn" style="margin-left: 5px;">最小化</button><button id="fl-m-cancel" class="close-btn" ${processStateManager.isStopRequested() ? '' : 'disabled'}>关闭</button>`; }
            else if (type === 'info_with_buttons' && preprocessingFailuresForLog && preprocessingFailuresForLog.length > 0) { htmlContent += `<button id="fl-m-copy-preprocessing-log" class="copy-btn">📋 复制日志</button><button id="fl-m-cancel" class="close-btn" style="margin-left:10px;">关闭</button>`; }
            else { htmlContent += `<button id="fl-m-cancel" class="close-btn">关闭</button>`; }
            htmlContent += `</div><div class="fastlink-modal-version">v${SCRIPT_VERSION}</div>`;
            this.modalElement.innerHTML = htmlContent; document.body.appendChild(this.modalElement);

            if (isOperationalModal(type)) this.activeModalOperationType = type; else this.activeModalOperationType = null;

             const confirmBtn = this.modalElement.querySelector('#fl-m-confirm');
             if(confirmBtn) {
                    confirmBtn.onclick = async () => {
                        const linkInputEl = this.modalElement.querySelector(`#fl-m-link-input`);
                        const fileInputEl = this.modalElement.querySelector(`#fl-m-file-input`);
                        const folderSelectorEl = this.modalElement.querySelector(`#fl-folder-selector`);
                        const startIndexEl = this.modalElement.querySelector(`#fl-start-index`);
                        const endIndexEl = this.modalElement.querySelector(`#fl-end-index`);
                        const dropArea = this.modalElement.querySelector('#fl-m-drop-area');
                        let link = linkInputEl ? linkInputEl.value.trim() : null;
                        let file = fileInputEl && fileInputEl.files && fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;
                        let targetFolderPath = folderSelectorEl ? folderSelectorEl.value.trim() : "";
                        let startIndex = startIndexEl && startIndexEl.value.trim() ? parseInt(startIndexEl.value) : null;
                        let endIndex = endIndexEl && endIndexEl.value.trim() ? parseInt(endIndexEl.value) : null;
                        
                        // Validate range if provided
                        if (startIndex !== null && startIndex <1) {
                            this.showAlert("起始序号必须大于等于1");
                            return;                            
                        }
                        if (endIndex !== null && endIndex < 1) {
                            this.showAlert("结束序号必须大于等于1");
                            return;
                        }
                        if (startIndex !== null && endIndex !== null && endIndex < startIndex) {
                            this.showAlert("结束序号必须大于等于起始序号");
                            return;
                        }
                         
                        confirmBtn.disabled = true;
                        this.modalElement.querySelector('#fl-m-cancel')?.setAttribute('disabled', 'true');

                        try {
                            if (dropArea && dropArea.dataset.fileContent) {
                                // Process dropped file
                                let jsonData;
                                if (dropArea.dataset.fileType === '.123share') {
                                    jsonData = convert123ShareToJson(dropArea.dataset.fileContent);
                                } else {
                                    jsonData = JSON.parse(dropArea.dataset.fileContent);
                                }
                                await coreLogic.transferImportedJsonData(jsonData, targetFolderPath, startIndex, endIndex);
                                //this.closeModal();
                            } else if (file) {
                                // Process selected file
                                processStateManager.appendInfoMessage(`ℹ️ 从文件 "${file.name}" 导入...`);
                                const fileContent = await file.text();
                                const jsonData = JSON.parse(fileContent);
                                await coreLogic.transferImportedJsonData(jsonData, targetFolderPath, startIndex, endIndex);
                            } else if (link) {
                                await coreLogic.transferFromShareLink(link, targetFolderPath, startIndex, endIndex);
                            } else {
                                this.showAlert("请输入链接或选择/拖放文件");
                            }
                        } catch (e) {
                            console.error(`[${SCRIPT_NAME}] 处理失败:`, e);
                            processStateManager.appendErrorMessage(`❌ 处理失败: ${e.message}`);
                            uiManager.showError(`处理失败: ${e.message}`);
                        } finally {
                            if(this.modalElement && confirmBtn) {
                                confirmBtn.disabled = false;
                                this.modalElement.querySelector('#fl-m-cancel')?.removeAttribute('disabled');
                            }
                        }
                    };
                }
             const saveFiltersBtn = this.modalElement.querySelector('#fl-m-save-filters'); if(saveFiltersBtn){ saveFiltersBtn.onclick = () => { if(filterManager.saveSettings()){ this.showAlert("✅ 过滤器设置已保存!", 1500); this.hideModal(); } else { this.showError("❌ 保存过滤器设置失败!"); } }; }
             if(type === 'filterSettings'){ filterManager.attachFilterEvents(); }
             if (type === 'inputLink') { const dropArea = this.modalElement.querySelector('#fl-m-drop-area'); const fileInputEl = this.modalElement.querySelector(`#fl-m-file-input`); const linkInputEl = this.modalElement.querySelector('#fl-m-link-input'); const statusDiv = this.modalElement.querySelector('#fl-m-file-drop-status'); if (dropArea && fileInputEl && linkInputEl && statusDiv) { linkInputEl.addEventListener('input', () => { if (linkInputEl.value.trim() !== '') { if (fileInputEl.files && fileInputEl.files.length > 0) fileInputEl.value = ''; statusDiv.textContent = ''; } });
linkInputEl.addEventListener('paste', async (event) => {
    const pastedData = (event.clipboardData || window.clipboardData).getData('text');
    if (!pastedData) return;

    let jsonData;
    let successfullyParsed = false;
    let finalPastedJsonString = pastedData;
    let isAlternativeFormat = false;

    // Display truncated content in input box if it's too long
    if (pastedData.length > 2000) {
        const truncatedContent = pastedData.substring(0, 2000) + '...';
        linkInputEl.value = truncatedContent;
        this.showAlert("JSON内容已截断显示(超过2000字符)", 3000);
    } else {
        linkInputEl.value = pastedData;
    }

    // Attempt 1: Try to parse as-is (complete JSON object/array)
    try {
        jsonData = JSON.parse(pastedData);
        // Check for alternative format first
        const convertedData = convertAlternativeJsonFormat(jsonData);
        if (convertedData) {
            jsonData = convertedData;
            finalPastedJsonString = JSON.stringify(convertedData, null, 2);
            successfullyParsed = true;
            isAlternativeFormat = true;
        } else if (typeof jsonData === 'object' && jsonData !== null) {
            if (Array.isArray(jsonData.files) || Array.isArray(jsonData)) {
                successfullyParsed = true;
                // If it's just an array of files, wrap it for consistency with expected structure
                if (Array.isArray(jsonData) && !jsonData.every(item => typeof item === 'object' && item.path && item.size && item.etag)) {
                    // If it's an array but not of file objects, it's not what we want for snippet processing
                    successfullyParsed = false; 
                } else if (Array.isArray(jsonData)) {
                    jsonData = { files: jsonData }; // Wrap the array
                    finalPastedJsonString = JSON.stringify(jsonData); // Re-stringify the wrapped version
                }
            }
        }
    } catch (e) { /* ignore, try next */ }

    // Attempt 2: Try to parse as a snippet (array of objects) by wrapping it
    if (!successfullyParsed) {
        try {
            const wrappedPastedData = `[${pastedData}]`;
            const tempJsonData = JSON.parse(wrappedPastedData);
            // Check for alternative format in wrapped data
            const convertedData = convertAlternativeJsonFormat(tempJsonData);
            if (convertedData) {
                jsonData = convertedData;
                finalPastedJsonString = JSON.stringify(convertedData, null, 2);
                successfullyParsed = true;
                isAlternativeFormat = true;
            } else if (Array.isArray(tempJsonData) && tempJsonData.every(item => typeof item === 'object' && item.path && item.size && item.etag)) {
                jsonData = { files: tempJsonData };
                finalPastedJsonString = JSON.stringify(jsonData);
                successfullyParsed = true;
            }
        } catch (e) { /* ignore, try next */ }
    }

    // Attempt 3: Try to parse as a snippet with a trailing comma
    if (!successfullyParsed && pastedData.trim().endsWith(',')) {
        try {
            const trimmedPastedData = pastedData.trim().slice(0, -1);
            const wrappedPastedData = `[${trimmedPastedData}]`;
            const tempJsonData = JSON.parse(wrappedPastedData);
            // Check for alternative format in wrapped data
            const convertedData = convertAlternativeJsonFormat(tempJsonData);
            if (convertedData) {
                jsonData = convertedData;
                finalPastedJsonString = JSON.stringify(convertedData, null, 2);
                successfullyParsed = true;
                isAlternativeFormat = true;
            } else if (Array.isArray(tempJsonData) && tempJsonData.every(item => typeof item === 'object' && item.path && item.size && item.etag)) {
                jsonData = { files: tempJsonData };
                finalPastedJsonString = JSON.stringify(jsonData);
                successfullyParsed = true;
            }
        } catch (e) { /* ignore */ }
    }

    if (successfullyParsed && jsonData && Array.isArray(jsonData.files) && jsonData.files.length > 0) {
        event.preventDefault(); // Prevent default paste behavior
        const jsonFile = new File([finalPastedJsonString], "pasted_snippet.json", { type: "application/json" });
        const dataTransfer = new DataTransfer();
        dataTransfer.items.add(jsonFile);
        fileInputEl.files = dataTransfer.files;
        statusDiv.textContent = isAlternativeFormat ? 
            `已粘贴并转换JSON片段为标准格式 (pasted_snippet.json)。请点击下方"转存"按钮。` :
            `已粘贴JSON片段并处理为 (pasted_snippet.json)。请点击下方"转存"按钮。`;
    } else {
        // If not successfully parsed as a file list or snippet, let it paste normally
        console.log("[FastLink] Pasted data was not recognized as a valid JSON file list or snippet.");
    }
});
fileInputEl.addEventListener('change', async () => {
    if (fileInputEl.files && fileInputEl.files.length > 0) {
        const file = fileInputEl.files[0];
        try {
            const reader = new FileReader();
            reader.onload = (event) => {
                let content = event.target.result;
                let jsonData;
                let isAlternativeFormat = false;
                
                try {
                    jsonData = JSON.parse(content);
                    const convertedData = convertAlternativeJsonFormat(jsonData);
                    if (convertedData) {
                        jsonData = convertedData;
                        content = JSON.stringify(convertedData, null, 2);
                        isAlternativeFormat = true;
                    }
                } catch (e) {
                    // If parsing fails, use original content
                    console.log("[FastLink] File content parsing failed:", e);
                }

                // Store the processed content for processing
                dropArea.dataset.fileContent = content;
                dropArea.dataset.fileType = file.name.endsWith('.123share') ? '.123share' : '.json';
                
                // Display truncated content in status if it's too long
                if (content.length > 2000) {
                    const truncatedContent = content.substring(0, 2000) + '...';
                    statusDiv.textContent = `已选择文件: ${file.name}${isAlternativeFormat ? ' (已转换为标准格式)' : ''} (内容已截断显示,超过2000字符)。请点击下方"转存"按钮。`;
                    linkInputEl.value = truncatedContent;
                } else {
                    statusDiv.textContent = `已选择文件: ${file.name}${isAlternativeFormat ? ' (已转换为标准格式)' : ''}。请点击下方"转存"按钮。`;
                    linkInputEl.value = content;
                }
            };
            reader.readAsText(file);
        } catch (err) {
            console.error("Error processing file:", err);
            this.showAlert("处理文件时发生错误。");
        }
    } else {
        statusDiv.textContent = '';
    }
});
fileInputEl.addEventListener('change', () => { if (fileInputEl.files && fileInputEl.files.length > 0) { statusDiv.textContent = `已选中文件: ${fileInputEl.files[0].name}。请点击下方"转存"按钮。`; if(linkInputEl) linkInputEl.value = ''; } else statusDiv.textContent = ''; }); ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => dropArea.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); }, false)); ['dragenter', 'dragover'].forEach(eventName => dropArea.addEventListener(eventName, () => dropArea.classList.add('drag-over-active'), false)); ['dragleave', 'drop'].forEach(eventName => dropArea.addEventListener(eventName, () => dropArea.classList.remove('drag-over-active'), false)); dropArea.addEventListener('drop', (e) => {
                    const dt = e.dataTransfer;
                    if (dt && dt.files && dt.files.length > 0) {
                        const droppedFile = dt.files[0];
                        if (droppedFile.name.endsWith('.json') || droppedFile.name.endsWith('.123share') || droppedFile.type === 'application/json') {
                            try {
                                const reader = new FileReader();
                                reader.onload = (event) => {
                                    let content = event.target.result;
                                    let jsonData;
                                    let isAlternativeFormat = false;
                                    
                                    try {
                                        jsonData = JSON.parse(content);
                                        const convertedData = convertAlternativeJsonFormat(jsonData);
                                        if (convertedData) {
                                            jsonData = convertedData;
                                            content = JSON.stringify(convertedData, null, 2);
                                            isAlternativeFormat = true;
                                        }
                                    } catch (e) {
                                        // If parsing fails, use original content
                                        console.log("[FastLink] File content parsing failed:", e);
                                    }

                                    // Store the processed content for processing
                                    dropArea.dataset.fileContent = content;
                                    dropArea.dataset.fileType = droppedFile.name.endsWith('.123share') ? '.123share' : '.json';
                                    
                                    // Display truncated content in status if it's too long
                                    if (content.length > 2000) {
                                        const truncatedContent = content.substring(0, 2000) + '...';
                                        statusDiv.textContent = `已选择文件: ${droppedFile.name}${isAlternativeFormat ? ' (已转换为标准格式)' : ''} (内容已截断显示,超过2000字符)。请点击下方"转存"按钮。`;
                                        linkInputEl.value = truncatedContent;
                                    } else {
                                        statusDiv.textContent = `已选择文件: ${droppedFile.name}${isAlternativeFormat ? ' (已转换为标准格式)' : ''}。请点击下方"转存"按钮。`;
                                        linkInputEl.value = content;
                                    }
                                };
                                reader.readAsText(droppedFile);
                            } catch (err) {
                                console.error("Error processing file:", err);
                                this.showAlert("处理文件时发生错误。");
                            }
                        } else {
                            this.showAlert("请拖放 .json 或 .123share 文件。");
                        }
                    }
                }, false); } const folderSelector = this.modalElement.querySelector('#fl-folder-selector'); const folderDropdown = this.modalElement.querySelector('#fl-folder-dropdown'); if (folderSelector && folderDropdown) { folderSelector.addEventListener('click', function() { folderDropdown.classList.toggle('active'); }); folderSelector.addEventListener('blur', function() { setTimeout(() => { folderDropdown.classList.remove('active'); }, 200); }); /* Other folder selector events... */ } }
             const generatePublicBtn = this.modalElement.querySelector('#fl-m-generate-public'); if(generatePublicBtn){ generatePublicBtn.onclick = async () => { const shareKeyEl = this.modalElement.querySelector('#fl-m-public-share-key'); const sharePwdEl = this.modalElement.querySelector('#fl-m-public-share-pwd'); const shareFidEl = this.modalElement.querySelector('#fl-m-public-share-fid'); const rawShareKeyInput = shareKeyEl ? shareKeyEl.value.trim() : null; let sharePwd = sharePwdEl ? sharePwdEl.value.trim() : null; const shareFid = shareFidEl ? shareFidEl.value.trim() : "0"; let finalShareKey = rawShareKeyInput; if (rawShareKeyInput) { if (rawShareKeyInput.includes('/s/')) { try { let url; try { url = new URL(rawShareKeyInput); } catch (e) { if (!rawShareKeyInput.startsWith('http')) url = new URL('https://' + rawShareKeyInput); else throw e; } const pathSegments = url.pathname.split('/'); const sIndex = pathSegments.indexOf('s'); if (sIndex !== -1 && pathSegments.length > sIndex + 1) { finalShareKey = pathSegments[sIndex + 1]; const searchParams = new URLSearchParams(url.search); const possiblePwdParams = ['pwd', '提取码', 'password', 'extract', 'code']; for (const paramName of possiblePwdParams) { if (searchParams.has(paramName)) { const urlPwd = searchParams.get(paramName); if (urlPwd && (!sharePwd || sharePwd.length === 0)) { sharePwd = urlPwd; if (sharePwdEl) sharePwdEl.value = sharePwd; } break; } } if ((!sharePwd || sharePwd.length === 0)) { const fullUrl = rawShareKeyInput; const pwdRegexes = [ /[?&]提取码[:=]([A-Za-z0-9]+)/, /提取码[:=]([A-Za-z0-9]+)/, /[?&]pwd[:=]([A-Za-z0-9]+)/, /[?&]password[:=]([A-Za-z0-9]+)/, /提取码[::]([A-Za-z0-9]+)/, /[?&]提取码[::]([A-Za-z0-9]+)/ ]; for (const regex of pwdRegexes) { const match = fullUrl.match(regex); if (match && match[1]) { sharePwd = match[1]; if (sharePwdEl) sharePwdEl.value = sharePwd; break; } } } } else { let pathAfterS = rawShareKeyInput.substring(rawShareKeyInput.lastIndexOf('/s/') + 3); finalShareKey = pathAfterS.split(/[/?#]/)[0]; } } catch (e) { let pathAfterS = rawShareKeyInput.substring(rawShareKeyInput.lastIndexOf('/s/') + 3); finalShareKey = pathAfterS.split(/[/?#]/)[0]; if (!sharePwd || sharePwd.length === 0) { const pwdMatch = rawShareKeyInput.match(/提取码[:=]([A-Za-z0-9]+)/); if (pwdMatch && pwdMatch[1]) { sharePwd = pwdMatch[1]; if (sharePwdEl) sharePwdEl.value = sharePwd; } } console.warn(`[${SCRIPT_NAME}] 分享链接解析失败: ${e.message}`); } } if (finalShareKey && finalShareKey.includes('自定义')) finalShareKey = finalShareKey.split('自定义')[0]; } if (!finalShareKey) { this.showAlert("请输入有效的分享Key或分享链接。"); return; } if (isNaN(parseInt(shareFid))) { this.showAlert("起始文件夹ID必须是数字。"); return; } generatePublicBtn.disabled = true; this.modalElement.querySelector('#fl-m-cancel')?.setAttribute('disabled', 'true'); await coreLogic.generateLinkFromPublicShare(finalShareKey, sharePwd, shareFid); if(this.modalElement && generatePublicBtn){ generatePublicBtn.disabled = false; this.modalElement.querySelector('#fl-m-cancel')?.removeAttribute('disabled');} };}
             const generatePublicBatchBtn = this.modalElement.querySelector('#fl-m-generate-public-batch');
            if(generatePublicBatchBtn) {
                generatePublicBatchBtn.onclick = async () => {
                    const batchInputEl = this.modalElement.querySelector('#fl-m-public-share-batch-input');
                    if (!batchInputEl || !batchInputEl.value.trim()) {
                        this.showAlert("请输入至少一个分享链接。");
                        return;
                    }

                    const lines = batchInputEl.value.trim().split('\n').filter(line => line.trim());
                    if (lines.length === 0) {
                        this.showAlert("请输入至少一个分享链接。");
                        return;
                    }

                    generatePublicBatchBtn.disabled = true;
                    this.modalElement.querySelector('#fl-m-cancel')?.setAttribute('disabled', 'true');

                    let successCount = 0;
                    let failureCount = 0;
                    let totalFiles = 0;
                    let totalSize = 0;

                    for (const line of lines) {
                        if (!line.trim()) continue;
                        const { shareKey, sharePwd } = extractShareInfoFromText(line);
                        if (!shareKey) {
                            processStateManager.appendErrorMessage(`❌ 无效的分享链接: ${line}`);
                            failureCount++;
                            continue;
                        }

                        try {
                            const result = await coreLogic.generateLinkFromPublicShare(shareKey, sharePwd, "0", true);
                            if (result && result.jsonData) {
                                // Generate filename for this share
                                let fileName;
                                if (settingsManager.get('useFolderNameForJson')) {
                                    const firstPathSegment = (result.jsonData.commonPath || '').split('/')[0];
                                    const sanitizedPath = firstPathSegment.replace(/[\/:*?"<>|]/g, '_').replace(/^\s+|\s+$/g, '');
                                    if (sanitizedPath) {
                                        if (settingsManager.get('appendDateToJson')) {
                                            const now = new Date();
                                            const dateStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
                                            fileName = `${sanitizedPath}_${dateStr}.json`;
                                        } else {
                                            fileName = `${sanitizedPath}.json`;
                                        }
                                    } else {
                                        fileName = `123FastLink_${Date.now()}.json`;
                                    }
                                } else {
                                    fileName = `123FastLink_${Date.now()}.json`;
                                }

                                // Silently export JSON
                                this._downloadToFile(JSON.stringify(result.jsonData, null, 2), fileName, 'application/json');

                                // Update totals
                                totalFiles += result.jsonData.totalFilesCount;
                                totalSize += result.jsonData.totalSize;
                                successCount++;
                            } else {
                                processStateManager.appendErrorMessage(`❌ 生成分享链接失败: ${shareKey}`);
                                failureCount++;
                            }
                        } catch (error) {
                            processStateManager.appendErrorMessage(`❌ 处理分享链接时出错: ${shareKey} - ${error.message}`);
                            failureCount++;
                        }
                    }

                    // Show final summary modal
                    const formattedTotalSize = formatBytes(totalSize);
                    const summary = `<div class="fastlink-result">
                        <h3>${failureCount > 0 && successCount > 0 ? "🎯 部分成功" : (successCount > 0 ? "🎉 生成成功" : "🤔 无有效数据")}</h3>
                        <p>📄 已处理分享: ${lines.length} 个</p>
                        <p>✅ 成功生成: ${successCount} 个</p>
                        <p>❌ 失败项目: ${failureCount} 个</p>
                        <p>📦 总文件数: ${totalFiles} 个</p>
                        <p>💾 总大小: ${formattedTotalSize}</p>
                    </div>`;

                    this.showModal("🎉 批量处理完成", summary, 'showLink', true);
                    processStateManager.appendInfoMessage(`\n📊 批量处理完成: 成功 ${successCount} 个, 失败 ${failureCount} 个`);
                    generatePublicBatchBtn.disabled = false;
                    this.modalElement.querySelector('#fl-m-cancel')?.removeAttribute('disabled');
                };
            }

            const copyBtn = this.modalElement.querySelector('#fl-m-copy'); if(copyBtn){ copyBtn.onclick = () => { const textToCopy = pureLinkForClipboard || this.modalElement.querySelector('.fastlink-link-text')?.value; if (textToCopy) { GM_setClipboard(textToCopy); this.showAlert("已复制到剪贴板!");} else this.showError("无法找到链接文本。"); };}
             const exportJsonBtn = this.modalElement.querySelector('#fl-m-export-json'); if(exportJsonBtn && jsonDataForExport){ exportJsonBtn.onclick = () => { try { 
                let fileName;
                if (settingsManager.get('useFolderNameForJson')) {
                    const firstPathSegment = (jsonDataForExport.commonPath || '').split('/')[0];
                    const sanitizedPath = firstPathSegment.replace(/[\/:*?"<>|]/g, '_').replace(/^\s+|\s+$/g, '');
                    if (sanitizedPath) {
                        if (settingsManager.get('appendDateToJson')) {
                            const now = new Date();
                            const dateStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
                            fileName = `${sanitizedPath}_${dateStr}.json`;
                        } else {
                            fileName = `${sanitizedPath}.json`;
                        }
                    } else {
                        fileName = `123FastLink_${Date.now()}.json`;
                    }
                } else {
                    fileName = `123FastLink_${Date.now()}.json`;
                }
                this._downloadToFile(JSON.stringify(jsonDataForExport, null, 2), fileName, 'application/json'); 
                this.showAlert("JSON文件已开始下载!"); 
              } catch (e) { 
                console.error(`[${SCRIPT_NAME}] 导出JSON失败:`, e); 
                this.showError(`导出JSON失败: ${e.message}`); 
              }};}
             const stopBtn = this.modalElement.querySelector(`#${processStateManager.getStopButtonId()}`); if(stopBtn){ stopBtn.onclick = () => { if (confirm("确定要停止当前操作吗?")) { processStateManager.requestStop(); const closeBtnForStop = this.modalElement.querySelector('#fl-m-cancel.close-btn'); if(closeBtnForStop) closeBtnForStop.disabled = false; const minimizeBtnForStop = this.modalElement.querySelector('#fl-m-minimize'); if(minimizeBtnForStop) minimizeBtnForStop.disabled = true; } }; }

             const minimizeBtn = this.modalElement.querySelector('#fl-m-minimize'); // Added for mini progress
             if (minimizeBtn) {
                 minimizeBtn.onclick = () => {
                     if (this.modalElement) this.modalElement.style.display = 'none';
                     this.showMiniProgress();
                     // Ensure the mini progress bar shows current progress immediately
                     processStateManager.updateProgressUINow(); // Call a direct update if needed or rely on next interval
                 };
             }
             const cancelBtn = this.modalElement.querySelector('#fl-m-cancel');
             const finalCloseBtn = this.modalElement.querySelector('#fl-m-final-close');
             if (cancelBtn && type === 'progress_stoppable') {
                cancelBtn.textContent = processStateManager.isStopRequested() ? "关闭" : "隐藏";
                cancelBtn.onclick = () => {
                    if (processStateManager.isStopRequested() || !this.modalElement.querySelector(`#${processStateManager.getStopButtonId()}`)) { this.hideModal(); } // Close if stopped or no stop button
                    else { if (this.modalElement) this.modalElement.style.display = 'none'; if (this.modalHideCallback) { this.modalHideCallback(); this.modalHideCallback = null; } }
                };
             } else if ((cancelBtn || finalCloseBtn) && closable) {
                if (cancelBtn) cancelBtn.onclick = () => this.hideModal();
                if (finalCloseBtn) finalCloseBtn.onclick = () => this.hideModal();
             }
             if(!closable && cancelBtn && type !== 'progress_stoppable') cancelBtn.disabled = true;
             const copyPreprocessingLogBtn = this.modalElement.querySelector('#fl-m-copy-preprocessing-log'); if(copyPreprocessingLogBtn && preprocessingFailuresForLog) { copyPreprocessingLogBtn.onclick = () => { const logText = preprocessingFailuresForLog.map(pf => `文件: ${pf.fileName || (pf.originalEntry&&pf.originalEntry.path)||'未知路径'}\n${(pf.originalEntry&&pf.originalEntry.etag)?('原始ETag: '+pf.originalEntry.etag+'\n'):(pf.etag?'处理后ETag: '+pf.etag+'\n':'')}${(pf.originalEntry&&pf.originalEntry.size)?('大小: '+pf.originalEntry.size+'\n'):(pf.size?'大小: '+pf.size+'\n':'')}错误: ${pf.error||'未知错误'}`).join('\n\n'); GM_setClipboard(logText); this.showAlert("预处理失败日志已复制到剪贴板!", 1500); };}

             const pauseBtn = this.modalElement.querySelector(`#${processStateManager.getPauseButtonId()}`);
             if (pauseBtn && (type === 'progress_stoppable' || closable)) {
                pauseBtn.disabled = false;
                pauseBtn.onclick = () => {
                    processStateManager.togglePause();
                };
             }

            if (type === 'progress_stoppable') { this.modalHideCallback = () => { const stopBtnInstance = this.modalElement?.querySelector(`#${processStateManager.getStopButtonId()}`); if (stopBtnInstance && !processStateManager.isStopRequested()) stopBtnInstance.textContent = "🛑 停止 (后台)"; }; }
             if(type === 'inputLink' || type === 'inputPublicShare'){ const firstInput = this.modalElement.querySelector('input[type="text"], textarea'); if(firstInput) setTimeout(() => firstInput.focus(), 100); }
        },
        enableModalCloseButton: function(enable = true) {
            if (this.modalElement) {
                const closeBtn = this.modalElement.querySelector('#fl-m-cancel.close-btn');
                if (closeBtn) { closeBtn.disabled = !enable; if(enable && this.activeModalOperationType === 'progress_stoppable') closeBtn.textContent = "关闭"; }
                const stopBtn = this.modalElement.querySelector(`#${processStateManager.getStopButtonId()}`);
                if (stopBtn && enable) stopBtn.disabled = true; // If enabling close, typically stop is done
            }
        },
        updateModalContent: function(newContent) { if (this.modalElement) { const ca = this.modalElement.querySelector(`#${this.MODAL_CONTENT_ID}`); if (ca) { if (ca.tagName === 'TEXTAREA' || ca.hasAttribute('contenteditable')) ca.value = newContent; else ca.innerHTML = newContent; ca.scrollTop = ca.scrollHeight;} } },
        hideModal: function() { if (this.modalElement) { this.modalElement.remove(); this.modalElement = null; } this.activeModalOperationType = null; this.modalHideCallback = null; },
        showInfo: function(message, duration = 2000) {
            const infoElement = document.createElement('div');
            infoElement.className = 'fastlink-info-popup';
            infoElement.innerHTML = message;
            document.body.appendChild(infoElement);
            setTimeout(() => {
                infoElement.classList.add('fastlink-info-popup-fadeout');
                setTimeout(() => { if (infoElement.parentNode) infoElement.parentNode.removeChild(infoElement); }, 500); // Wait for fadeout animation
            }, duration);
        },
        showAlert: function(message, duration = 2000) { this.showInfo(message, duration); },
        showError: function(message, duration = 3000) { this.showInfo(`<span style="color: red;">⚠️ ${message}</span>`, duration); },
        getModalElement: function() { return this.modalElement; },
        showSettingsDialog: function() {
            const settingsContent = `
                <div class="settings-dialog">
                    <div class="settings-group">
                        <h4>⚙️ 设置</h4>
                        <div class="settings-item">
                            <input type="checkbox" id="setting-debug-mode" ${settingsManager.get('debugMode') ? 'checked' : ''}>
                            <label for="setting-debug-mode">调试模式</label>
                            <div class="settings-description">启用后将显示更多调试信息</div>
                        </div>
                        <div class="settings-item">
                            <input type="checkbox" id="setting-use-folder-name" ${settingsManager.get('useFolderNameForJson') ? 'checked' : ''}>
                            <label for="setting-use-folder-name">使用文件夹名作为JSON文件名</label>
                            <div class="settings-description">导出JSON时使用文件夹名作为文件名前缀</div>
                        </div>
                        <div class="settings-item">
                            <input type="checkbox" id="setting-append-date" ${settingsManager.get('appendDateToJson') ? 'checked' : ''}>
                            <label for="setting-append-date">在JSON文件名中添加日期</label>
                            <div class="settings-description">导出JSON时在文件名中添加日期信息</div>
                        </div>
                        <div class="settings-item">
                            <input type="checkbox" id="setting-use-base62-etags" ${settingsManager.get('usesBase62EtagsInExport') ? 'checked' : ''}>
                            <label for="setting-use-base62-etags">使用Base62格式的ETag</label>
                            <div class="settings-description">导出JSON时使用Base62格式的ETag(更短的格式�?</div>
                        </div>
                        <div class="settings-item">
                            <input type="color" id="setting-fastlink-btn-bg" value="${settingsManager.get('fastlinkButtonBg') || '#fff'}" style="width: 32px; height: 32px; border: none; margin-right: 8px; vertical-align: middle;">
                            <label for="setting-fastlink-btn-bg">秒传按钮背景色</label>
                            <div class="settings-description">自定义秒传按钮的背景色</div>
                        </div>
                        <div class="settings-item">
                            <input type="color" id="setting-fastlink-btn-text" value="${settingsManager.get('fastlinkButtonText') || '#333'}" style="width: 32px; height: 32px; border: none; margin-right: 8px; vertical-align: middle;">
                            <label for="setting-fastlink-btn-text">秒传按钮文字颜色</label>
                            <div class="settings-description">自定义秒传按钮的文字颜色</div>
                        </div>
                        <div class="settings-item">
                            <div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
                                <span style="margin-right: 8px;">颜色预设:</span>
                                <span class="fastlink-btn-preset" data-bg="#1677ff" data-fg="#fff" title="蓝色"><span style="background:#1677ff;color:#fff;">蓝</span></span>
                                <span class="fastlink-btn-preset" data-bg="#52c41a" data-fg="#fff" title="绿色"><span style="background:#52c41a;color:#fff;">绿</span></span>
                                <span class="fastlink-btn-preset" data-bg="#faad14" data-fg="#fff" title="橙色"><span style="background:#faad14;color:#fff;">橙</span></span>
                                <span class="fastlink-btn-preset" data-bg="#f5222d" data-fg="#fff" title="红色"><span style="background:#f5222d;color:#fff;">红</span></span>
                                <span class="fastlink-btn-preset" data-bg="#595959" data-fg="#fff" title="深灰"><span style="background:#595959;color:#fff;">灰</span></span>
                                <span class="fastlink-btn-preset" data-bg="#fff" data-fg="#1677ff" title="白底蓝字"><span style="background:#fff;color:#1677ff;border:1px solid #1677ff;">白</span></span>
                                <span class="fastlink-btn-preset" data-bg="#fff" data-fg="#333" title="白底黑字"><span style="background:#fff;color:#333;border:1px solid #ccc;">白</span></span>
                                <span class="fastlink-btn-preset" data-bg="#722ed1" data-fg="#fff" title="紫色"><span style="background:#722ed1;color:#fff;">紫</span></span>
                                <span class="fastlink-btn-preset" data-bg="#13c2c2" data-fg="#fff" title="青色"><span style="background:#13c2c2;color:#fff;">青</span></span>
                                <span class="fastlink-btn-preset" data-bg="#eb2f96" data-fg="#fff" title="粉色"><span style="background:#eb2f96;color:#fff;">粉</span></span>
                                <span class="fastlink-btn-preset" data-bg="#fadb14" data-fg="#222" title="亮黄"><span style="background:#fadb14;color:#222;">黄</span></span>
                                <span class="fastlink-btn-preset" data-bg="#36cfc9" data-fg="#fff" title="亮蓝绿"><span style="background:#36cfc9;color:#fff;">蓝绿</span></span>
                                <span class="fastlink-btn-preset fastlink-btn-preset-random" data-bg="random" data-fg="random" title="随机"><span style="background:linear-gradient(90deg,#1677ff,#52c41a,#faad14,#eb2f96,#36cfc9,#722ed1,#fff);color:#333;border:1px solid #ccc;">随机</span></span>
                            </div>
                            <div class="settings-description">点击预设快速应用现代风格配色</div>
                        </div>
                    </div>
                    ${settingsManager.isDebugMode() ? `
                    <div class="settings-group">                        
                        <div class="api-test-container">
                            <h4>🔍 API 测试</h4>
                            <div class="api-test-row">
                                <label for="api-path-select">API 路径:</label>
                                <select id="api-path-select" class="api-path-select">
                                    ${apiTestManager.getApiPathOptions().map(option => 
                                        `<option value="${option.value}">${option.label}</option>`
                                    ).join('')}
                                </select>
                            </div>
                            <div class="api-test-row">
                                <label for="api-method-select">请求方法:</label>
                                <select id="api-method-select" class="api-path-select" style="width:100px;">
                                    <option value="POST">POST</option>
                                    <option value="GET">GET</option>
                                </select>
                            </div>
                            <div id="api-params-container" class="api-params-container">
                                <!-- Parameters will be dynamically added here -->
                            </div>
                            <div class="api-test-row">
                                <label for="api-retry-count">重试次数:</label>
                                <input type="number" id="api-retry-count" class="api-param-input" min="0" max="20" value="3" style="width:80px;">
                            </div>
                            <div class="api-test-row">
                                <button id="api-test-button" class="api-test-button">测试 API</button>
                            </div>
                            <div id="api-test-result" class="api-test-result">
                                <!-- Test results will be shown here -->
                            </div>
                        </div>
                    </div>
                    ` : ''}
                </div>`;

            this.showModal("⚙️ 设置", settingsContent, 'settings', true);

            // Add event listeners for settings
            const debugModeCheckbox = this.modalElement.querySelector('#setting-debug-mode');
            const useFolderNameCheckbox = this.modalElement.querySelector('#setting-use-folder-name');
            const appendDateCheckbox = this.modalElement.querySelector('#setting-append-date');
            const useBase62EtagsCheckbox = this.modalElement.querySelector('#setting-use-base62-etags');
            const fastlinkBtnBgInput = this.modalElement.querySelector('#setting-fastlink-btn-bg');
            const fastlinkBtnTextInput = this.modalElement.querySelector('#setting-fastlink-btn-text');
            const fastlinkBtnPresets = this.modalElement.querySelectorAll('.fastlink-btn-preset');

            if (debugModeCheckbox) {
                debugModeCheckbox.onchange = () => {
                    settingsManager.set('debugMode', debugModeCheckbox.checked);
                    // Refresh the settings dialog to show/hide API test section
                    this.showSettingsDialog();
                };
            }

            if (useFolderNameCheckbox) {
                useFolderNameCheckbox.onchange = () => {
                    settingsManager.set('useFolderNameForJson', useFolderNameCheckbox.checked);
                };
            }

            if (appendDateCheckbox) {
                appendDateCheckbox.onchange = () => {
                    settingsManager.set('appendDateToJson', appendDateCheckbox.checked);
                };
            }

            if (useBase62EtagsCheckbox) {
                useBase62EtagsCheckbox.onchange = () => {
                    settingsManager.set('usesBase62EtagsInExport', useBase62EtagsCheckbox.checked);
                };
            }

            if (fastlinkBtnBgInput) {
                fastlinkBtnBgInput.oninput = () => {
                    settingsManager.set('fastlinkButtonBg', fastlinkBtnBgInput.value);
                    settingsManager.set('fastlinkButtonRandom', false);
                    // Update the button color live if present
                    const btn = document.querySelector('.fastlink-menu-btn');
                    if (btn) btn.style.background = fastlinkBtnBgInput.value;
                };
            }

            if (fastlinkBtnTextInput) {
                fastlinkBtnTextInput.oninput = () => {
                    settingsManager.set('fastlinkButtonText', fastlinkBtnTextInput.value);
                    settingsManager.set('fastlinkButtonRandom', false);
                    // Update the button text color live if present
                    const btn = document.querySelector('.fastlink-menu-btn');
                    if (btn) btn.style.color = fastlinkBtnTextInput.value;
                };
            }

            if (fastlinkBtnPresets && fastlinkBtnPresets.length > 0) {
                // Helper to update selected indicator
                function updatePresetSelectionIndicator(selectedBg, selectedFg, isRandom) {
                    fastlinkBtnPresets.forEach(preset => {
                        const bg = preset.getAttribute('data-bg');
                        const fg = preset.getAttribute('data-fg');
                        if (isRandom) {
                            if (bg === 'random' && fg === 'random') preset.classList.add('fastlink-btn-preset-selected');
                            else preset.classList.remove('fastlink-btn-preset-selected');
                        } else {
                            if (bg === selectedBg && fg === selectedFg) preset.classList.add('fastlink-btn-preset-selected');
                            else preset.classList.remove('fastlink-btn-preset-selected');
                        }
                    });
                }
                // On dialog open, set indicator
                if (settingsManager.get('fastlinkButtonRandom')) {
                    updatePresetSelectionIndicator(null, null, true);
                } else {
                    updatePresetSelectionIndicator(settingsManager.get('fastlinkButtonBg'), settingsManager.get('fastlinkButtonText'), false);
                }
                fastlinkBtnPresets.forEach(preset => {
                    preset.style.cursor = 'pointer';
                    preset.style.display = 'inline-block';
                    preset.style.borderRadius = '4px';
                    preset.style.padding = '2px 8px';
                    preset.style.margin = '2px 0';
                    preset.style.border = '1px solid #eee';
                    preset.addEventListener('click', () => {
                        let bg = preset.getAttribute('data-bg');
                        let fg = preset.getAttribute('data-fg');
                        if (bg === 'random' && fg === 'random') {
                            settingsManager.set('fastlinkButtonRandom', true);
                            updatePresetSelectionIndicator(null, null, true);
                            // On next reload, a random color will be chosen
                        } else {
                            settingsManager.set('fastlinkButtonRandom', false);
                            settingsManager.set('fastlinkButtonBg', bg);
                            settingsManager.set('fastlinkButtonText', fg);
                            updatePresetSelectionIndicator(bg, fg, false);
                            // Update color pickers
                            if (fastlinkBtnBgInput) fastlinkBtnBgInput.value = bg;
                            if (fastlinkBtnTextInput) fastlinkBtnTextInput.value = fg;
                            // Update the button live
                            const btn = document.querySelector('.fastlink-menu-btn');
                            if (btn) {
                                btn.style.background = bg;
                                btn.style.color = fg;
                            }
                        }
                    });
                });
            }

            // Add API test functionality if debug mode is enabled
            if (settingsManager.isDebugMode()) {
                const apiPathSelect = this.modalElement.querySelector('#api-path-select');
                const paramsContainer = this.modalElement.querySelector('#api-params-container');
                const testButton = this.modalElement.querySelector('#api-test-button');
                const resultContainer = this.modalElement.querySelector('#api-test-result');
                const retryCountInput = this.modalElement.querySelector('#api-retry-count');

                function updateParamsFields() {
                    const selectedPath = apiPathSelect.value;
                    const requiredParams = apiTestManager.getRequiredParams(selectedPath);
                    const defaultParams = apiTestManager.getDefaultParams(selectedPath);
                    
                    paramsContainer.innerHTML = requiredParams.map(param => {
                        const defaultValue = defaultParams[param] !== undefined ? defaultParams[param] : '';
                        return `
                            <div class="api-test-row">
                                <label for="param-${param}">${param}:</label>
                                <input type="text" id="param-${param}" class="api-param-input" placeholder="输入 ${param}" value="${defaultValue}">
                            </div>
                        `;
                    }).join('');
                }

                apiPathSelect.onchange = updateParamsFields;
                updateParamsFields();

                testButton.onclick = async () => {
                    const selectedPath = apiPathSelect.value;
                    const selectedMethod = this.modalElement.querySelector('#api-method-select').value;
                    const requiredParams = apiTestManager.getRequiredParams(selectedPath);
                    const defaultParams = apiTestManager.getDefaultParams(selectedPath);
                    const params = {};
                    let retryCount = 3;
                    if (retryCountInput && retryCountInput.value !== '') {
                        retryCount = parseInt(retryCountInput.value, 10);
                        if (isNaN(retryCount) || retryCount < 0) retryCount = 0;
                    }
                    // Collect parameter values
                    requiredParams.forEach(param => {
                        const input = this.modalElement.querySelector(`#param-${param}`);
                        if (input) {
                            params[param] = input.value;
                        }
                    });

                    // Show loading state
                    testButton.disabled = true;
                    testButton.textContent = '测试�?...';
                    resultContainer.innerHTML = '<div class="api-test-loading">正在测试 API...</div>';

                    try {
                        const result = await apiTestManager.testApi(selectedPath, params, retryCount, selectedMethod);
                        if (result.success) {
                            resultContainer.innerHTML = `
                                <div class="api-test-success">
                                    <h4>�? 请求成功</h4>
                                    <pre>${JSON.stringify(result.data, null, 2)}</pre>
                                </div>
                            `;
                        } else {
                            resultContainer.innerHTML = `
                                <div class="api-test-error">
                                    <h4>�? 请求失败</h4>
                                    <pre>${result.error}</pre>
                                </div>
                            `;
                        }
                    } catch (error) {
                        console.error(`[${SCRIPT_NAME}] API测试出错:`, error);
                        resultContainer.innerHTML = `
                            <div class="api-test-error">
                                <h4>�? 请求失败</h4>
                                <pre>${error.message || '未知错误'}</pre>
                            </div>
                        `;
                    } finally {
                        testButton.disabled = false;
                        testButton.textContent = '测试 API';
                    }
                };
            }
        },
    };

    function initialize() { 
        console.log(`[${SCRIPT_NAME}] ${SCRIPT_VERSION} 初始化...`); 
        settingsManager.init(); 
        filterManager.init(); 
        uiManager.applyStyles(); 
        uiManager.initMiniProgress(); 
        let loadAttempts = 0; 
        const maxAttempts = 10; 
        function tryAddButton() { 
            loadAttempts++; 
            const pageSeemsReady = document.querySelector(DOM_SELECTORS.TARGET_BUTTON_AREA); 
            if (pageSeemsReady) { 
                if (document.querySelector('.fastlink-main-button-container')) return; 
                if (uiManager.createDropdownButton()) return; 
            } 
            if (loadAttempts < maxAttempts) { 
                const delay = loadAttempts < 3 ? 1500 : 3000; 
                setTimeout(tryAddButton, delay); 
            } else {
                console.warn(`[${SCRIPT_NAME}] 达到最大尝试次数,未能添加按钮。请刷新页面重试。`); 
            }
        } 
        const observer = new MutationObserver((mutations, obs) => { 
            const targetAreaExists = !!document.querySelector(DOM_SELECTORS.TARGET_BUTTON_AREA); 
            const ourButtonExists = !!document.querySelector('.fastlink-main-button-container'); 
            if (targetAreaExists && !ourButtonExists) { 
                loadAttempts = 0; 
                setTimeout(tryAddButton, 700); 
            } 
        }); 
        observer.observe(document.documentElement, { childList: true, subtree: true }); 
        setTimeout(tryAddButton, 500); 
    }

    if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(initialize, 300); } else { window.addEventListener('DOMContentLoaded', () => setTimeout(initialize, 300)); }

    function isValidHex(str) { 
        if (typeof str !== 'string' || str.length === 0) return false; 
        return /^[0-9a-fA-F]+$/.test(str); 
    }

    function isValidBase62(str) {
        if (typeof str !== 'string' || str.length === 0) return false;
        return /^[0-9a-zA-Z]+$/.test(str);
    }

    function bigIntToBase62(num) {
        if (typeof num !== 'bigint') throw new Error("Input must be a BigInt.");
        if (num === 0n) return BASE62_CHARS[0];
        let base62 = "";
        let n = num;
        while (n > 0n) {
            base62 = BASE62_CHARS[Number(n % 62n)] + base62;
            n = n / 62n;
        }
        return base62;
    }

    function base62ToBigInt(str) {
        if (typeof str !== 'string' || str.length === 0) throw new Error("Input must be non-empty string.");
        let num = 0n;
        for (let i = 0; i < str.length; i++) {
            const char = str[i];
            const val = BASE62_CHARS.indexOf(char);
            if (val === -1) throw new Error(`Invalid Base62 char: ${char}`);
            num = num * 62n + BigInt(val);
        }
        return num;
    }

    function hexToOptimizedEtag(hexEtag) {
        // First check if it's already a Base62 ETag
        if (isValidBase62(hexEtag) && !isValidHex(hexEtag)) {
            console.log(`[${SCRIPT_NAME}] ETag is already in Base62 format: ${hexEtag}`);
            return { original: hexEtag, optimized: hexEtag, useV2: true };
        }

        // Then check if it's a valid hex ETag
        if (!isValidHex(hexEtag) || hexEtag.length === 0) {
            console.log(`[${SCRIPT_NAME}] Invalid hex ETag: ${hexEtag}`);
            return { original: hexEtag, optimized: null, useV2: false };
        }

        try {
            if (settingsManager.isDebugMode()) console.log(`[${SCRIPT_NAME}] Converting hex ETag: ${hexEtag}`);
            const bigIntValue = BigInt('0x' + hexEtag);
            const base62Value = bigIntToBase62(bigIntValue);
            if (base62Value.length > 0 && base62Value.length < hexEtag.length) {
                if (settingsManager.isDebugMode()) console.log(`[${SCRIPT_NAME}] Converted to Base62 ETag: ${base62Value}`);
                return { original: hexEtag, optimized: base62Value, useV2: true };
            }
            if (settingsManager.isDebugMode()) console.log(`[${SCRIPT_NAME}] Keeping original hex ETag (conversion not beneficial): ${hexEtag}`);
            return { original: hexEtag, optimized: hexEtag, useV2: false };
        } catch (e) {
            console.warn(`[${SCRIPT_NAME}] ETag "${hexEtag}" to Base62 failed: ${e.message}. Using original.`);
            return { original: hexEtag, optimized: null, useV2: false };
        }
    }

    function optimizedEtagToHex(optimizedEtag, isV2Etag) {
        if (!isV2Etag) return optimizedEtag;
        if (typeof optimizedEtag !== 'string' || optimizedEtag.length === 0) {
            throw new Error("V2 ETag cannot be empty.");
        }

        // If it's already a hex string, return it
        if (isValidHex(optimizedEtag)) {
            console.log(`[${SCRIPT_NAME}] ETag is already in hex format: ${optimizedEtag}`);
            return optimizedEtag;
        }

        // If it's a Base62 string, convert it
        if (isValidBase62(optimizedEtag)) {
            try {
                console.log(`[${SCRIPT_NAME}] Converting Base62 ETag: ${optimizedEtag}`);
                const bigIntValue = base62ToBigInt(optimizedEtag);
                let hex = bigIntValue.toString(16).toLowerCase();
                // Always pad to 32 characters for hex ETags
                if (hex.length < 32) {
                    hex = hex.padStart(32, '0');
                }
                console.log(`[${SCRIPT_NAME}] Converted to hex ETag: ${hex}`);
                return hex;
            } catch (e) {
                console.error(`[${SCRIPT_NAME}] Failed to convert Base62 ETag "${optimizedEtag}" to hex:`, e);
                throw new Error(`Base62 ETag "${optimizedEtag}" to Hex failed: ${e.message}`);
            }
        }

        throw new Error(`Invalid ETag format: ${optimizedEtag}`);
    }

    // Add new function to handle .123share file conversion
    function convert123ShareToJson(fileContent) {
        try {
            // Decode base64 content
            const decodedContent = atob(fileContent);
            // Parse JSON
            const rawData = JSON.parse(decodedContent);
            
            // Build folder structure and file paths
            const folderMap = new Map(); // Map<folderId, folderName>
            const fileEntries = []; // Array of file entries to be included in output
            
            // First pass: collect all folders
            rawData.forEach(item => {
                if (item.Type === 1) { // Folder
                    folderMap.set(item.FileId, item.FileName);
                }
            });
            
            // Second pass: process files and build paths
            rawData.forEach(item => {
                if (item.Type === 0) { // File
                    // Build path by following parent chain
                    let currentPath = item.FileName;
                    let currentParentId = item.parentFileId;
                    let rootFolderId = currentParentId; // Track the root folder ID
                    
                    while (currentParentId && currentParentId !== "0") {
                        const parentName = folderMap.get(currentParentId);
                        if (parentName) {
                            currentPath = parentName + "/" + currentPath;
                            // Find parent's parent
                            const parentItem = rawData.find(i => i.FileId === currentParentId);
                            currentParentId = parentItem ? parentItem.parentFileId : "0";
                            if (currentParentId === "0") {
                                rootFolderId = item.parentFileId; // This is the root folder ID
                            }
                        } else {
                            break;
                        }
                    }
                    
                    fileEntries.push({
                        path: currentPath,
                        size: String(item.Size || 0),
                        etag: item.Etag,
                        rootFolderId: rootFolderId // Store the root folder ID for each file
                    });
                }
            });
            
            // Calculate total size
            const totalSize = fileEntries.reduce((sum, entry) => sum + (Number(entry.size) || 0), 0);
            
            // Find common root folder
            let commonPath = "";
            if (fileEntries.length > 0) {
                const firstRootFolderId = fileEntries[0].rootFolderId;
                const allShareSameRoot = fileEntries.every(entry => entry.rootFolderId === firstRootFolderId);
                
                if (allShareSameRoot && firstRootFolderId !== "0") {
                    // Find the root folder name
                    const rootFolderName = folderMap.get(firstRootFolderId);
                    if (rootFolderName) {
                        commonPath = rootFolderName + "/";
                    }
                }
            }
            
            // Check if we should use V2 format (Base62 ETags)
            const useV2Format = settingsManager.get('usesBase62EtagsInExport');
            
            // Convert to expected format
            const jsonData = {
                scriptVersion: SCRIPT_VERSION,
                exportVersion: "1.0",
                usesBase62EtagsInExport: useV2Format,
                commonPath: commonPath,
                totalFilesCount: fileEntries.length,
                totalSize: totalSize,
                formattedTotalSize: formatBytes(totalSize),
                files: fileEntries.map(entry => ({
                    path: commonPath ? entry.path.substring(commonPath.length) : entry.path,
                    size: entry.size,
                    etag: useV2Format ? hexToOptimizedEtag(entry.etag).optimized : entry.etag
                }))
            };
            
            return jsonData;
        } catch (e) {
            console.error(`[${SCRIPT_NAME}] Error converting .123share file:`, e);
            throw new Error("Invalid .123share file format");
        }
    }

    // Add new function to handle JSON to .123share file conversion
    function convertJsonTo123Share(jsonData) {
        try {
            if (!jsonData || typeof jsonData !== 'object' || !Array.isArray(jsonData.files)) {
                throw new Error("Invalid JSON data format");
            }

            // Convert files array to raw data format
            const rawData = [];
            const folderMap = new Map(); // Map<folderPath, folderId>
            let nextFolderId = 1;

            // Helper function to ensure folder exists and get its ID
            function ensureFolderExists(folderPath) {
                if (!folderPath) return "0"; // Root folder
                if (folderMap.has(folderPath)) return folderMap.get(folderPath);
                
                const folderId = String(nextFolderId++);
                folderMap.set(folderPath, folderId);
                
                // Add folder entry
                rawData.push({
                    FileId: folderId,
                    FileName: folderPath.split('/').pop(),
                    Type: 1, // Folder
                    Size: 0,
                    Etag: "",
                    ParentFileId: folderPath.includes('/') ? ensureFolderExists(folderPath.substring(0, folderPath.lastIndexOf('/'))) : "0"
                });
                
                return folderId;
            }

            // Process each file
            jsonData.files.forEach(file => {
                const fullPath = jsonData.commonPath ? jsonData.commonPath + file.path : file.path;
                const fileName = fullPath.split('/').pop();
                const folderPath = fullPath.substring(0, fullPath.lastIndexOf('/'));
                const parentFolderId = ensureFolderExists(folderPath);

                // Add file entry
                rawData.push({
                    FileId: String(nextFolderId++),
                    FileName: fileName,
                    Type: 0, // File
                    Size: parseInt(file.size) || 0,
                    Etag: file.etag,
                    ParentFileId: parentFolderId
                });
            });

            // Convert to base64 with proper Unicode handling
            const jsonString = JSON.stringify(rawData);
            // First encode the string to UTF-8 using encodeURIComponent
            const utf8String = encodeURIComponent(jsonString)
                .replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16)));
            // Then convert to base64
            return btoa(utf8String);
        } catch (e) {
            console.error(`[${SCRIPT_NAME}] Error converting JSON to .123share file:`, e);
            throw new Error("Invalid JSON data format or conversion failed");
        }
    }

    function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; }

    // Add settings click handler
    document.addEventListener('click', function(e) {
        if (e.target.id === 'fastlink-settings') {
            uiManager.showSettingsDialog();
        }
    });

    // Add helper function to convert alternative format to standard format
    function convertAlternativeJsonFormat(jsonData) {
        if (!Array.isArray(jsonData)) return null;
        
        // Check if it's the alternative format (array of arrays with 3 elements)
        if (jsonData.every(item => Array.isArray(item) && item.length === 3 && 
            typeof item[0] === 'string' && typeof item[1] === 'number' && typeof item[2] === 'string')) {
            
            return {
                usesBase62EtagsInExport: false,
                files: jsonData.map(item => ({
                    path: item[2],
                    size: String(item[1]),
                    etag: item[0].toLowerCase() // Convert etag to lowercase
                }))
            };
        }
        return null;
    }

    function extractShareInfoFromText(text) {
        let shareKey = null;
        let sharePwd = null;

        // Remove @ prefix if present
        text = text.replace(/^@/, '');

        // Try to parse as URL first
        try {
            let url;
            try {
                url = new URL(text);
            } catch (e) {
                if (!text.startsWith('http')) {
                    url = new URL('https://' + text);
                } else {
                    throw e;
                }
            }

            const pathSegments = url.pathname.split('/');
            const sIndex = pathSegments.indexOf('s');
            if (sIndex !== -1 && pathSegments.length > sIndex + 1) {
                // Get everything after /s/ up to the next / or end of string
                shareKey = pathSegments[sIndex + 1].split(/[\/?#]/)[0];
                const searchParams = new URLSearchParams(url.search);
                const possiblePwdParams = ['pwd', '提取码', 'password', 'extract', 'code'];
                for (const paramName of possiblePwdParams) {
                    if (searchParams.has(paramName)) {
                        sharePwd = searchParams.get(paramName);
                        break;
                    }
                }
            }
        } catch (e) {
            // If URL parsing fails, try to extract from text
            const sMatch = text.match(/\/s\/([^\/\s?#]+)/);
            if (sMatch) {
                shareKey = sMatch[1].split(/[\/?#]/)[0];
            }
        }

        // If not found as URL, try to extract share key and password from text
        if (!shareKey) {
            // Try to find share key in various formats, including those with special characters
            const keyMatch = text.match(/\/s\/([^\/\s?#]+)/) || text.match(/([A-Za-z0-9\-_%:]{8,})/);
            if (keyMatch) {
                shareKey = keyMatch[1].split(/[\/?#]/)[0];
            }
        }

        // Try to find password in various formats
        if (!sharePwd) {
            const pwdRegexes = [
                /提取码[:=]([A-Za-z0-9]+)/,
                /[:=]([A-Za-z0-9]+)$/,  // Match password at the end after : or =
                /[?&]提取码[:=]([A-Za-z0-9]+)/,
                /[?&]pwd[:=]([A-Za-z0-9]+)/,
                /[?&]password[:=]([A-Za-z0-9]+)/,
                /提取码[::]([A-Za-z0-9]+)/,
                /[?&]提取码[::]([A-Za-z0-9]+)/
            ];
            for (const regex of pwdRegexes) {
                const match = text.match(regex);
                if (match) {
                    sharePwd = match[1];
                    break;
                }
            }
        }

        return { shareKey, sharePwd };
    }

    // Batch processing code
    generatePublicBatchBtn.onclick = async function() {
        const batchInputEl = this.modalElement.querySelector('#fl-m-public-share-batch-input');
        if (!batchInputEl || !batchInputEl.value.trim()) {
            this.showAlert("请输入至少一个分享链接。");
            return;
        }

        const lines = batchInputEl.value.trim().split('\n').filter(line => line.trim());
        if (lines.length === 0) {
            this.showAlert("请输入至少一个分享链接。");
            return;
        }

        generatePublicBatchBtn.disabled = true;
        this.modalElement.querySelector('#fl-m-cancel')?.setAttribute('disabled', 'true');

        let successCount = 0;
        let failureCount = 0;
        let totalFiles = 0;
        let totalSize = 0;

        for (const line of lines) {
            if (!line.trim()) continue;
            const { shareKey, sharePwd } = extractShareInfoFromText(line);
            if (!shareKey) {
                processStateManager.appendErrorMessage(`❌ 无效的分享链接: ${line}`);
                failureCount++;
                continue;
            }

            try {
                const result = await coreLogic.generateLinkFromPublicShare(shareKey, sharePwd, "0", true);
                if (result && result.jsonData) {
                    // Generate filename for this share
                    let fileName;
                    if (settingsManager.get('useFolderNameForJson')) {
                        const firstPathSegment = (result.jsonData.commonPath || '').split('/')[0];
                        const sanitizedPath = firstPathSegment.replace(/[\/:*?"<>|]/g, '_').replace(/^\s+|\s+$/g, '');
                        if (sanitizedPath) {
                            if (settingsManager.get('appendDateToJson')) {
                                const now = new Date();
                                const dateStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
                                fileName = `${sanitizedPath}_${dateStr}.json`;
                            } else {
                                fileName = `${sanitizedPath}.json`;
                            }
                        } else {
                            fileName = `123FastLink_${Date.now()}.json`;
                        }
                    } else {
                        fileName = `123FastLink_${Date.now()}.json`;
                    }

                    // Silently export JSON
                    this._downloadToFile(JSON.stringify(result.jsonData, null, 2), fileName, 'application/json');

                    // Update totals
                    totalFiles += result.jsonData.totalFilesCount;
                    totalSize += result.jsonData.totalSize;
                    successCount++;
                } else {
                    processStateManager.appendErrorMessage(`❌ 生成分享链接失败: ${shareKey}`);
                    failureCount++;
                }
            } catch (error) {
                processStateManager.appendErrorMessage(`❌ 处理分享链接时出错: ${shareKey} - ${error.message}`);
                failureCount++;
            }
        }

        // Show final summary modal
        const formattedTotalSize = formatBytes(totalSize);
        const summary = `<div class="fastlink-result">
            <h3>${failureCount > 0 && successCount > 0 ? "🎯 部分成功" : (successCount > 0 ? "🎉 生成成功" : "🤔 无有效数据")}</h3>
            <p>📄 已处理分享: ${lines.length} 个</p>
            <p>✅ 成功生成: ${successCount} 个</p>
            <p>❌ 失败项目: ${failureCount} 个</p>
            <p>📦 总文件数: ${totalFiles} 个</p>
            <p>💾 总大小: ${formattedTotalSize}</p>
        </div>`;

        this.showModal("🎉 批量处理完成", summary, 'showLink', true);
        processStateManager.appendInfoMessage(`\n📊 批量处理完成: 成功 ${successCount} 个, 失败 ${failureCount} 个`);
        generatePublicBatchBtn.disabled = false;
        this.modalElement.querySelector('#fl-m-cancel')?.removeAttribute('disabled');
    };

})();