123FastLink

123云盘秒传链接脚本

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         123FastLink
// @namespace    http://tampermonkey.net/
// @version      2026.5.27.1
// @description  123云盘秒传链接脚本
// @author       Baoqing
// @author       Chaofan
// @author       lipkiat
// @include      *://*.123*.com/*
// @include      *://*.123*.cn/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=123pan.com
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==

/******/ (() => { // webpackBootstrap
/******/ 	"use strict";

;// ./src/config.js
var GlobalConfig = {
    scriptVersion: "3.2.0",
    usesBase62EtagsInExport: true,
    getFileListPageDelay: 500,
    getFileInfoBatchSize: 100,
    getFileInfoDelay: 200,
    getFolderInfoDelay: 300,
    saveLinkDelay: 100,
    mkdirDelay: 100,
    scriptName: "123FASTLINKV3",
    COMMON_PATH_LINK_PREFIX_V2: "123FLCPV2$",
    MAX_TEXT_FILE_SIZE: 3 * 1024 * 1024,
    DEFAULT_EXPORT_FILENAME: "123FastLink_Export",
    DEBUGMODE: true,
    seedFilePathId: null,
    secondaryLinkUseJson: true
};

function initSettings() {
    const Settings = GM_getValue('fastlink_settings', null);
    if (Settings) {
        try {
            GlobalConfig = { ...GlobalConfig, ...Settings };
        } catch (e) {
            console.error("加载设置失败:", e);
            saveSettings({});
        }
    }
}

function saveSettings(settings) {
    GlobalConfig = { ...GlobalConfig, ...settings };
    GM_setValue('fastlink_settings', settings);
}

function deleteSettings() {
    GM_setValue('fastlink_settings', null);
    GM_setValue('fastlink_first_time', true);
}

function isFirstTime() {
    const firstTime = GM_getValue('fastlink_first_time', true);
    if (firstTime) {
        GM_setValue('fastlink_first_time', false);
    }
    return firstTime;
}

;// ./src/logger.js


const COLORS = {
    log: 'color: #6b7280',
    info: 'color: #3b82f6',
    warn: 'color: #f59e0b; font-weight: bold',
    error: 'color: #ef4444; font-weight: bold'
};

function timestamp() {
    const d = new Date();
    return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}:${d.getSeconds().toString().padStart(2, '0')}.${d.getMilliseconds().toString().padStart(3, '0')}`;
}

function createLogger(moduleName) {
    return {
        log(...args) {
            if (!GlobalConfig.DEBUGMODE) return;
            window.console.log(`%c[${timestamp()}] [123FL] [${moduleName}]`, COLORS.log, ...args);
        },
        info(...args) {
            if (!GlobalConfig.DEBUGMODE) return;
            window.console.info(`%c[${timestamp()}] [123FL] [${moduleName}]`, COLORS.info, ...args);
        },
        warn(...args) {
            window.console.warn(`%c[${timestamp()}] [123FL] [${moduleName}]`, COLORS.warn, ...args);
        },
        error(...args) {
            window.console.error(`%c[${timestamp()}] [123FL] [${moduleName}]`, COLORS.error, ...args);
        }
    };
}

const logger = createLogger('Main');

;// ./src/PanApiClient.js



const log = createLogger('API');
const uploadLog = createLogger('Upload');
const downloadLog = createLogger('Download');

class PanApiClient {
    constructor() {
        this.init();
    }

    init() {
        this.host = 'https://' + window.location.host;
        this.authToken = localStorage['authorToken'];
        this.loginUuid = localStorage['LoginUuid'];
        this.appVersion = '3';
        this.referer = document.location.href;
        this.getFileListPageDelay = GlobalConfig.getFileListPageDelay;
        this.maxTextFileSize = GlobalConfig.MAX_TEXT_FILE_SIZE;
        this.progress = 0;
        this.progressDesc = "";
    }

    buildURL(path, queryParams) {
        const queryString = new URLSearchParams(queryParams || {}).toString();
        return `${this.host}${path}?${queryString}`;
    }

    async sendRequest(method, path, queryParams, body) {
        const headers = {
            'Content-Type': 'application/json;charset=UTF-8',
            'Authorization': 'Bearer ' + this.authToken,
            'platform': 'web',
            'App-Version': this.appVersion,
            'LoginUuid': this.loginUuid,
            'Origin': this.host,
            'Referer': this.referer,
        };
        try {
            const response = await fetch(this.buildURL(path, queryParams), {
                method, headers, body, credentials: 'include'
            });
            const data = await response.json();
            if (data.code !== 0) {
                throw new Error(data.message);
            }
            return data;
        } catch (e) {
            log.error('API请求失败:', e);
            throw e;
        }
    }

    async getOnePageFileList(parentFileId, page) {
        const urlParams = {
            //'2015049069': '1756010983-3879364-4059457292',
            driveId: '0',
            limit: '100',
            next: '0',
            orderBy: 'file_name',
            orderDirection: 'asc',
            parentFileId: parentFileId.toString(),
            trashed: 'false',
            SearchData: '',
            Page: page.toString(),
            OnlyLookAbnormalFile: '0',
            event: 'homeListFile',
            operateType: '1',
            inDirectSpace: 'false'
        };
        const data = await this.sendRequest("GET", "/b/api/file/list/new", urlParams);
        //log.log("获取文件列表:", data.data.InfoList);
        log.log("获取文件列表 ID:", parentFileId, "Page:", page);
        return { data: { InfoList: data.data.InfoList }, total: data.data.Total };
        //return { data: { fileList: data.data.fileList } };
    }

    async getFileList(parentFileId) {
        let InfoList = [];
        this.progress = 0;
        this.progressDesc = `获取文件列表 文件夹ID:${parentFileId}`;
        // 默认一页100
        // 先获取一次,得到Total
        log.log("开始获取文件列表,ID:", parentFileId);
        const info = await this.getOnePageFileList(parentFileId, 1);
        InfoList.push(...info.data.InfoList);
        const total = info.total;
        if (total > 100) {
            const times = Math.ceil(total / 100);
            for (let i = 2; i < times + 1; i++) {
                this.progress = Math.ceil((i / times) * 100);
                // this.progressDesc = `获取文件列表: ${this.progress}%`;
                const pageInfo = await this.getOnePageFileList(parentFileId, i);
                InfoList.push(...pageInfo.data.InfoList);
                // 延时
                await new Promise(resolve => setTimeout(resolve, this.getFileListPageDelay));
            }
        }
        this.progress = 100;
        return { data: { InfoList }, total: total };
    }

    async getFileInfo(idList) {
        const fileIdList = idList.map(fileId => ({ fileId }));
        const data = await this.sendRequest("POST", "/b/api/file/info", {}, JSON.stringify({ fileIdList }));
        return { data: { InfoList: data.data.infoList } };
    }

    // 尝试秒传
    async fastUpload(fileInfo) {
        try {
            const response = await this.sendRequest('POST', '/b/api/file/upload_request', {}, JSON.stringify({
                ...fileInfo, RequestSource: null
            }));
            const reuse = response['data']['Reuse'];
            log.log('reuse:', reuse);
            if (response['code'] !== 0) {
                return [false, response['message'], null];
            }
            if (!reuse) {
                log.error('保存文件失败:', fileInfo.fileName, 'response:', response);
                return [false, "未能实现秒传", null];
            } else {
                return [true, null, response['data']['Info']['FileId']];
            }
        } catch (error) {
            log.error('上传请求失败:', error);
            return [false, '请求失败', null];
        }
    }

    // 从sessionStorage中获取父级文件ID
    async getParentFileId() {
        const homeFilePath = JSON.parse(sessionStorage['filePath'])['homeFilePath'];
        const parentFileId = (homeFilePath[homeFilePath.length - 1] || 0);
        log.log('parentFileId:', parentFileId);
        return parentFileId.toString();
    }

    /**
     * 获取文件,尝试秒传
     * @param {dict} fileInfo - 文件信息字典,包含 etag, fileName, size 字段
     * @param {string} parentFileId
     * @returns [boolean, string] - [是否成功, 错误信息]
     */
    async getFile(fileInfo, parentFileId) {
        if (!parentFileId) {
            parentFileId = await this.getParentFileId();
        }
        return await this.fastUpload({
            driveId: 0,
            etag: fileInfo.etag,
            fileName: fileInfo.fileName,
            parentFileId,
            size: fileInfo.size,
            type: 0,
            duplicate: 1
        });
    }

    async mkdirInNowFolder(folderName = "New Folder") {
        const parentFileId = await this.getParentFileId();
        return this.mkdir(parentFileId, folderName);
    }

    async mkdir(parentFileId, folderName = "New Folder") {
        let folderFileId = null;
        try {
            const response = await this.sendRequest('POST', '/b/api/file/upload_request', {}, JSON.stringify({
                driveId: 0,
                etag: "",
                fileName: folderName,
                parentFileId,
                size: 0,
                type: 1,
                duplicate: 1,
                NotReuse: true,
                event: "newCreateFolder",
                operateType: 1,
                RequestSource: null
            }));
            folderFileId = response['data']['Info']['FileId'];
        } catch (error) {
            log.error('创建文件夹失败:', error);
            return {
                'folderFileId': null, 'folderName': folderName, 'success': false
            };
        }
        log.log('创建文件夹 ID:', folderFileId);
        return {
            'folderFileId': folderFileId, 'folderName': folderName, 'success': true
        };
    }


    calculateStringSize(text) {
        // 使用TextEncoder计算UTF-8编码的字节大小
        const encoder = new TextEncoder();
        return encoder.encode(text).length;
    }

    md5(inputString) {
        var hc = "0123456789abcdef";

        function rh(n) {
            var j, s = "";
            for (j = 0; j <= 3; j++) s += hc.charAt((n >> (j * 8 + 4)) & 0x0F) + hc.charAt((n >> (j * 8)) & 0x0F);
            return s;
        }

        function ad(x, y) {
            var l = (x & 0xFFFF) + (y & 0xFFFF);
            var m = (x >> 16) + (y >> 16) + (l >> 16);
            return (m << 16) | (l & 0xFFFF);
        }

        function rl(n, c) {
            return (n << c) | (n >>> (32 - c));
        }

        function cm(q, a, b, x, s, t) {
            return ad(rl(ad(ad(a, q), ad(x, t)), s), b);
        }

        function ff(a, b, c, d, x, s, t) {
            return cm((b & c) | ((~b) & d), a, b, x, s, t);
        }

        function gg(a, b, c, d, x, s, t) {
            return cm((b & d) | (c & (~d)), a, b, x, s, t);
        }

        function hh(a, b, c, d, x, s, t) {
            return cm(b ^ c ^ d, a, b, x, s, t);
        }

        function ii(a, b, c, d, x, s, t) {
            return cm(c ^ (b | (~d)), a, b, x, s, t);
        }

        function sb(x) {
            var i;
            var nblk = ((x.length + 8) >> 6) + 1;
            var blks = new Array(nblk * 16);
            for (i = 0; i < nblk * 16; i++) blks[i] = 0;
            for (i = 0; i < x.length; i++) blks[i >> 2] |= x.charCodeAt(i) << ((i % 4) * 8);
            blks[i >> 2] |= 0x80 << ((i % 4) * 8);
            blks[nblk * 16 - 2] = x.length * 8;
            return blks;
        }

        var i, x = sb(inputString), a = 1732584193, b = -271733879, c = -1732584194, d = 271733878, olda, oldb,
            oldc, oldd;
        for (i = 0; i < x.length; i += 16) {
            olda = a;
            oldb = b;
            oldc = c;
            oldd = d;
            a = ff(a, b, c, d, x[i + 0], 7, -680876936);
            d = ff(d, a, b, c, x[i + 1], 12, -389564586);
            c = ff(c, d, a, b, x[i + 2], 17, 606105819);
            b = ff(b, c, d, a, x[i + 3], 22, -1044525330);
            a = ff(a, b, c, d, x[i + 4], 7, -176418897);
            d = ff(d, a, b, c, x[i + 5], 12, 1200080426);
            c = ff(c, d, a, b, x[i + 6], 17, -1473231341);
            b = ff(b, c, d, a, x[i + 7], 22, -45705983);
            a = ff(a, b, c, d, x[i + 8], 7, 1770035416);
            d = ff(d, a, b, c, x[i + 9], 12, -1958414417);
            c = ff(c, d, a, b, x[i + 10], 17, -42063);
            b = ff(b, c, d, a, x[i + 11], 22, -1990404162);
            a = ff(a, b, c, d, x[i + 12], 7, 1804603682);
            d = ff(d, a, b, c, x[i + 13], 12, -40341101);
            c = ff(c, d, a, b, x[i + 14], 17, -1502002290);
            b = ff(b, c, d, a, x[i + 15], 22, 1236535329);
            a = gg(a, b, c, d, x[i + 1], 5, -165796510);
            d = gg(d, a, b, c, x[i + 6], 9, -1069501632);
            c = gg(c, d, a, b, x[i + 11], 14, 643717713);
            b = gg(b, c, d, a, x[i + 0], 20, -373897302);
            a = gg(a, b, c, d, x[i + 5], 5, -701558691);
            d = gg(d, a, b, c, x[i + 10], 9, 38016083);
            c = gg(c, d, a, b, x[i + 15], 14, -660478335);
            b = gg(b, c, d, a, x[i + 4], 20, -405537848);
            a = gg(a, b, c, d, x[i + 9], 5, 568446438);
            d = gg(d, a, b, c, x[i + 14], 9, -1019803690);
            c = gg(c, d, a, b, x[i + 3], 14, -187363961);
            b = gg(b, c, d, a, x[i + 8], 20, 1163531501);
            a = gg(a, b, c, d, x[i + 13], 5, -1444681467);
            d = gg(d, a, b, c, x[i + 2], 9, -51403784);
            c = gg(c, d, a, b, x[i + 7], 14, 1735328473);
            b = gg(b, c, d, a, x[i + 12], 20, -1926607734);
            a = hh(a, b, c, d, x[i + 5], 4, -378558);
            d = hh(d, a, b, c, x[i + 8], 11, -2022574463);
            c = hh(c, d, a, b, x[i + 11], 16, 1839030562);
            b = hh(b, c, d, a, x[i + 14], 23, -35309556);
            a = hh(a, b, c, d, x[i + 1], 4, -1530992060);
            d = hh(d, a, b, c, x[i + 4], 11, 1272893353);
            c = hh(c, d, a, b, x[i + 7], 16, -155497632);
            b = hh(b, c, d, a, x[i + 10], 23, -1094730640);
            a = hh(a, b, c, d, x[i + 13], 4, 681279174);
            d = hh(d, a, b, c, x[i + 0], 11, -358537222);
            c = hh(c, d, a, b, x[i + 3], 16, -722521979);
            b = hh(b, c, d, a, x[i + 6], 23, 76029189);
            a = hh(a, b, c, d, x[i + 9], 4, -640364487);
            d = hh(d, a, b, c, x[i + 12], 11, -421815835);
            c = hh(c, d, a, b, x[i + 15], 16, 530742520);
            b = hh(b, c, d, a, x[i + 2], 23, -995338651);
            a = ii(a, b, c, d, x[i + 0], 6, -198630844);
            d = ii(d, a, b, c, x[i + 7], 10, 1126891415);
            c = ii(c, d, a, b, x[i + 14], 15, -1416354905);
            b = ii(b, c, d, a, x[i + 5], 21, -57434055);
            a = ii(a, b, c, d, x[i + 12], 6, 1700485571);
            d = ii(d, a, b, c, x[i + 3], 10, -1894986606);
            c = ii(c, d, a, b, x[i + 10], 15, -1051523);
            b = ii(b, c, d, a, x[i + 1], 21, -2054922799);
            a = ii(a, b, c, d, x[i + 8], 6, 1873313359);
            d = ii(d, a, b, c, x[i + 15], 10, -30611744);
            c = ii(c, d, a, b, x[i + 6], 15, -1560198380);
            b = ii(b, c, d, a, x[i + 13], 21, 1309151649);
            a = ii(a, b, c, d, x[i + 4], 6, -145523070);
            d = ii(d, a, b, c, x[i + 11], 10, -1120210379);
            c = ii(c, d, a, b, x[i + 2], 15, 718787259);
            b = ii(b, c, d, a, x[i + 9], 21, -343485551);
            a = ad(a, olda);
            b = ad(b, oldb);
            c = ad(c, oldc);
            d = ad(d, oldd);
        }
        return rh(a) + rh(b) + rh(c) + rh(d);
    }


    /**
     * 第一步:上传请求
     */
    async uploadRequest(fileInfo) {
        try {
            const data = await this.sendRequest('POST', '/b/api/file/upload_request', {}, JSON.stringify({
                ...fileInfo, RequestSource: null
            }));

            uploadLog.log('upload_request响应:', data);

            if (data.code !== 0) {
                return [false, data.message, null];
            }

            return [true, null, data.data];

        } catch (error) {
            uploadLog.error('上传请求失败:', error);
            return [false, '上传请求失败: ' + error.message, null];
        }
    }

    /**
     * 第二步:获取S3上传凭证
     */
    async getUploadAuth(bucket, key, uploadId, storageNode, partNumberStart = 1, partNumberEnd = 1) {
        try {
            const data = await this.sendRequest('POST', '/b/api/file/s3_upload_object/auth', {}, JSON.stringify({
                bucket: bucket,
                key: key,
                partNumberEnd: partNumberEnd.toString(),
                partNumberStart: partNumberStart.toString(),
                uploadId: uploadId,
                StorageNode: storageNode
            }));

            uploadLog.log('获取上传凭证响应:', data);

            if (data.code !== 0) {
                return [false, data.message, null];
            }

            const presignedUrls = data.data.presignedUrls;
            const firstUrl = presignedUrls[partNumberStart.toString()];

            if (!firstUrl) {
                return [false, '未获取到上传URL', null];
            }

            return [true, null, firstUrl];

        } catch (error) {
            uploadLog.error('获取上传凭证失败:', error);
            return [false, '获取上传凭证失败: ' + error.message, null];
        }
    }

    /**
     * 第三步:上传文本到S3
     * 上传整个文本内容(非分片),不使用sendRequest
     * @param {string} presignedUrl - 预签名URL
     * @param {string} text - 要上传的文本内容
     * @returns {Promise<Array>} [是否成功, 错误信息]
     */
    async uploadToS3Entire(presignedUrl, text) {
        try {
            // 将文本转换为Blob
            const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });

            uploadLog.log('开始上传到S3:', presignedUrl);

            // 移除 x-amz-acl 头,因为预签名URL通常已经包含了所有必要的认证信息
            const response = await fetch(presignedUrl, {
                method: 'PUT', headers: {
                    'Content-Type': 'text/plain;charset=utf-8'
                    // 注意:不要添加 x-amz-acl,因为预签名URL已经包含了权限信息
                }, body: blob
            });

            uploadLog.log('S3上传响应状态:', response.status, response.statusText);

            if (response.ok || response.status === 200) {
                return [true, null];
            } else {
                const errorText = await response.text();
                uploadLog.error('S3上传失败详情:', errorText);

                // 尝试不带Content-Type头再次上传(某些S3配置可能不需要)
                if (response.status === 403 || response.status === 400) {
                    uploadLog.log('尝试不带Content-Type头上传');
                    const retryResponse = await fetch(presignedUrl, {
                        method: 'PUT', body: blob
                        // 完全不设置headers
                    });

                    if (retryResponse.ok || retryResponse.status === 200) {
                        return [true, null];
                    } else {
                        const retryError = await retryResponse.text();
                        return [false, `S3上传失败: ${response.status} ${response.statusText}, 重试: ${retryResponse.status} ${retryResponse.statusText}`];
                    }
                }

                return [false, `S3上传失败: ${response.status} ${response.statusText}`];
            }

        } catch (error) {
            uploadLog.error('S3上传失败:', error);
            return [false, 'S3上传失败: ' + error.message];
        }
    }

    /**
     * 第四步:完成上传
     */
    async completeUpload(fileId, bucket, fileSize, key, uploadId, storageNode) {
        try {
            const data = await this.sendRequest('POST', '/b/api/file/upload_complete/v2', {}, JSON.stringify({
                fileId: fileId, bucket: bucket, fileSize: fileSize.toString(), key: key, isMultipart: false,  // 文本文件较小,单分片上传
                uploadId: uploadId, StorageNode: storageNode
            }));

            uploadLog.log('完成上传响应:', data);

            if (data.code !== 0) {
                return [false, data.message, null];
            }

            return [true, null, data.data];

        } catch (error) {
            uploadLog.error('完成上传失败:', error);
            return [false, '完成上传失败: ' + error.message, null];
        }
    }

    /**
     * 完整文本上传流程
     * @param {string} fileName - 文件名
     * @param {string} text - 要上传的文本内容
     * @param {string|number} parentFileId - 父文件夹ID,可选
     * @param {boolean} calculateMD5 - 是否计算MD5,默认true
     * @returns {Promise<Array>} [是否成功, 错误信息, 文件ID]
     */
    async uploadTextFile(fileName, text, parentFileId = 0) {
        try {
            uploadLog.log('开始上传文本文件:', fileName);

            // 1. 获取父文件夹ID
            if (!parentFileId) {
                parentFileId = await this.getParentFileId();
            }

            // 2. 计算文件大小
            const fileSize = this.calculateStringSize(text);
            uploadLog.log('文件大小:', fileSize, '字节');

            // 3. 计算MD5
            const md5 = this.md5(text);
            uploadLog.log('文件MD5:', md5);

            // 4. 第一步:上传请求
            uploadLog.log('步骤1: 上传请求');
            const [requestSuccess, requestError, uploadData] = await this.uploadRequest({
                driveId: 0, etag: md5, fileName: fileName, parentFileId: parentFileId, size: fileSize, type: 0,  // 0表示文件,1表示文件夹
                duplicate: 1,  // 1表示覆盖同名文件
                event: "homeUploadFile",  // 添加事件类型
                operateType: 1
            });

            if (!requestSuccess) {
                return [false, '上传请求失败: ' + requestError, null];
            }

            uploadLog.log('上传请求数据:', uploadData);

            // 5. 检查是否秒传
            if (uploadData.Reuse) {
                uploadLog.log('秒传成功,文件ID:', uploadData.FileId);
                return [true, "秒传成功", uploadData.FileId, {
                    etag: md5, fileName: fileName, size: fileSize
                }];
            }

            // 6. 第二步:获取上传凭证
            uploadLog.log('步骤2: 获取上传凭证');
            const [authSuccess, authError, presignedUrl] = await this.getUploadAuth(uploadData.Bucket, uploadData.Key, uploadData.UploadId, uploadData.StorageNode);

            if (!authSuccess) {
                return [false, '获取上传凭证失败: ' + authError, null];
            }

            uploadLog.log('预签名URL:', presignedUrl);

            // 7. 第三步:上传到S3
            uploadLog.log('步骤3: 上传到S3');
            const [uploadSuccess, uploadError] = await this.uploadToS3Entire(presignedUrl, text);

            if (!uploadSuccess) {
                return [false, 'S3上传失败: ' + uploadError, null];
            }

            uploadLog.log('S3上传成功');

            // 8. 第四步:完成上传
            uploadLog.log('步骤4: 完成上传');
            const [completeSuccess, completeError, completeData] = await this.completeUpload(uploadData.FileId, uploadData.Bucket, fileSize, uploadData.Key, uploadData.UploadId, uploadData.StorageNode);

            if (!completeSuccess) {
                return [false, '完成上传失败: ' + completeError, null];
            }

            uploadLog.log('上传完成,文件ID:', uploadData.FileId);
            return [true, "上传完成", uploadData.FileId, {
                etag: md5, fileName: fileName, size: fileSize
            }];

        } catch (error) {
            uploadLog.error('文本上传流程失败:', error);
            return [false, '上传流程失败: ' + error.message, null];
        }
    }

    /**
     * 创建文本文件(在指定文件夹中)
     * @param {string} fileName - 文件名
     * @param {string} text - 文本内容
     * @param {string|number} folderId - 文件夹ID
     * @returns {Promise<Array>} [是否成功, 错误信息, 文件ID]
     */
    async createTextFileInFolder(fileName, text, folderId) {
        return await this.uploadTextFile(fileName, text, folderId, true);
    }

    /**
     * 在当前文件夹创建文本文件
     * @param {string} fileName - 文件名
     * @param {string} text - 文本内容
     * @returns {Promise<Array>} [是否成功, 错误信息, 文件ID]
     */
    async createTextFileInCurrentFolder(fileName, text) {
        const parentFileId = await this.getParentFileId();
        return await this.uploadTextFile(fileName, text, parentFileId, true);
    }

    /**
     * 第一步:获取下载调度列表
     * @param {Object} fileInfo - 文件信息
     * @param {string} fileInfo.etag - 文件MD5
     * @param {string|number} fileInfo.fileId - 文件ID
     * @param {string} fileInfo.s3keyFlag - S3 Key标志
     * @param {string} fileInfo.fileName - 文件名
     * @param {string|number} fileInfo.size - 文件大小
     * @returns {Promise<Array>} [是否成功, 错误信息, 调度数据]
     */
    async getDispatchList(fileInfo) {
        try {
            downloadLog.log('获取下载调度列表,文件:', fileInfo.fileName);

            const data = await this.sendRequest('POST', '/b/api/v2/file/download_info', {}, JSON.stringify({
                driveId: 0,
                etag: fileInfo.etag,
                fileId: fileInfo.fileId.toString(),
                s3keyFlag: fileInfo.s3keyFlag,
                type: 0,  // 0-文件
                fileName: fileInfo.fileName,
                size: fileInfo.size.toString()
            }));

            downloadLog.log('获取下载调度列表响应:', data);

            if (data.code !== 0) {
                downloadLog.error('获取下载调度列表失败:', data.message);
                return [false, data.message, null];
            }

            return [true, null, data.data];

        } catch (error) {
            downloadLog.error('获取下载调度列表异常:', error);
            return [false, '获取下载调度列表失败: ' + error.message, null];
        }
    }

    /**
     * 第二步:获取下载链接
     * 随机选择一个下载线路,拼接完整下载链接
     * @param {Object} fileInfo - 文件信息
     * @param {string} fileInfo.etag - 文件MD5
     * @param {string|number} fileInfo.fileId - 文件ID
     * @param {string} fileInfo.s3keyFlag - S3 Key标志
     * @param {string} fileInfo.fileName - 文件名
     * @param {string|number} fileInfo.size - 文件大小
     * @param {string} preferredIsp - 优先选择的ISP线路(可选)
     * @returns {Promise<Array>} [是否成功, 错误信息, 下载链接]
     */
    async getDownloadLink(fileInfo, preferredIsp = null) {
        try {
            downloadLog.log('获取下载链接,文件:', fileInfo.fileName);

            // 1. 获取调度列表
            const [dispatchSuccess, dispatchError, dispatchData] = await this.getDispatchList(fileInfo);
            if (!dispatchSuccess) {
                return [false, dispatchError, null];
            }

            const { dispatchList, downloadPath } = dispatchData;

            if (!dispatchList || dispatchList.length === 0) {
                return [false, '没有可用的下载线路', null];
            }

            if (!downloadPath) {
                return [false, '没有获取到下载路径', null];
            }

            // 2. 选择下载线路
            let selectedDispatch = null;

            if (preferredIsp) {
                // 如果指定了优先线路,尝试匹配
                selectedDispatch = dispatchList.find(item => item.isp === preferredIsp);
            }

            // 如果没有匹配到指定线路,随机选择一个
            if (!selectedDispatch) {
                const randomIndex = Math.floor(Math.random() * dispatchList.length);
                selectedDispatch = dispatchList[randomIndex];
            }

            downloadLog.log('选择的下载线路:', selectedDispatch.isp, 'URL前缀:', selectedDispatch.prefix);

            // 3. 拼接完整的下载链接
            // 确保前缀不以斜杠结尾,路径以斜杠开头
            const cleanPrefix = selectedDispatch.prefix.endsWith('/') ? selectedDispatch.prefix.slice(0, -1) : selectedDispatch.prefix;
            const cleanPath = downloadPath.startsWith('/') ? downloadPath : '/' + downloadPath;

            const downloadLink = `${cleanPrefix}${cleanPath}`;

            downloadLog.log('完整下载链接:', downloadLink);

            return [true, null, {
                downloadLink,
                isp: selectedDispatch.isp,
                prefix: selectedDispatch.prefix,
                downloadPath,
                fileId: fileInfo.fileId,
                fileName: fileInfo.fileName
            }];

        } catch (error) {
            downloadLog.error('获取下载链接异常:', error);
            return [false, '获取下载链接失败: ' + error.message, null];
        }
    }

    /**
     * 第三步:获取链接文本内容
     * 通过GET请求下载链接,返回文本内容
     * @param {string} downloadLink - 下载链接
     * @param {Object} options - 可选参数
     * @param {number} options.timeout - 超时时间(毫秒),默认30000
     * @param {boolean} options.includeHeaders - 是否包含响应头信息
     * @returns {Promise<Array>} [是否成功, 错误信息, 文本内容/响应数据]
     */
    async getLinkTextContent(downloadLink, options = {}) {
        const {
            timeout = 30000, includeHeaders = false
        } = options;

        try {
            downloadLog.log('获取链接文本内容:', downloadLink);

            // 使用AbortController实现超时控制
            const controller = new AbortController();
            const timeoutId = setTimeout(() => controller.abort(), timeout);

            try {
                const response = await fetch(downloadLink, {
                    method: 'GET', signal: controller.signal, headers: {
                        'Accept': 'text/plain,text/html,application/json,*/*',
                        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
                    }
                });

                clearTimeout(timeoutId);

                downloadLog.log('响应状态:', response.status, response.statusText);

                if (!response.ok) {
                    // 尝试获取更多错误信息
                    let errorText = '';
                    try {
                        errorText = await response.text();
                        downloadLog.error('错误响应内容:', errorText);
                    } catch (e) {
                        // 忽略读取错误
                    }

                    return [false, `HTTP ${response.status}: ${response.statusText}`, null];
                }

                // 获取响应头
                const headers = {};
                response.headers.forEach((value, key) => {
                    headers[key] = value;
                });

                // 获取文本内容
                const textContent = await response.text();
                downloadLog.log('获取到文本内容,长度:', textContent.length, '字符');

                if (includeHeaders) {
                    return [true, null, {
                        content: textContent,
                        headers: headers,
                        status: response.status,
                        statusText: response.statusText
                    }];
                } else {
                    return [true, null, textContent];
                }

            } catch (fetchError) {
                clearTimeout(timeoutId);
                throw fetchError;
            }

        } catch (error) {
            if (error.name === 'AbortError') {
                downloadLog.error('请求超时:', timeout, 'ms');
                return [false, `请求超时 (${timeout}ms)`, null];
            }

            downloadLog.error('获取链接文本内容异常:', error);
            return [false, '获取链接文本内容失败: ' + error.message, null];
        }
    }

    /**
     * 完整的下载文本文件流程
     * 整合上述三个步骤,从文件信息直接获取文本内容
     * @param {Object} fileInfo - 文件信息
     * @param {string} fileInfo.etag - 文件MD5
     * @param {string|number} fileInfo.fileId - 文件ID
     * @param {string} fileInfo.s3keyFlag - S3 Key标志
     * @param {string} fileInfo.fileName - 文件名
     * @param {string|number} fileInfo.size - 文件大小
     * @param {string} preferredIsp - 优先选择的ISP线路(可选)
     * @param {Object} options - 可选参数
     * @returns {Promise<Array>} [是否成功, 错误信息, 文本内容]
     */
    async downloadTextFile(fileInfo, preferredIsp = null, options = {}) {
        try {
            downloadLog.log('开始下载文本文件:', fileInfo.fileName);

            // 1. 获取下载链接
            downloadLog.log('步骤1: 获取下载链接');
            const [linkSuccess, linkError, linkData] = await this.getDownloadLink(fileInfo, preferredIsp);
            if (!linkSuccess) {
                return [false, '获取下载链接失败: ' + linkError, null];
            }

            const { downloadLink } = linkData;

            // 2. 获取文本内容
            downloadLog.log('步骤2: 获取文本内容');
            const [contentSuccess, contentError, content] = await this.getLinkTextContent(downloadLink, options);

            if (!contentSuccess) {
                return [false, '获取文本内容失败: ' + contentError, null];
            }

            downloadLog.log('下载完成,文件:', fileInfo.fileName);
            return [true, null, content];

        } catch (error) {
            downloadLog.error('下载文本文件流程异常:', error);
            return [false, '下载文本文件失败: ' + error.message, null];
        }
    }

    /**
     * 通过文件ID获取文件信息并下载
     * 这是一个便捷方法,先通过fileId获取文件信息,然后下载
     * @param {string|number} fileId - 文件ID
     * @param {string} preferredIsp - 优先选择的ISP线路(可选)
     * @returns {Promise<Array>} [是否成功, 错误信息, 文本内容]
     */
    async downloadTextFileById(fileId, preferredIsp = null) {
        try {
            downloadLog.log('通过文件ID下载文本文件:', fileId);

            // 1. 先获取文件信息
            const fileInfoResponse = await this.getFileInfo([fileId]);
            // 检查文件大小
            if (fileInfoResponse.data.InfoList[0].Size > this.maxTextFileSize) {
                return [false, `文件过大,无法作为文本下载(最大支持 ${this.maxTextFileSize} 字节)`, null];
            }
            if (!fileInfoResponse.data.InfoList || fileInfoResponse.data.InfoList.length === 0) {
                return [false, '文件不存在或无法访问', null];
            }

            const fileData = fileInfoResponse.data.InfoList[0];

            // 2. 构建文件信息对象
            const fileInfo = {
                fileId: fileData.FileId,
                etag: fileData.Etag,
                s3keyFlag: fileData.S3KeyFlag,
                fileName: fileData.FileName,
                size: fileData.Size
            };

            // 为防止非文本文件过大造成卡顿,对文件最大值进行限制
            if (fileInfo.size > this.maxTextFileSize) {
                return [false, `文件过大,无法作为文本下载(最大支持 ${this.maxTextFileSize} 字节)`, null];
            }
            downloadLog.log('获取到文件信息:', fileInfo);

            // 3. 下载文件
            return await this.downloadTextFile(fileInfo, preferredIsp);

        } catch (error) {
            downloadLog.error('通过文件ID下载异常:', error);
            return [false, '通过文件ID下载失败: ' + error.message, null];
        }
    }

    /**
     * 下载文件并保存为Blob(适用于二进制文件)
     * @param {Object} fileInfo - 文件信息
     * @param {string} preferredIsp - 优先选择的ISP线路
     * @returns {Promise<Array>} [是否成功, 错误信息, Blob对象]
     */
    async downloadFileAsBlob(fileInfo, preferredIsp = null) {
        try {
            downloadLog.log('下载文件为Blob:', fileInfo.fileName);

            // 1. 获取下载链接
            const [linkSuccess, linkError, linkData] = await this.getDownloadLink(fileInfo, preferredIsp);
            if (!linkSuccess) {
                return [false, linkError, null];
            }

            const { downloadLink } = linkData;

            // 2. 下载为Blob
            const response = await fetch(downloadLink, {
                method: 'GET', headers: {
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
                }
            });

            if (!response.ok) {
                return [false, `HTTP ${response.status}: ${response.statusText}`, null];
            }

            const blob = await response.blob();
            downloadLog.log('下载Blob完成,大小:', blob.size, '字节');

            return [true, null, blob];

        } catch (error) {
            downloadLog.error('下载文件为Blob异常:', error);
            return [false, '下载文件失败: ' + error.message, null];
        }
    }
}

;// ./src/TableRowSelector.js
class TableRowSelector {
    /**
     * @param {number} tableIndex - 页面中第几个 .ant-table-wrapper (从0开始)
     */
    constructor(tableIndex = 0) {
        this.tableIndex = tableIndex;
    }

    /**
     * 从 React Fiber 树中提取 dataSource
     * @private
     * @returns {FileRecord[]}
     */
    _getDataSource() {
        const tables = document.querySelectorAll('.ant-table-wrapper');
        const table = tables[this.tableIndex];
        if (!table) return [];

        const fiberKey = Object.keys(table).find(k => k.startsWith('__reactFiber$'));
        if (!fiberKey) return [];

        const rootFiber = table[fiberKey];
        // depth 1: InternalTable 组件
        const internalTableFiber = rootFiber?.return;
        let data = internalTableFiber?.memoizedProps?.dataSource;

        // depth 2: Table 组件 (备用)
        if (!Array.isArray(data)) {
            data = internalTableFiber?.return?.memoizedProps?.dataSource;
        }

        return Array.isArray(data) ? data : [];
    }

    /**
     * 获取表格全部行数据
     * @returns {FileRecord[]}
     */
    getAll() {
        return structuredClone(this._getDataSource());
    }

    /**
     * 获取当前选中的行数据
     * @returns {FileRecord[]}
     */
    getSelection() {
        return structuredClone(this._getDataSource().filter(item => item.checked));
    }
}

;// ./src/ShareLinkManager.js



const ShareLinkManager_log = createLogger('ShareLink');

class ShareLinkManager {
    constructor(apiClient) {
        this.apiClient = apiClient;
        this.init();
    }

    init() {
        this.apiClient.init();
        this.progress = 0;
        this.progressDesc = "";
        this.taskCancel = false; // 取消当前任务的请求标志
        this.getFileInfoBatchSize = GlobalConfig.getFileInfoBatchSize;
        this.getFileInfoDelay = GlobalConfig.getFileInfoDelay;
        this.getFolderInfoDelay = GlobalConfig.getFolderInfoDelay;
        this.saveLinkDelay = GlobalConfig.saveLinkDelay;
        this.mkdirDelay = GlobalConfig.mkdirDelay;
        this.fileInfoList = []; // fileInfoList 在递归获取文件时使用的全局变量
        // this.scriptName = GlobalConfig.scriptName,
        // this.commonPath = "";
        this.COMMON_PATH_LINK_PREFIX_V2 = GlobalConfig.COMMON_PATH_LINK_PREFIX_V2;
        this.usesBase62EtagsInExport = GlobalConfig.usesBase62EtagsInExport;
        this.scriptVersion = GlobalConfig.scriptVersion;
        this.defaultExportName = GlobalConfig.DEFAULT_EXPORT_FILENAME;
        this.secondaryLinkUseJson = GlobalConfig.secondaryLinkUseJson;
    }

    /**
     * 递归获取指定文件夹ID下的所有文件信息
     * @param {*} parentFileId
     * @param folderName
     * @param {*} total 仅用来计算进度
     */
    async _getAllFileInfoByFolderId(parentFileId, folderName = '', total) {
        //log.log(await this.apiClient.getFileList(parentFileId));
        this.progressDesc = `正在扫描文件夹:${folderName}`;
        let progress = this.progress;

        const progressUpdater = setInterval(() => {
            //this.showProgressModal("生成秒传链接", , this.progressDesc);
            this.progress = progress + this.apiClient.progress / total;
            this.progressDesc = this.apiClient.progressDesc;
            // 不主动停止
            if (this.progress > 100) {
                clearInterval(progressUpdater);
                //setTimeout(() => this.hideProgressModal(), 500);
            }
        }, 500);
        const allFileInfoList = (await this.apiClient.getFileList(parentFileId)).data.InfoList.map(file => ({
            fileName: file.FileName, etag: file.Etag, size: file.Size, type: file.Type, fileId: file.FileId
        }));
        clearInterval(progressUpdater);

        // 分开文件和文件夹
        // 文件添加所在文件夹名称
        const fileInfo = allFileInfoList.filter(file => file.type !== 1);
        fileInfo.forEach(file => {
            file.path = folderName + file.fileName;
        });

        this.fileInfoList.push(...fileInfo);
        ShareLinkManager_log.log("获取文件列表,ID:", parentFileId);

        const directoryFileInfo = allFileInfoList.filter(file => file.type === 1);

        for (const folder of directoryFileInfo) {
            // 延时
            await new Promise(resolve => setTimeout(resolve, this.getFolderInfoDelay));

            // 任务取消,停止深入文件夹
            if (this.taskCancel) {
                this.progressDesc = "任务已取消";
                return;
            }
            await this._getAllFileInfoByFolderId(folder.fileId, folderName + folder.fileName + "/", total * directoryFileInfo.length);
        }
        this.progress = progress + 100 / total;
    }

    /**
     * 分批获取文件信息
     * @param {*} idList - 文件ID列表
     * @returns - 来自服务器的文件全面数据
     */
    async _getFileInfoBatch(idList) {
        const total = idList.length;
        let completed = 0;
        let allFileInfo = [];
        for (let i = 0; i < total; i += this.getFileInfoBatchSize) {
            const batch = idList.slice(i, i + this.getFileInfoBatchSize);
            try {
                const response = await this.apiClient.getFileInfo(batch);
                allFileInfo = allFileInfo.concat(response.data.InfoList || []);
            } catch (e) {
                ShareLinkManager_log.error('获取文件信息失败:', e);
            }
            completed += batch.length;
            // 不能走到100,否则会自动消失,下面获取文件夹还用使用
            this.progress = Math.round((completed / total) * 100 - 1);
            this.progressDesc = `正在获取文件信息... (${completed} / ${total})`;
            await new Promise(resolve => setTimeout(resolve, this.getFileInfoDelay));
        }
        return allFileInfo.map(file => ({
            fileName: file.FileName, etag: file.Etag, size: file.Size, type: file.Type, fileId: file.FileId
        }));
    }

    /**
     * 获取fileInfoList的公共路径
     * @returns commonPath
     */
    async _getCommonPath(fileInfoList) {
        if (!fileInfoList || fileInfoList.length === 0) return '';
        // 提取所有路径并转换为目录组件数组
        const pathArrays = fileInfoList.map(file => {
            const path = file.path || '';
            // 移除路径末尾的文件名(如果有)
            const lastSlashIndex = path.lastIndexOf('/');
            return lastSlashIndex === -1 ? [] : path.substring(0, lastSlashIndex).split('/');
        });

        // 找出最长的公共前缀
        let commonPrefix = [];
        const firstPath = pathArrays[0];

        for (let i = 0; i < firstPath.length; i++) {
            const currentComponent = firstPath[i];
            const allMatch = pathArrays.every(pathArray => pathArray.length > i && pathArray[i] === currentComponent);

            if (allMatch) {
                commonPrefix.push(currentComponent);
            } else {
                break;
            }
        }

        // 将公共前缀组件组合为路径字符串
        const commonPath = commonPrefix.length > 0 ? commonPrefix.join('/') + '/' : '';
        // this.commonPath = commonPath;
        return commonPath;
    }

    /**
     * 获取所有选择的文件,进入文件夹
     * @param {FileRecord[]} selectedFiles - 来自selector.getSelection()
     * @returns  - [boolean, 错误信息, 文件信息列表,commonPath]
     */
    async _getSelectedFilesInfo(selectedFiles) {
        this.fileInfoList = [];
        if (!selectedFiles || selectedFiles.length === 0) {
            return [false, "未选择文件", null];
        }
        let fileSelectFolderInfoList = [];

        this.progress = 10;
        this.progressDesc = "正在递归获取选择的文件..."

        const allFileInfo = selectedFiles.map(file => ({
            fileName: file.FileName, etag: file.Etag, size: file.Size, type: file.Type, fileId: file.FileId
        }));
        const fileInfo = allFileInfo.filter(file => file.type !== 1);
        fileInfo.forEach(file => {
            file.path = file.fileName;
        });
        this.fileInfoList.push(...fileInfo);
        fileSelectFolderInfoList = allFileInfo.filter(file => file.type === 1);

        // 处理文件夹,递归获取全部文件
        // this.progressDesc = "正在递归获取选择的文件,如果文件夹过多则可能耗时较长";
        for (let i = 0; i < fileSelectFolderInfoList.length; i++) {
            const folderInfo = fileSelectFolderInfoList[i];
            this.progress = Math.round((i / fileSelectFolderInfoList.length) * 100);
            await new Promise(resolve => setTimeout(resolve, this.getFolderInfoDelay));
            // 任务取消
            if (this.taskCancel) {
                this.progressDesc = "任务已取消";
                return [true, "任务已取消", this.fileInfoList];
                // 已经获取的文件保留
            }

            await this._getAllFileInfoByFolderId(folderInfo.fileId, folderInfo.fileName + "/", fileSelectFolderInfoList.length);
        }
        // 处理文件夹路径
        // 检查commonPath
        const commonPath = await this._getCommonPath(this.fileInfoList);
        // 去除文件夹路径中的公共路径
        if (commonPath) {
            this.fileInfoList.forEach(info => {
                // 切片
                info.path = info.path.slice(commonPath.length);
            });
        }

        return [true, null, this.fileInfoList, commonPath];
    }

    /**
     * 从选择文件生成分享链接
     * @param {*} fileSelectionDetails - 来自selector.getSelection()
     * @returns {Promise<string>} - 分享链接,如果未选择文件则返回空字符串
     */
    async generateShareLink(fileSelectionDetails, jsonExport = false) {
        this.progress = 0;
        this.progressDesc = "准备获取文件信息...";

        // 获取选中的文件(文件夹)的详细信息
        const [resultSuccess, resultError, fileInfoList, commonPath] = await this._getSelectedFilesInfo(fileSelectionDetails);
        if (!resultSuccess) return [false, resultError, null];
        //// if (hasFolder) alert("文件夹暂时无法秒传,将被忽略");
        let allFilePath = [];
        for (const fileInfo of fileInfoList) {
            if (fileInfo.type !== 1) {
                allFilePath.push(fileInfo.path);
            }
        }
        this.progressDesc = "秒传链接生成完成";
        return [...this.buildShareLink(fileInfoList, commonPath, jsonExport), allFilePath];
    }

    /**
     * 拼接链接 etag: 来自服务器md5,由函数转换格式
     * @param {*} fileInfoList - {etag: string, size: number, path: string, fileName: string}
     */
    buildShareLink(fileInfoList, commonPath, jsonExport = false) {
        if (jsonExport) {
            return this._buildJsonShareLink(fileInfoList, commonPath, this.usesBase62EtagsInExport);
        } else {
            const shareLinkFileInfo = fileInfoList.map(info => {
                //if (info.type === 0) {
                return [this.usesBase62EtagsInExport ? this._hexToBase62(info.etag) : info.etag, info.size, info.path.replace(/[%#$]/g, '')].join('#');
                //}
            }).filter(Boolean).join('$');
            const shareLink = `${this.COMMON_PATH_LINK_PREFIX_V2}${commonPath}%${shareLinkFileInfo}`;
            return [true, null, shareLink];
        }
    }

    _isValidEtag(etag) {
        // 简单校验etag格式为32位十六进制字符串或Base62字符串
        const hexRegex = /^[a-fA-F0-9]{32}$/;
        const base62Regex = /^[A-Za-z0-9]{22}$/;
        return hexRegex.test(etag) || base62Regex.test(etag);
    }

    /**
     * 解析文本秒传链接
     * @param {*} shareLink     秒传链接
     * @param {*} InputUsesBase62  输入是否使用Base62
     * @param {*} outputUsesBase62 函数输出是否使用Base62,本脚本中使用hex传递,默认false
     * @returns {Array} - [boolean, 错误信息, 文件信息列表 - [{etag: string, size: number, path: string, fileName: string}], 失败列表, commonPath]
     */
    _parseTextShareLink(shareLink, InputUsesBase62 = true, outputUsesBase62 = false) {
        // Why use Base62 ???
        // 本脚本采用hex传递
        // 兼容旧版本,检查是否有链接头
        let commonPath = '';
        let shareFileInfo = '';
        if (shareLink.slice(0, 4) === "123F") {
            const commonPathLinkPrefix = shareLink.split('$')[0];
            shareLink = shareLink.replace(`${commonPathLinkPrefix}$`, '');

            if (commonPathLinkPrefix + "$" === this.COMMON_PATH_LINK_PREFIX_V2) {
                commonPath = shareLink.split('%')[0];
                shareFileInfo = shareLink.replace(`${commonPath}%`, '');

            } else {
                ShareLinkManager_log.error('不支持的公共路径格式', commonPathLinkPrefix);
                return [false, '不支持的公共路径格式', null];
            }

        } else {
            shareFileInfo = shareLink;
            InputUsesBase62 = false;
        }

        const shareLinkList = Array.from(shareFileInfo.replace(/\r?\n/g, '$').split('$'));
        // this.commonPath = commonPath;
        let failList = [];
        const fileList = shareLinkList.map(singleShareLink => {
            const singleFileInfoList = singleShareLink.split('#');
            if (singleFileInfoList.length < 3) return null;
            const etag = InputUsesBase62 ? (outputUsesBase62 ? singleFileInfoList[0] : this._base62ToHex(singleFileInfoList[0])) : (outputUsesBase62 ? this._hexToBase62(singleFileInfoList[0]) : singleFileInfoList[0]);
            let failed = false;
            // etag校验
            if (!this._isValidEtag(etag)) {
                ShareLinkManager_log.error('无效的etag:', etag);
                failed = true;
            }
            const size = singleFileInfoList[1];
            if (isNaN(size) || Number(size) < 0) {
                ShareLinkManager_log.error('无效的文件大小:', size);
                failed = true;
            }
            if (!singleFileInfoList[2]) {
                ShareLinkManager_log.error('无效的文件路径:', singleFileInfoList[2]);
                failed = true;
            }
            if (failed) {
                failList.push({
                    etag: etag,
                    size: size,
                    path: singleFileInfoList[2],
                    fileName: singleFileInfoList[2].split('/').pop()
                });
                return null;
            }
            return {
                // etag: InputUsesBase62 ? (outputUsesBase62 ? singleFileInfoList[0] : this._base62ToHex(singleFileInfoList[0])) : (outputUsesBase62 ? this._hexToBase62(singleFileInfoList[0]) : singleFileInfoList[0]),
                etag: etag,
                size: size,
                path: singleFileInfoList[2],
                fileName: singleFileInfoList[2].split('/').pop()
            };
        }).filter(Boolean);
        if (fileList.length === 0) {
            return [false, '未解析到有效的文件信息', null, failList];
        }
        return [true, null, fileList, failList, commonPath];
    }

    /**
     * 自动判断秒传链接格式并解析
     * @param {string} shareLink
     */
    async parseShareLink(shareLink) {
        let result = null;
        try {
            // 尝试作为JSON解析
            const jsonData = this.safeParse(shareLink);
            if (jsonData) {
                result = await this._parseJsonShareLink(jsonData);
            } else {
                // 作为普通秒传链接处理
                result = await this._parseTextShareLink(shareLink, this.usesBase62EtagsInExport, false);
            }
        } catch (error) {
            return [false, '保存失败: ' + error.message, result];
        }
        return result;
    }

    /**
     * 先创建文件夹,给shareFileList添加上parentFolderId,便于保存文件
     * @param {*} fileList - {etag: string, size: number, path: string, fileName: string}
     * @returns shareFileList - {etag: string, size: number, path: string, fileName: string, parentFolderId: number}
     */
    async _makeDirForFiles(shareFileList, commonPath) {
        const total = shareFileList.length;
        // 文件夹创建,并为shareFileList添加parentFolderId------------------------------------
        // 记录文件夹(path)
        this.progressDesc = `正在创建文件夹...`;
        let folder = {};
        // 如果存在commonPath,先创建文件夹
        const rootFolderId = await this.apiClient.getParentFileId();
        if (commonPath) {
            const commonPathParts = commonPath.split('/').filter(part => part !== '');
            let currentParentId = rootFolderId;

            for (let i = 0; i < commonPathParts.length; i++) {
                const currentPath = commonPathParts.slice(0, i + 1).join('/');
                const folderName = commonPathParts[i];

                if (!folder[currentPath]) {
                    const newFolder = await this.apiClient.mkdir(currentParentId, folderName);
                    await new Promise(resolve => setTimeout(resolve, this.mkdirDelay));
                    folder[currentPath] = newFolder.folderFileId;
                }

                currentParentId = folder[currentPath];
            }
        } else {
            folder[''] = rootFolderId;
        }

        for (let i = 0; i < shareFileList.length; i++) {
            const item = shareFileList[i];
            const itemPath = item.path.split('/').slice(0, -1);

            // 记得去掉commonPath末尾的斜杠
            let nowParentFolderId = folder[commonPath.slice(0, -1)] || rootFolderId;
            for (let i = 0; i < itemPath.length; i++) {
                const path = itemPath.slice(0, i + 1).join('/');
                if (!folder[path]) {
                    const newFolderID = await this.apiClient.mkdir(nowParentFolderId, itemPath[i]);
                    await new Promise(resolve => setTimeout(resolve, this.mkdirDelay));
                    folder[path] = newFolderID.folderFileId;
                    nowParentFolderId = newFolderID.folderFileId;
                } else {
                    nowParentFolderId = folder[path];
                }

                // 任务取消
                if (this.taskCancel) {
                    this.progressDesc = "任务已取消";
                    return shareFileList;
                }
            }
            shareFileList[i].parentFolderId = nowParentFolderId;
            this.progress = Math.round((i / total) * 100);
            this.progressDesc = `正在创建文件夹... (${i + 1} / ${total})`;
        }
        return shareFileList;
    }

    /**
     * 保存文件列表
     * @param {Array} shareFileList - 带parentFolderId的 - _makeDirForFiles - {etag: string, size: number, path: string, fileName: string, parentFolderId: number}
     * @returns {Object} -  {success: [], failed: [fileInfo]}
     */
    async _saveFileList(shareFileList) {
        let completed = 0;
        let success = 0;
        let failed = 0;
        let successList = [];
        let failedList = [];
        const total = shareFileList.length;
        // 获取文件 -----------------------------
        for (let i = 0; i < shareFileList.length; i++) {

            // 任务取消
            if (this.taskCancel) {
                this.progressDesc = "任务已取消";
                break;
            }

            const fileInfo = shareFileList[i];
            if (i > 0) {
                await new Promise(resolve => setTimeout(resolve, this.saveLinkDelay));
            }

            const reuse = await this.apiClient.getFile({
                etag: fileInfo.etag, size: fileInfo.size, fileName: fileInfo.fileName
            }, fileInfo.parentFolderId);
            if (reuse[0]) {
                success++;
                successList.push(fileInfo);
            } else {
                failed++;
                ShareLinkManager_log.error('保存文件失败:', fileInfo.fileName);
                fileInfo.error = reuse[1];
                failedList.push(fileInfo);
            }
            completed++;
            ShareLinkManager_log.log('已保存:', fileInfo.fileName);
            this.progress = Math.round((completed / total) * 100);
            this.progressDesc = `(成功: ${success},失败: ${failed})
            正在保存第 ${completed} / ${total} 个文件(${fileInfo.fileName})...`;
        }
        // this.progress = 100;
        // this.progressDesc = "保存完成";
        return {
            success: successList, failed: failedList
        };
    }

    /**
     *  保存秒传链接(自动判断格式)
     * @param {string} content
     * @returns {Promise<object>} - 保存结果
     * {success: [], failed: []}
     */
    async saveShareLink(content) {
        let saveResult = { success: [], failed: [] };

        const fileInfoList = await this.parseShareLink(content);
        if (!fileInfoList[0]) {
            saveResult.failed.push(...fileInfoList[3]); // 添加解析失败的文件
            return [false, '保存失败: ' + fileInfoList[1], saveResult];
        }
        saveResult = await this._saveFileList(await this._makeDirForFiles(fileInfoList[2], fileInfoList[4]));
        saveResult.failed.push(...fileInfoList[3]); // 添加解析失败的文件
        saveResult.commonPath = fileInfoList[4];
        return [true, null, saveResult];
    }

    async saveShareLinkOnlyText(shareLink, fileName) {
        return this.apiClient.createTextFileInCurrentFolder(fileName, shareLink);
    }

    /**
     * 重试保存失败的文件
     * @param {*} FileList - 包含parentFolderId - {etag: string, size: number, path: string, fileName: string, parentFolderId: number}
     * 失败的文件列表 - this.saveShareLink()[2].failed
     * @returns
     */
    // // TODO commonPath 处理
    async retrySaveFailed(FileList) {
        return [true, null, await this._saveFileList(FileList)];
    }

    // ------------------二级秒传链接相关----------------------
    /**
     * 获取文件内容为文本
     * @param {string} fileId
     * @returns [boolean, string, string] - 是否成功, 错误信息, 文本内容
     */
    async getFileContentAsText(fileId) {
        const fileTextInfo = await this.apiClient.downloadTextFileById(fileId);
        if (!fileTextInfo[0]) {
            return [false, fileTextInfo[1], null];
        }
        return [true, null, fileTextInfo[2]];
    }

    /**
     * 从文本文件获取并保存秒传链接
     * @param {string} fileId - 二级秒传链接文件ID
     * @returns
     */
    async saveShareLinkFile(fileId) {
        const [downloadSuccess, downloadError, fileContent] = await this.getFileContentAsText(fileId);
        if (!downloadSuccess) {
            return [false, '获取文件内容失败: ' + downloadError, null];
        }
        return await this.saveShareLink(fileContent);
    }

    async saveSecondaryShareLink(secondaryLink, seedFilePathId = null) {

        if (seedFilePathId && seedFilePathId.toString().length !== 8) {
            return [false, '种子文件路径ID无效,请清除路径', null];
        }
        // 提取文件信息
        const secondaryFileInfoRes = await this.parseShareLink(secondaryLink);
        if (!secondaryFileInfoRes[0]) {
            return [false, '解析二级秒传链接失败: ' + secondaryFileInfoRes[1], null];
        }
        const secondaryFileInfo = secondaryFileInfoRes[2];
        if (secondaryFileInfo.length !== 1) {
            return [false, '二级秒传链接格式错误,应该只包含一个文件', null];
        }
        // 保存文件(要先保存才能获取文件内容)
        this.progress = 10;
        this.progressDesc = "正在保存二级秒传链接文件...";
        const saveResult = await this.apiClient.getFile({
            etag: secondaryFileInfo[0].etag,
            size: secondaryFileInfo[0].size,
            fileName: secondaryFileInfo[0].fileName
        }, seedFilePathId);
        if (!saveResult[0]) {
            return [false, '保存二级秒传链接文件失败: ' + saveResult[1], null];
        }
        // 获取文件内容
        this.progress = 50;
        this.progressDesc = "正在获取二级秒传链接文件内容...";
        const getResult = await this.getFileContentAsText(saveResult[2]);
        const [downloadSuccess, downloadError, fileContent] = getResult;
        if (!downloadSuccess) {
            return [false, '获取文件内容失败: ' + downloadError, null];
        }
        this.progress = 70;
        this.progressDesc = "正在保存秒传链接...";
        return await this.saveShareLink(fileContent);
    }

    /**
     *
     * @param {dict} fileSelectionDetails - 文件选择
     * @param {*} fileName - 二级秒传链接文件名
     * @returns
     */
    async generateSecondaryShareLink(fileSelectionDetails, fileName, seedFilePathId = null) {
        if (seedFilePathId && seedFilePathId.toString().length !== 8) {
            return [false, '种子文件路径ID无效,请清除路径', null];
        }
        // 固定parentFolderId为当前文件夹,防止用户切换页面
        let parentFolderId = null;
        if (seedFilePathId) {
            parentFolderId = seedFilePathId;
        } else {
            parentFolderId = await this.apiClient.getParentFileId();
        }
        // 先根据fileSelectionDetails 生成一级秒传链接
        const [linkSuccess, linkError, shareLink] = await this.generateShareLink(fileSelectionDetails, this.secondaryLinkUseJson);
        if (!linkSuccess) {
            return [false, '生成一级秒传链接失败: ' + linkError, null];
        }
        // 判断文件名
        if (!fileName || fileName.trim() === '') {
            fileName = await this.getExportFilename(shareLink) + '.123fastlink.' + (this.secondaryLinkUseJson ? 'json' : 'txt');
        }
        // 然后保存为文本文件
        const saveResult = await this.apiClient.createTextFileInFolder(fileName, shareLink, parentFolderId);
        if (!saveResult[0]) {
            return [false, '保存一级秒传链接文件失败: ' + saveResult[1], null];
        }
        // const fileId =  saveResult[2];
        const fileInfo = saveResult[3]; // {fileName, etag, size}
        fileInfo.path = fileName;
        // 最后生成二级秒传链接
        const secondaryShareLink = this.buildShareLink([fileInfo], '', false)[2];
        return [true, null, secondaryShareLink];
    }

    // -------------------JSON相关-----------------------
    safeParse(str) {
        try {
            return JSON.parse(str);
        } catch {
            return null;
        }
    }

    _base62chars() {
        return '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    }

    _hexToBase62(hex) {
        if (!hex) return '';
        let num = BigInt('0x' + hex);
        if (num === 0n) return '0';
        let chars = [];
        const base62 = this._base62chars();
        while (num > 0n) {
            chars.push(base62[Number(num % 62n)]);
            num = num / 62n;
        }
        return chars.reverse().join('');
    }

    _base62ToHex(base62) {
        if (!base62) return '';
        const chars = this._base62chars();
        let num = 0n;
        for (let i = 0; i < base62.length; i++) {
            num = num * 62n + BigInt(chars.indexOf(base62[i]));
        }
        let hex = num.toString(16);
        if (hex.length % 2) hex = '0' + hex;
        while (hex.length < 32) hex = '0' + hex;
        return hex;
    }


    /**
     * 解析JSON格式的秒传链接
     * @param {object} jsonData
     * @returns {Array} - [boolean, string, [{etag: string, size: number, path: string, fileName: string}], commonPath] - 是否成功, 错误信息, 文件列表
     */
    _parseJsonShareLink(jsonData) {
        // 如果是字符串,先尝试解析为JSON对象
        if (typeof jsonData === 'string') {
            jsonData = this.safeParse(jsonData);
            if (!jsonData) {
                return [false, '无效的JSON格式', null];
            }
        }
        let failedList = [];
        try {
            const commonPath = jsonData['commonPath'] || '';
            // this.commonPath = commonPath;
            const shareFileList = jsonData['files'];
            if (jsonData['usesBase62EtagsInExport']) {
                shareFileList.forEach(file => {
                    file.etag = this._base62ToHex(file.etag);
                    if (!this._isValidEtag(file.etag)) {
                        ShareLinkManager_log.error('无效的etag:', file.etag);
                        failedList.push({
                            etag: file.etag, size: file.size, path: file.path, fileName: file.path.split('/').pop()
                        });
                    }
                });
            }
            shareFileList.forEach(file => {
                file.fileName = file.path.split('/').pop();
            });
            return [true, null, shareFileList, failedList, commonPath];
        } catch (error) {
            ShareLinkManager_log.error('解析JSON格式秒传链接失败:', error);
            return [false, '解析JSON格式秒传链接失败: ' + error.message, null, null];
        }
    }

    // 格式化文件大小
    _formatSize(size) {
        const KB = 1024;
        const MB = KB * 1024;
        const GB = MB * 1024;
        const TB = GB * 1024;
        const PB = TB * 1024;

        if (size < KB) return size + ' B';
        if (size < MB) return (size / KB).toFixed(2) + ' KB';
        if (size < GB) return (size / MB).toFixed(2) + ' MB';
        if (size < TB) return (size / GB).toFixed(2) + ' GB';
        if (size < PB) return (size / TB).toFixed(2) + ' TB';
        return (size / PB).toFixed(2) + ' PB';
    }

    validateJson(json) {
        return (json && Array.isArray(json.files) && json.files.every(f => f.etag && f.size && f.path));
    }

    /**
     * 将秒传链接转换为JSON格式
     * @param {*} shareLink
     * @returns
     */
    textShareLinkToJson(shareLink) {
        const [, , fileInfoList, , commonPath] = this._parseTextShareLink(shareLink);
        // const commonPath = this.commonPath;
        return this._buildJsonShareLink(fileInfoList, commonPath, this.usesBase62EtagsInExport);
    }

    _buildJsonShareLink(fileInfoList, commonPath = '', usesBase62EtagsInExport = false) {
        if (fileInfoList.length === 0) {
            ShareLinkManager_log.error('解析秒传链接失败:', shareLink);
            return [false, '解析秒传链接失败: 文件列表为空', null];
        }
        // if (usesBase62EtagsInExport) {
        //     fileInfoList.forEach(f => {
        //         f.etag = this._hexToBase62(f.etag);
        //     });
        // }
        const totalSize = fileInfoList.reduce((sum, f) => sum + Number(f.size), 0);
        const jsonData = {
            scriptVersion: this.scriptVersion,
            exportVersion: "1.0",
            usesBase62EtagsInExport: usesBase62EtagsInExport,
            commonPath: commonPath,
            totalFilesCount: fileInfoList.length,
            totalSize,
            formattedTotalSize: this._formatSize(totalSize),
            files: fileInfoList.map(f => ({
                // 去掉fileName
                ...f, fileName: undefined, etag: usesBase62EtagsInExport ? this._hexToBase62(f.etag) : f.etag
            }))
        };
        return [true, null, JSON.stringify(jsonData, null, 2)];
    }

    /**
     * 从JSON格式生成文本秒传链接
     * @param {string} jsonText
     * @returns    {boolean, string, string} - 是否成功, 错误信息, 秒传链接
     */
    jsonToTextShareLink(jsonText) {
        const jsonData = this.safeParse(jsonText);
        if (!this.validateJson(jsonData)) {
            return [false, '无效的JSON格式', null];
        }
        const shareFileList = this._parseJsonShareLink(jsonData);
        const [success, errorMsg, filePath, fileName, etag] = shareFileList;
        if (!success) {
            return [false, '解析JSON失败: ' + errorMsg, null];
        }
        return this.buildShareLink(filePath, etag, false);
    }

    // -------------------工具函数-----------------------
    /**
     * 获取导出文件名,默认根据公共路径或第一个文件名生成,不带扩展名
     * @param {string} shareLink(text)
     * @param {string} defaultName
     * @returns
     */
    async getExportFilename(shareLink, defaultName = this.defaultExportName) {
        const [success, , fileInfoList, , commonPath] = await this.parseShareLink(shareLink);
        if (!success) {
            return defaultName;
        }
        if (commonPath) {
            const commonPathClean = commonPath.replace(/\/$/, '');
            return `${commonPathClean}`;
        }
        // 获取第一个文件名

        if (fileInfoList.length > 0) {
            const firstFileName = fileInfoList[0].fileName;
            const baseName = firstFileName.split('.')[0] || 'export';
            return `${baseName}`;
        }
    }

    linkChecker(shareLink) {
        if (!shareLink || shareLink.trim() === '') {
            return [false, '链接为空'];
        }
        if (this._parseTextShareLink(shareLink)[0]) {
            return [true, null, "text"];
        }
        if (this._parseJsonShareLink(shareLink)[0]) {
            return [true, null, "json"];
        }
        return [false, '无效的秒传链接格式', null];
    }

}


;// ./src/styles.css
/* harmony default export */ const styles = (":root{--primary-color:#6366f1;--primary-hover:#4f46e5;--secondary-color:#10b981;--secondary-hover:#059669;--danger-color:#ef4444;--danger-hover:#dc2626;--warning-color:#f59e0b;--warning-hover:#d97706;--info-color:#3b82f6;--info-hover:#2563eb;--background:#ffffff;--surface:#f8fafc;--border:#e2e8f0;--text-primary:#1e293b;--text-secondary:#64748b;--text-tertiary:#94a3b8;--shadow-sm:0 1px 2px 0 rgba(0,0,0,0.05);--shadow:0 4px 6px -1px rgba(0,0,0,0.1),0 2px 4px -1px rgba(0,0,0,0.06);--shadow-lg:0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -2px rgba(0,0,0,0.05);--shadow-xl:0 20px 25px -5px rgba(0,0,0,0.1),0 10px 10px -5px rgba(0,0,0,0.04);--radius-sm:6px;--radius:12px;--radius-lg:16px;--transition:all 0.2s cubic-bezier(0.4,0,0.2,1)}\n.fs-modal-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.5);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;z-index:9999;animation:fadeIn 0.2s ease-out}\n.modal{background:var(--background);border-radius:var(--radius-lg);box-shadow:var(--shadow-xl);width:90%;max-width:500px;max-height:90vh;overflow:hidden;border:1px solid var(--border);transform:translateY(0);animation:slideUp 0.3s cubic-bezier(0.4,0,0.2,1)}\n.fs-modal-header{padding:24px 24px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}\n.fs-modal-title{font-size:20px;font-weight:600;color:var(--text-primary);display:flex;align-items:center;gap:8px}\n.fs-modal-title svg{width:20px;height:20px}\n.fs-modal-close{background:none;border:none;height:32px;display:flex;align-items:center;justify-content:center;color:var(--text-secondary);cursor:pointer;transition:var(--transition)}\n.fs-modal-close:hover{background:var(--surface);color:var(--text-primary)}\n.fs-modal-content{padding:24px}\n.fs-modal-footer{padding:16px 24px 24px;border-top:1px solid var(--border);display:flex;gap:12px;justify-content:flex-end}\n.fs-file-input{display:none}\n.fs-file-list-container{background:var(--surface);border-radius:var(--radius);padding:16px;margin-bottom:20px;max-height:200px;overflow-y:auto}\n.fs-file-list-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}\n.fs-file-count{font-size:13px;color:var(--text-secondary);font-weight:500}\n.fs-file-list{display:flex;flex-direction:column;gap:8px}\n.fs-file-item{font-size:13px;color:var(--text-primary);padding:8px 12px;background:white;border-radius:var(--radius-sm);border:1px solid var(--border);word-break:break-all;line-height:1.4}\n.modal textarea{width:100%;min-height:120px;padding:16px;border:2px solid var(--border);border-radius:var(--radius);background:var(--surface);color:var(--text-primary);font-family:'JetBrains Mono','Consolas','Monaco',monospace;font-size:13px;line-height:1.5;resize:vertical;transition:var(--transition);box-sizing:border-box}\n.modal textarea:focus{outline:none;border-color:var(--primary-color);box-shadow:0 0 0 3px rgba(99,102,241,0.1)}\n.modal textarea.drag-over{border-color:var(--primary-color);background:rgba(99,102,241,0.05)}\n.button-group{display:flex;gap:12px;align-items:center}\n.btn{padding:10px 20px;border-radius:var(--radius);font-size:14px;font-weight:500;border:none;cursor:pointer;transition:var(--transition);display:inline-flex;align-items:center;justify-content:center;gap:8px;min-width:100px}\n.btn:disabled{opacity:0.5;cursor:not-allowed}\n.fs-btn-primary{background:linear-gradient(135deg,var(--primary-color),var(--primary-hover));color:white;box-shadow:var(--shadow)}\n.fs-btn-primary:hover:not(:disabled){transform:translateY(-1px);box-shadow:var(--shadow-lg)}\n.fs-btn-secondary{background:linear-gradient(135deg,var(--secondary-color),var(--secondary-hover));color:white;box-shadow:var(--shadow)}\n.fs-btn-secondary:hover:not(:disabled){transform:translateY(-1px);box-shadow:var(--shadow-lg)}\n.fs-btn-outline{background:white;color:var(--text-primary);border:1px solid var(--border)}\n.fs-btn-outline:hover:not(:disabled){background:var(--surface);border-color:var(--text-secondary)}\n.fs-btn-danger{background:var(--danger-color);color:white}\n.fs-btn-danger:hover:not(:disabled){background:var(--danger-hover)}\n.dropdown{position:relative}\n.fs-dropdown-toggle{display:inline-flex;align-items:center;gap:4px}\n.fs-dropdown-menu{position:absolute;bottom:100%;left:0;background:white;border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-lg);min-width:140px;z-index:1001;margin-bottom:8px;opacity:0;transform:translateY(10px);visibility:hidden;transition:var(--transition)}\n.dropdown:hover .fs-dropdown-menu{opacity:1;transform:translateY(0);visibility:visible}\n.fs-dropdown-item{padding:10px 16px;font-size:13px;color:var(--text-primary);cursor:pointer;transition:var(--transition);display:flex;align-items:center;gap:8px}\n.fs-dropdown-item:hover{background:var(--surface)}\n.fs-dropdown-item:first-child{border-radius:var(--radius) var(--radius) 0 0}\n.fs-dropdown-item:last-child{border-radius:0 0 var(--radius) var(--radius)}\n.fs-dropdown-divider{height:1px;background:var(--border);margin:4px 0}\n.toast{position:fixed;top:24px;right:24px;background:white;color:var(--text-primary);padding:12px 20px;border-radius:var(--radius);box-shadow:var(--shadow-lg);z-index:10002;font-size:14px;max-width:320px;animation:slideInRight 0.3s cubic-bezier(0.4,0,0.2,1);border-left:4px solid var(--info-color);display:flex;align-items:center;gap:12px}\n.toast.success{border-left-color:var(--secondary-color)}\n.toast.error{border-left-color:var(--danger-color)}\n.toast.warning{border-left-color:var(--warning-color)}\n.toast.info{border-left-color:var(--info-color)}\n.toast-icon{width:20px;height:20px}\n.fs-progress-modal{animation:modalSlideIn 0.3s cubic-bezier(0.4,0,0.2,1)}\n.fs-progress-content{padding:24px;text-align:center}\n.fs-progress-title{font-size:18px;font-weight:600;color:var(--text-primary);margin-bottom:20px;word-break:break-all;line-height:1.4}\n.fs-progress-bar-container{height:8px;background:var(--surface);border-radius:4px;overflow:hidden;margin-bottom:12px}\n.fs-progress-bar{height:100%;background:linear-gradient(90deg,var(--primary-color),var(--secondary-color));border-radius:4px;transition:width 0.3s ease}\n.fs-progress-info{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}\n.fs-progress-percent{font-size:16px;font-weight:600;color:var(--primary-color)}\n.fs-progress-desc{font-size:13px;color:var(--text-secondary);text-align:left;background:var(--surface);padding:12px;border-radius:var(--radius);margin-top:16px;word-break:break-all;line-height:1.4}\n.fs-progress-minimize-btn{position:absolute;top:16px;right:16px;width:32px;height:32px;border-radius:50%;background:var(--surface);border:1px solid var(--border);color:var(--text-secondary);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:var(--transition)}\n.fs-progress-minimize-btn:hover{background:var(--border);color:var(--text-primary)}\n.minimized-widget{position:fixed;right:24px;bottom:24px;background:white;border-radius:var(--radius);box-shadow:var(--shadow-lg);padding:12px 16px;z-index:10005;min-width:240px;cursor:pointer;transition:var(--transition);border:1px solid var(--border)}\n.minimized-widget:hover{transform:translateY(-2px);box-shadow:var(--shadow-xl)}\n.fs-widget-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}\n.fs-widget-title{font-size:12px;font-weight:500;color:var(--text-primary)}\n.fs-widget-badge{background:var(--danger-color);color:white;font-size:11px;font-weight:600;padding:2px 8px;border-radius:10px}\n.fs-widget-progress{display:flex;align-items:center;gap:12px}\n.fs-widget-bar{flex:1;height:4px;background:var(--surface);border-radius:2px;overflow:hidden}\n.fs-widget-fill{height:100%;background:linear-gradient(90deg,var(--primary-color),var(--secondary-color));border-radius:2px}\n.fs-widget-percent{font-size:12px;font-weight:600;color:var(--primary-color);min-width:40px}\n.fs-task-list-container{margin-top:20px}\n.fs-task-toggle{width:100%;padding:10px 16px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);color:var(--text-secondary);font-size:13px;display:flex;align-items:center;justify-content:space-between;cursor:pointer;transition:var(--transition)}\n.fs-task-toggle:hover{background:#f1f5f9}\n.fs-task-toggle.active{background:var(--primary-color);color:white;border-color:var(--primary-color)}\n.fs-task-list{max-height:160px;overflow-y:auto;border:1px solid var(--border);border-top:none;border-radius:0 0 var(--radius) var(--radius);background:white;display:none}\n.fs-task-list.show{display:block}\n.fs-task-item{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;transition:var(--transition)}\n.fs-task-item:last-child{border-bottom:none}\n.fs-task-item.current{background:rgba(99,102,241,0.05)}\n.fs-task-info{display:flex;align-items:center;gap:8px}\n.fs-task-icon{width:12px;height:12px;border-radius:50%}\n.fs-task-icon.generate{background:var(--secondary-color)}\n.fs-task-icon.save{background:var(--info-color)}\n.fs-task-icon.retry{background:var(--warning-color)}\n.fs-task-name{font-size:13px;color:var(--text-primary)}\n.fs-task-status{font-size:12px;color:var(--text-secondary)}\n.fs-task-remove{width:24px;height:24px;border-radius:50%;border:none;background:var(--surface);color:var(--text-secondary);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:var(--transition)}\n.fs-task-remove:hover{background:var(--danger-color);color:white}\n.fs-task-remove:disabled{opacity:0.5;cursor:not-allowed}\n.fs-results-content{text-align:left}\n.fs-results-stats{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px}\n.fs-stat-card{padding:16px;border-radius:var(--radius);text-align:center}\n.fs-stat-card.success{background:rgba(16,185,129,0.1);border:1px solid rgba(16,185,129,0.2)}\n.fs-stat-card.failed{background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.2)}\n.fs-stat-value{font-size:24px;font-weight:700;margin-bottom:4px}\n.fs-stat-value.success{color:var(--secondary-color)}\n.fs-stat-value.failed{color:var(--danger-color)}\n.fs-stat-label{font-size:12px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.5px}\n.fs-failed-list{max-height:200px;overflow-y:auto;background:var(--surface);border-radius:var(--radius);padding:12px}\n.fs-failed-item{padding:8px 12px;background:white;border-radius:var(--radius-sm);border:1px solid var(--border);margin-bottom:8px;font-size:12px}\n.fs-failed-item:last-child{margin-bottom:0}\n.fs-failed-name{color:var(--text-primary);word-break:break-all}\n.fs-failed-error{color:var(--danger-color);font-size:11px;margin-top:4px}\n.fs-mfy-button-container{position:relative;display:inline-block}\n.fs-mfy-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:8px 16px;background:linear-gradient(135deg,#64cc77,#4db366);color:white;border:none;border-radius:var(--radius);font-size:14px;font-weight:500;cursor:pointer;transition:var(--transition);box-shadow:var(--shadow);width:90px;box-sizing:border-box}\n.fs-mfy-button:hover{transform:translateY(-1px);box-shadow:var(--shadow-lg)}\n.fs-mfy-button svg{width:16px;height:16px}\n.fs-mfy-dropdown{position:absolute;top:calc(100% + 4px);left:0;background:white;border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-lg);min-width:160px;z-index:1000;opacity:0;transform:translateY(-10px);visibility:hidden;transition:var(--transition)}\n.fs-mfy-button-container:hover .fs-mfy-dropdown{opacity:1;transform:translateY(0);visibility:visible}\n.fs-mfy-dropdown-item{padding:10px 16px;font-size:13px;color:var(--text-primary);cursor:pointer;transition:var(--transition);display:flex;align-items:center;gap:8px}\n.fs-mfy-dropdown-item:hover{background:var(--surface)}\n.fs-mfy-dropdown-item:first-child{border-radius:var(--radius) var(--radius) 0 0}\n.fs-mfy-dropdown-item:last-child{border-radius:0 0 var(--radius) var(--radius)}\n.fs-mfy-dropdown-divider{height:1px;background:var(--border);margin:4px 0}\n@keyframes fadeIn{from{opacity:0}\nto{opacity:1}\n}@keyframes slideUp{from{opacity:0;transform:translateY(20px)}\nto{opacity:1;transform:translateY(0)}\n}@keyframes slideInRight{from{opacity:0;transform:translateX(100%)}\nto{opacity:1;transform:translateX(0)}\n}@keyframes modalSlideIn{from{opacity:0;transform:translateY(-20px) scale(0.95)}\nto{opacity:1;transform:translateY(0) scale(1)}\n}@keyframes pulse{0%,100%{opacity:1}\n50%{opacity:0.5}\n}.animate-pulse{animation:pulse 2s cubic-bezier(0.4,0,0.6,1) infinite}\n/* ============================================================\n设置页面样式\n============================================================ */\n/* 1. 容器与布局 */\n.fs-settings-container {display: flex;flex-direction: column;gap: 12px;padding: 4px 0;}\n/* 2. 设置行项目 - 卡片感设计 */\n.fs-setting-row {display: flex;align-items: center;justify-content: space-between;padding: 16px;background: var(--surface);border-radius: var(--radius);transition: var(--transition);border: 1px solid transparent;}\n.fs-setting-row:hover {background: #ffffff;border-color: var(--border);box-shadow: var(--shadow-sm);transform: translateY(-1px);}\n.fs-setting-row.readonly {opacity: 0.6;cursor: not-allowed;}\n/* 3. 文本信息区 */\n.fs-setting-info {display: flex;flex-direction: column;gap: 4px;flex: 1;padding-right: 24px;}\n.fs-setting-label-text {font-size: 14.5px;font-weight: 600;color: var(--text-primary);letter-spacing: 0.3px;}\n.fs-setting-describe {font-size: 12.5px;color: var(--text-secondary);line-height: 1.5;}\n/* 4. 交互控件区 */\n.fs-setting-action {display: flex;align-items: center;justify-content: flex-end;min-width: 100px;}\n/* 5. Switch 开关 */\n.fs-settings-switch {position: relative;display: inline-block;width: 44px;height: 24px;}\n.fs-settings-switch input {opacity: 0;width: 0;height: 0;}\n.switch-slider {position: absolute;cursor: pointer;top: 0; left: 0; right: 0; bottom: 0;background-color: var(--border);transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);border-radius: 24px;}\n.switch-slider:before {position: absolute;content: \"\";height: 18px;width: 18px;left: 3px;bottom: 3px;background-color: white;transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);border-radius: 50%;box-shadow: 0 2px 4px rgba(0,0,0,0.1);}\n.fs-settings-switch input:checked + .switch-slider {background-color: var(--primary-color);}\n.fs-settings-switch input:focus + .switch-slider {box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);}\n.fs-settings-switch input:checked + .switch-slider:before {transform: translateX(20px);}\n/* 6. 分段选择器 (Segmented Radio) */\n.fs-settings-radio-group {display: flex;background: #eef2f6;padding: 3px;border-radius: 10px;gap: 2px;}\n.fs-reset-tab {cursor: pointer;position: relative;}\n.fs-reset-tab input {position: absolute;opacity: 0;}\n.fs-reset-tab span {display: block;padding: 6px 14px;font-size: 12px;font-weight: 500;border-radius: 7px;color: var(--text-secondary);transition: all 0.2s;}\n.fs-reset-tab input:checked + span {background: white;color: var(--primary-color);box-shadow: var(--shadow-sm);}\n/* 7. 输入框与下拉框 */\n.fs-settings-input, .fs-settings-select {width: 100%;max-width: 180px;padding: 8px 12px;border: 1.5px solid var(--border);border-radius: var(--radius-sm);background: #ffffff !important;color: var(--text-primary);font-size: 13px;transition: var(--transition);}\n.fs-settings-input:focus, .fs-settings-select:focus {border-color: var(--primary-color);outline: none;box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);}\n/* 8. 底部状态反馈样式 */\n.fs-settings-status {flex: 1;font-size: 13px;display: flex;align-items: center;gap: 6px;}\n.fs-status-warning {color: var(--warning-color);background: rgba(245, 158, 11, 0.1);padding: 4px 10px;border-radius: 20px;}\n.fs-status-success {color: var(--secondary-color);animation: fadeIn 0.3s ease;}\n/* 9. 底部按钮组微调 */\n.fs-modal-footer .button-group {display: flex;gap: 10px;}\n.fs-reset-default-btn {margin-right: auto; /* 将恢复默认按钮推向最左侧 */color: var(--text-secondary) !important;}\n.fs-reset-default-btn:hover {color: var(--danger-color) !important;border-color: var(--danger-color) !important;}/* 模态框布局固定 */\n.fs-settings-modal-box {width: 90%;max-width: 600px;max-height: 85vh; /* 限制最高高度 */display: flex;flex-direction: column; /* 纵向排列 Header, Content, Footer */}\n/* 内容滚动区 */\n.fs-settings-scroll-area {flex: 1; /* 自动占据剩余高度 */overflow-y: auto; /* 关键:设置项过多时在此滚动 */padding: 24px;background: #ffffff;}\n/* 只读行样式 */\n.fs-setting-row.readonly-row {background: #f1f5f9; /* 灰色背景 */opacity: 0.75;cursor: not-allowed;border: 1px dashed var(--border);}\n.fs-setting-row.readonly-row:hover {transform: none;box-shadow: none;}\n.readonly-badge {background: var(--text-tertiary);color: white;font-size: 10px;padding: 2px 6px;border-radius: 4px;margin-left: 8px;vertical-align: middle;}\n/* 禁用控件样式 */\n.fs-settings-switch.readonly, \n.fs-settings-radio-group.readonly,\n.fs-settings-select:disabled,\n.fs-settings-input:read-only {pointer-events: none; /* 禁止点击 */filter: grayscale(1); /* 置灰 */}\n/* Footer 固定在底部 */\n.fs-modal-footer {flex-shrink: 0;background: white;z-index: 10;}");
;// ./src/UiManager.js



const UiManager_log = createLogger('UI');



class UiManager {
    constructor(shareLinkManager, selector, firstTime = false) {
        this.firstTime = firstTime;
        this.shareLinkManager = shareLinkManager;
        this.selector = selector;
        this.isProgressMinimized = false;
        this.minimizeWidgetId = 'fs-progress-minimize-widget';
        this.settings = [{
            key: "scriptVersion",
            label: "脚本版本",
            type: "text",
            value: this.shareLinkManager.scriptVersion,
            readonly: true
        }, {
            key: "COMMON_PATH_LINK_PREFIX_V2",
            label: "公共路径链接前缀",
            type: "text",
            value: this.shareLinkManager.COMMON_PATH_LINK_PREFIX_V2,
            readonly: true
        }, {
            key: "DEFAULT_EXPORT_FILENAME",
            label: "默认导出文件名",
            type: "text",
            value: this.shareLinkManager.defaultExportName,
            description: "当无法从公共路径或文件名生成时使用此默认名称"
        }, {
            key: "seedFilePathId",
            label: "秒传文件保存文件夹ID",
            type: "number",
            value: GlobalConfig.seedFilePathId,
            description: "用于保存二级秒传链接文件的文件夹ID,留空则使用当前文件夹"
        }, {
            key: "usesBase62EtagsInExport",
            label: "Base62编码",
            type: "checkbox",
            value: GlobalConfig.usesBase62EtagsInExport,
            description: "Base62编码的etag可以减少链接长度,但不兼容旧版本脚本"
        }, {
            key: "secondaryLinkUseJson",
            label: "二级秒传链接使用JSON格式",
            type: "checkbox",
            value: GlobalConfig.secondaryLinkUseJson,
            description: "启用后生成二级秒传链接时秒传文件将采用JSON格式"
        }, {
            key: "getFileListPageDelay",
            label: "获取文件列表每页延时 (毫秒)",
            type: "number",
            value: GlobalConfig.getFileListPageDelay
        }, {
            key: "getFileInfoBatchSize",
            label: "批量获取文件信息的数量",
            type: "number",
            value: GlobalConfig.getFileInfoBatchSize
        }, {
            key: "getFileInfoDelay",
            label: "获取文件信息延时 (毫秒)",
            type: "number",
            value: GlobalConfig.getFileInfoDelay
        }, {
            key: "getFolderInfoDelay",
            label: "获取文件夹信息延时 (毫秒)",
            type: "number",
            value: GlobalConfig.getFolderInfoDelay
        }, {
            key: "saveLinkDelay", label: "保存链接延时 (毫秒)", type: "number", value: GlobalConfig.saveLinkDelay
        }, { key: "mkdirDelay", label: "创建文件夹延时 (毫秒)", type: "number", value: GlobalConfig.mkdirDelay }, {
            key: "maxTextFileSize",
            label: "文本文件最大大小 (字节)",
            type: "number",
            value: GlobalConfig.MAX_TEXT_FILE_SIZE
        }, {
            key: "DEBUGMODE",
            label: "调试模式",
            type: "checkbox",
            value: GlobalConfig.DEBUGMODE,
            description: "启用调试模式,页面刷新后生效"
        }];

        this.iconLibrary = {
            transfer: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <polyline points="16 18 22 12 16 6"></polyline>
                        <polyline points="8 6 2 12 8 18"></polyline>
                    </svg>`, generate: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
                        <polyline points="13 2 13 9 20 9"></polyline>
                    </svg>`, save: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
                        <polyline points="17 21 17 13 7 13 7 21"></polyline>
                        <polyline points="7 3 7 8 15 8"></polyline>
                    </svg>`, generateSecondary: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
                    <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
                </svg>`,

            saveSecondary: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                <polyline points="7 10 12 15 17 10"></polyline>
                <line x1="12" y1="15" x2="12" y2="3"></line>
            </svg>`, getFromFile: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
                <polyline points="13 2 13 9 20 9"></polyline>
                <line x1="12" y1="15" x2="12" y2="9"></line>
                <polyline points="9 12 12 9 15 12"></polyline>
            </svg>`, settings: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <circle cx="12" cy="12" r="3"></circle>
                <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
            </svg>`
        };

        // --------------------------任务相关----------------------------
        // taskList = [{id: string, type: 'generate'|'save', params: {}}]
        this.taskList = [];                 // 任务列表
        this.taskIdCounter = 0;             // 任务ID计数器
        this.currentTask = null;            // 当前正在执行的任务
        this.isTaskRunning = false;         // 任务是否在运行

        // 任务处理器
        this.taskHandlers = {
            'generate': {
                addTask: function (params = {}) {
                    const fileSelectInfo = this.selector.getSelection();
                    if (!fileSelectInfo || fileSelectInfo.length === 0) {
                        this.showToast("请先选择文件", 'warning');
                        return null;
                    }
                    return { type: 'generate', params: { fileSelectInfo } };
                }, handler: async function (task) {
                    await this.launchGenerateModal(task.params.fileSelectInfo);
                }, description: '生成秒传链接'
            }, 'generateSecondary': {
                addTask: function (params = {}) {
                    const fileSelectInfo = this.selector.getSelection();
                    if (!fileSelectInfo || fileSelectInfo.length === 0) {
                        this.showToast("请先选择文件", 'warning');
                        return null;
                    }
                    return { type: 'generateSecondary', params: { fileSelectInfo } };
                }, handler: async function (task) {
                    await this.launchSecondaryGenerateModal(task.params.fileSelectInfo);
                }, description: '生成二级秒传链接'
            }, 'save': {
                addTask: function (params = {}) {
                    return { type: 'save', params: { content: params.content } };
                }, handler: async function (task) {
                    await this.launchSaveLink(task.params.content);
                }, description: '保存秒传链接'
            }, 'retry': {
                addTask: function (params = {}) {
                    return { type: 'retry', params: { fileList: params.fileList } };
                }, handler: async function (task) {
                    await this.launchSaveLink(task.params.fileList, true);
                }, description: '重试保存失败的文件'
            }, 'saveOnlyLink': {
                addTask: function (params = {}) {
                    return {
                        type: 'saveOnlyLink', params: {
                            content: params.content, fileName: params.fileName || '123FastLink.123share'
                        }
                    };
                }, handler: async function (task) {
                    await this.launchSaveLinkOnlyText(task.params.content, task.params.fileName);
                }, description: '保存为文本文件'
            }, 'saveSecondary': {
                addTask: function (params = {}) {
                    return { type: 'saveSecondary', params: { content: params.content } };
                }, handler: async function (task) {
                    await this.launchSaveSecondaryLink(task.params.content);
                }, description: '保存二级秒传链接'
            }, 'convert': {
                addTask: function (params = {}) {
                    return { type: 'convert', params: { content: params.content } };
                }, handler: async function (task) {
                    await this.launchConvert(task.params.content);
                }, description: '转换链接格式'
            }, 'saveFromFile': {
                addTask: function (params = {}) {
                    const fileSelectInfo = this.selector.getSelection();
                    if (!fileSelectInfo || fileSelectInfo.length === 0) {
                        this.showToast("请先选择文件", 'warning');
                        return null;
                    }
                    return { type: 'saveFromFile', params: { fileSelectInfo } };
                }, handler: async function (task) {
                    await this.launchSaveFromFile(task.params.fileSelectInfo);
                }, description: '从秒传文件获取并保存'
            }
        };

        this.resetSettings();
    }

    resetSettings() {
        this.maxTextFileSize = GlobalConfig.MAX_TEXT_FILE_SIZE;
        this.seedFilePathId = GlobalConfig.seedFilePathId;
    }

    /**
     * 初始化UI管理器,插入样式表,设置按钮事件
     */
    init() {
        // 按钮插入 ==========================================
        // todo: 二级链接转换
        // 定义功能按钮
        const features = [{
            iconKey: 'generate', text: '生成秒传链接', handler: () => this.addAndRunTask('generate')
        }, {
            iconKey: 'save', text: '保存秒传链接', handler: () => this.showInputModal()
        }, {
            iconKey: 'generateSecondary',
            text: '生成二级链接',
            handler: () => this.addAndRunTask('generateSecondary')
        }, {
            iconKey: 'saveSecondary',
            text: '保存二级链接',
            handler: () => this.showInputModal("saveSecondary", false, '保存二级链接')
        }, {
            iconKey: 'transfer', text: '转换链接格式', handler: () => this.showInputModal("convert", false, '确定')
        }, {
            iconKey: 'getFromFile', text: '从秒传文件获取', handler: () => this.addAndRunTask('saveFromFile')
        }, {
            iconKey: 'settings', text: '设置', handler: () => this.showSettingsModal()
        }];

        // 页面加载完成后插入样式表和添加按钮
        window.addEventListener('load', () => {
            this.insertStyle();
            this.addButton(features);
        });

        // 监听URL变化,重新添加按钮,防止切换页面后按钮消失 =======

        const triggerUrlChange = () => {
            setTimeout(() => this.addButton(features), 10);
        };

        const originalPushState = history.pushState;
        const originalReplaceState = history.replaceState;

        history.pushState = function () {
            originalPushState.apply(this, arguments);
            triggerUrlChange();
        };

        history.replaceState = function () {
            originalReplaceState.apply(this, arguments);
            triggerUrlChange();
        };

        window.addEventListener('popstate', triggerUrlChange);

        // 首次运行提示
        if (this.firstTime) {
            setTimeout(() => {
                this.showFirstTimeGuide();
            }, 1000);
        }
    }

    saveSettings() {
        // 保存设置
        const newSettings = {};
        this.settings.forEach(setting => {
            if (!setting.readonly) {
                newSettings[setting.key] = setting.value;
            }
        });

        // 全局保存函数
        saveSettings(newSettings);
        // 应用到ShareLinkManager
        this.shareLinkManager.init();

        this.resetSettings();
    }

    /**
     * 插入样式表
     */
    insertStyle() {
        if (!document.getElementById("fs-modal-style")) {
            let style = document.createElement("style");
            style.id = "fs-modal-style";
            style.innerHTML = styles;
            document.head.appendChild(style);
        }
    }

    /**
     * 显示提示消息
     */
    showToast(message, type = 'info', duration = 3000) {
        const icons = {
            success: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>',
            error: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
            warning: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
            info: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>'
        };

        const toast = document.createElement('div');
        toast.className = `toast ${type}`;
        toast.innerHTML = `
        <div class="toast-icon">${icons[type]}</div>
        <div>${message}</div>
    `;

        document.body.appendChild(toast);

        setTimeout(() => {
            toast.style.opacity = '0';
            toast.style.transform = 'translateX(100%)';
            setTimeout(() => {
                if (toast.parentNode) {
                    toast.parentNode.removeChild(toast);
                }
            }, 300);
        }, duration);
    }

    // ------------------------------ 设置页 ----------------------------------
    /**
     * 显示系统设置模态框
     */
    showSettingsModal() {
        // this.insertStyle();

        // 1. 防止重复打开
        const existingModal = document.getElementById('fs-settings-modal');
        if (existingModal) existingModal.remove();

        // 2. 数据副本隔离:深拷贝原始设置
        const editingSettings = JSON.parse(JSON.stringify(this.settings));
        let settingsChanged = false;

        // 3. 生成设置项 HTML
        let settingsHtml = '';
        editingSettings.forEach((setting) => {
            const id = `fs-setting-${setting.key}`;
            let controlHtml = '';
            // 判定是否只读
            const isReadonly = setting.readonly === true;

            switch (setting.type) {
                case 'checkbox':
                    controlHtml = `
                <label class="fs-settings-switch ${isReadonly ? 'readonly' : ''}">
                    <input type="checkbox" id="${id}" ${setting.value ? 'checked' : ''} 
                            class="fs-settings-control-input" data-key="${setting.key}" ${isReadonly ? 'disabled' : ''}>
                    <span class="switch-slider"></span>
                </label>`;
                    break;
                case 'select':
                    controlHtml = `
                <select id="${id}" class="fs-settings-select" data-key="${setting.key}" ${isReadonly ? 'disabled' : ''}>
                    ${setting.options.map(opt => `<option value="${opt.value}" ${opt.value === setting.value ? 'selected' : ''}>${opt.label}</option>`).join('')}
                </select>`;
                    break;
                case 'radio':
                    controlHtml = `
                <div class="fs-settings-radio-group ${isReadonly ? 'readonly' : ''}" data-key="${setting.key}">
                    ${setting.options.map(opt => `
                        <label class="fs-reset-tab">
                            <input type="radio" name="${setting.key}" value="${opt.value}" 
                                ${opt.value === setting.value ? 'checked' : ''} 
                                class="fs-settings-control-input" ${isReadonly ? 'disabled' : ''}>
                            <span>${opt.label}</span>
                        </label>
                    `).join('')}
                </div>`;
                    break;
                default:
                    controlHtml = `<input type="${setting.type || 'text'}" id="${id}" value="${setting.value}" 
                                class="fs-settings-input" data-key="${setting.key}" ${isReadonly ? 'readonly' : ''}>`;
            }

            settingsHtml += `
                <div class="fs-setting-row ${isReadonly ? 'readonly-row' : ''}">
                    <div class="fs-setting-info">
                        <div class="fs-setting-label-text">${setting.label}</div>
                        <div class="fs-setting-describe">${setting.describe || setting.description || ''}</div>
                    </div>
                    <div class="fs-setting-action">${controlHtml}</div>
                </div>`;
        });

        // 4. 构建模态框结构 (关键:固定头部底部,中间滚动)
        const modalOverlay = document.createElement('div');
        modalOverlay.className = 'fs-modal-overlay';
        modalOverlay.id = 'fs-settings-modal';
        modalOverlay.innerHTML = `
        <div class="modal fs-settings-modal-box">
            <div class="fs-modal-header">
                <div class="fs-modal-title">
                    <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
                    系统设置
                </div>
                <button class="fs-modal-close" id="close-modal-btn">
                    <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
                </button>
            </div>
            <div class="fs-modal-content fs-settings-scroll-area">
                <div class="fs-settings-container">${settingsHtml}</div>
            </div>
            <div class="fs-modal-footer">
                <div id="fs-settings-status-bar" class="fs-settings-status"></div>
                <div class="button-group">
                    <button class="btn fs-btn-outline fs-reset-default-btn" id="fs-reset-btn">恢复默认</button>
                    <button class="btn fs-btn-outline" id="cancel-btn">取消</button>
                    <button class="btn fs-btn-primary" id="save-btn">保存设置</button>
                </div>
            </div>
        </div>`;

        document.body.appendChild(modalOverlay);

        // --- 内部逻辑函数 ---

        const setStatus = (msg, type = 'info') => {
            const statusEl = document.getElementById('fs-settings-status-bar');
            if (statusEl) statusEl.innerHTML = `<span class="fs-status-${type}">${msg}</span>`;
        };

        const closeSettings = (needConfirm = true) => {
            if (settingsChanged && needConfirm) {
                this.showAlertModal('warning', '未保存', '确定放弃当前修改并离开吗?', {
                    confirmText: '放弃',
                    showCancel: true,
                    cancelText: '返回',
                    onConfirm: () => modalOverlay.remove()
                });
            } else {
                modalOverlay.remove();
            }
        };

        const saveSettings = () => {
            if (!settingsChanged) return closeSettings(false);
            try {
                editingSettings.forEach(edited => {
                    const original = this.settings.find(s => s.key === edited.key);
                    if (original && !original.readonly) {
                        // 检查seedFilePath
                        if (edited.key === 'seedFilePathId') {
                            if (edited.value) {
                                const pathLenth = edited.value.toString().length;
                                if (pathLenth === 1 || pathLenth === 8) {
                                    this.seedFilePathId = edited.value;
                                    original.value = edited.value; // 更新内存
                                } else {
                                    this.showAlertModal('error', '种子文件路径错误', "跳过设置路径ID");
                                }
                            }
                        } else {
                            original.value = edited.value; // 更新内存
                        }
                        if (typeof GlobalConfig !== 'undefined' && GlobalConfig.hasOwnProperty(edited.key)) {
                            GlobalConfig[edited.key] = edited.value; // 更新业务配置
                        }
                        this.saveSettings(); // 持久化
                    }
                });
                this.showToast('设置保存成功', 'success', 2000);
                closeSettings(false);
            } catch (e) {
                this.showToast('持久化失败: ' + e.message, 'error');
            }
        };

        // --- 事件绑定 ---

        modalOverlay.addEventListener('change', (e) => {
            const target = e.target;
            const key = target.dataset.key || target.name;
            if (!key) return;

            const setting = editingSettings.find(s => s.key === key);
            // 【核心拦截】如果副本标志位或原始项是 readonly,直接不响应
            if (!setting || setting.readonly) return;

            if (target.type === 'checkbox') setting.value = target.checked; else if (target.type === 'radio') setting.value = target.value; else if (target.type === 'number') setting.value = parseFloat(target.value); else setting.value = target.value;

            settingsChanged = true;
            setStatus('设置已修改,请保存', 'warning');
        });

        modalOverlay.querySelector('#save-btn').onclick = saveSettings;
        modalOverlay.querySelector('#cancel-btn').onclick = () => closeSettings(true);
        modalOverlay.querySelector('#close-modal-btn').onclick = () => closeSettings(true);
        modalOverlay.querySelector('#fs-reset-btn').onclick = () => {
            this.showAlertModal('error', '重置所有设置?', '该操作将清除 GM 存储并刷新页面恢复默认配置!', {
                confirmText: '立即重置', showCancel: true, onConfirm: () => {
                    deleteSettings();
                    location.reload();
                }
            });
        };

        // 遮罩点击及键盘支持
        modalOverlay.onclick = (e) => {
            if (e.target === modalOverlay) closeSettings(true);
        };
        const handleKey = (e) => {
            if (e.key === 'Escape') closeSettings(true);
            if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) saveSettings();
        };
        document.addEventListener('keydown', handleKey);
        const originalRemove = modalOverlay.remove;
        modalOverlay.remove = function () {
            document.removeEventListener('keydown', handleKey);
            originalRemove.call(this);
        };
    }

    showFirstTimeGuide() {
        this.showAlertModal('info', '欢迎使用123FastLink', `如果您是第一次使用本脚本,建议先阅读使用说明文档,了解基本功能和操作方法。
            \n
            ✅️ 如果要使用二级秒传链接,
            建议先在设置中设置秒传文件路径`);
    }

    /**
     * 显示复制弹窗
     */
    showCopyModal(defaultText = "", allFilePath = [], title = "秒传链接") {
        const fileListHtml = Array.isArray(this.shareLinkManager.fileInfoList) && allFilePath.length > 0 ? `
            <div class="fs-file-list-container">
                <div class="fs-file-list-header">
                    <div class="fs-file-count">文件列表(共${allFilePath.length}个)</div>
                </div>
                <div class="fs-file-list">
                    ${allFilePath.map(f => `
                        <div class="fs-file-item">${f}</div>
                    `).join('')}
                </div>
            </div>
        ` : '';

        const modalOverlay = document.createElement('div');
        modalOverlay.className = 'fs-modal-overlay';
        modalOverlay.innerHTML = `
        <div class="modal">
            <div class="fs-modal-header">
                <div class="fs-modal-title">
                    <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <polyline points="16 18 22 12 16 6"></polyline>
                        <polyline points="8 6 2 12 8 18"></polyline>
                    </svg>
                    ${title}
                </div>
                <button class="fs-modal-close" onclick="this.closest('.fs-modal-overlay').remove()">
                    <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <line x1="18" y1="6" x2="6" y2="18"></line>
                        <line x1="6" y1="6" x2="18" y2="18"></line>
                    </svg>
                </button>
            </div>
            <div class="fs-modal-content">
                ${fileListHtml}
                <textarea id="copyText" placeholder="请输入或粘贴秒传链接...">${defaultText}</textarea>
            </div>
            <div class="fs-modal-footer">
                <div class="dropdown">
                    <button class="btn fs-btn-primary fs-dropdown-toggle">
                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                            <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
                            <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
                        </svg>
                        复制
                    </button>
                    <div class="fs-dropdown-menu">
                        <div class="fs-dropdown-item" data-type="json">
                            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                <path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"></path>
                                <path d="M18 14h-8"></path>
                                <path d="M15 18h-5"></path>
                                <path d="M10 6h8v4h-8V6Z"></path>
                            </svg>
                            复制JSON
                        </div>
                        <div class="fs-dropdown-item" data-type="text">
                            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                <polyline points="16 18 22 12 16 6"></polyline>
                                <polyline points="8 6 2 12 8 18"></polyline>
                            </svg>
                            复制纯文本
                        </div>
                    </div>
                </div>
                <button class="btn fs-btn-secondary" id="exportJsonButton">
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                        <polyline points="7 10 12 15 17 10"></polyline>
                        <line x1="12" y1="15" x2="12" y2="3"></line>
                    </svg>
                    导出JSON
                </button>
            </div>
        </div>
    `;

        // 复制菜单事件
        const dropdownItems = modalOverlay.querySelectorAll('.fs-dropdown-item');
        dropdownItems.forEach(item => {
            item.addEventListener('click', (e) => {
                e.stopPropagation();
                const type = item.dataset.type;
                this.copyContent(type);
            });
        });

        // 主复制按钮事件
        modalOverlay.querySelector('.fs-dropdown-toggle').addEventListener('click', (e) => {
            e.stopPropagation();
            this.copyContent('default');
        });

        // 导出按钮事件
        modalOverlay.querySelector('#exportJsonButton').addEventListener('click', (e) => {
            e.stopPropagation();
            this.exportJson();
        });

        // 点击遮罩关闭
        // modalOverlay.addEventListener('click', (e) => {
        //     if (e.target === modalOverlay) modalOverlay.remove();
        // });

        document.body.appendChild(modalOverlay);

        // 自动聚焦文本域
        setTimeout(() => {
            const textarea = modalOverlay.querySelector('#copyText');
            if (textarea && !defaultText) textarea.focus();
        }, 100);
    }

    /**
     * 复制内容到剪贴板
     * @param {*} type - 复制类型(文本或JSON)
     * @returns
     */
    copyContent(type) {
        const inputField = document.querySelector('#copyText');
        if (!inputField) return;

        let contentToCopy = inputField.value;

        if (type !== 'default') {
            let contentType = this.shareLinkManager.linkChecker(contentToCopy);
            if (!contentType[0]) {
                this.showToast('无效的秒传链接,无法复制', 'error');
                return;
            }

            if (type === 'json') {
                if (contentType[2] === 'text') {
                    contentToCopy = this.shareLinkManager.textShareLinkToJson(contentToCopy)[2];
                    // contentToCopy = JSON.stringify(contentToCopyInfo, null, 2);
                }
            } else if (type === 'text') {
                if (contentType[2] === 'json') {
                    contentToCopy = this.shareLinkManager.jsonToTextShareLink(contentToCopy)[2];
                }
            }
        }

        navigator.clipboard.writeText(contentToCopy).then(() => {
            this.showToast(`已成功复制到剪贴板 📋`, 'success');
        }).catch(err => {
            this.showToast(`复制失败: ${err.message || '请手动复制内容'}`, 'error');
        });
    }

    /**
     * 导出JSON
     * @returns
     */
    exportJson() {
        const inputField = document.querySelector('#copyText');
        if (!inputField) return;

        const shareLink = inputField.value;
        if (!shareLink.trim()) {
            this.showToast('没有内容可导出', 'warning');
            return;
        }


        const jsonContent = this.shareLinkManager.textShareLinkToJson(shareLink)[2];
        // const jsonContent = JSON.stringify(jsonData, null, 2);
        this.shareLinkManager.getExportFilename(shareLink).then(filename => {
            this.downloadJsonFile(jsonContent, filename + '.json');
            this.showToast('JSON文件导出成功 📁', 'success');
        }).catch(err => {
            this.showToast(`导出失败: ${err.message || '请重试'}`, 'error');
        });

    }

    // 下载JSON文件
    downloadJsonFile(content, filename) {
        const blob = new Blob([content], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

    /**
     * 显示或更新进度模态框
     * @param title - 标题
     * @param percent - 进度百分比(0-100)
     * @param desc - 进度描述
     * @param taskCount - 任务队列长度
     */
    updateProgressModal(title = "正在处理...", percent = 0, desc = "", taskCount = 1) {
        percent = Math.ceil(percent);

        if (this.isProgressMinimized) {
            this.updateMinimizedWidget(title, percent, desc, taskCount);
            return;
        }

        let modal = document.getElementById('fs-progress-modal');
        if (!modal) {
            modal = document.createElement('div');
            modal.id = 'fs-progress-modal';
            modal.className = 'fs-modal-overlay fs-progress-modal';
            modal.innerHTML = `
            <div class="modal" style="max-width: 400px;">
                <div class="fs-modal-header">
                    <div class="fs-modal-title">
                        <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="animate-pulse">
                            <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"></path>
                        </svg>
                        ${title}${taskCount > 1 ? ` - 队列 ${taskCount}` : ''}
                    </div>
                    <button class="fs-progress-minimize-btn" title="最小化">
                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                            <polyline points="4 14 10 14 10 20"></polyline>
                            <polyline points="20 10 14 10 14 4"></polyline>
                            <line x1="14" y1="10" x2="21" y2="3"></line>
                            <line x1="3" y1="21" x2="10" y2="14"></line>
                        </svg>
                    </button>
                </div>
                <div class="fs-progress-content">
                    <div class="fs-progress-bar-container">
                        <div class="fs-progress-bar" id="fs-progress-bar" style="width: ${percent}%"></div>
                    </div>
                    <div class="fs-progress-info">
                        <div class="fs-progress-percent">${percent}%</div>
                    </div>
                    ${desc ? `<div class="fs-progress-desc">${desc}</div>` : ''}
                </div>
            </div>
        `;

            // 最小化按钮事件
            const minimizeBtn = modal.querySelector('.fs-progress-minimize-btn');
            minimizeBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                this.isProgressMinimized = true;
                this.removeProgressModalAndKeepState();
                this.updateMinimizedWidget(title, percent, desc, taskCount);
            });

            document.body.appendChild(modal);
        } else {
            const titleElement = modal.querySelector('.fs-modal-title');
            const barElement = modal.querySelector('#fs-progress-bar');
            const percentElement = modal.querySelector('.fs-progress-percent');
            const descElement = modal.querySelector('.fs-progress-desc');

            if (titleElement) {
                titleElement.innerHTML = `
                <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="animate-pulse">
                    <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"></path>
                </svg>
                ${title}${taskCount > 1 ? ` - 队列 ${taskCount}` : ''}
            `;
            }

            if (barElement) barElement.style.width = percent + '%';
            if (percentElement) percentElement.textContent = percent + '%';

            if (desc) {
                if (!descElement) {
                    const progressContent = modal.querySelector('.fs-progress-content');
                    const descDiv = document.createElement('div');
                    descDiv.className = 'fs-progress-desc';
                    descDiv.textContent = desc;
                    progressContent.appendChild(descDiv);
                } else {
                    descElement.textContent = desc;
                }
            } else if (descElement) {
                descElement.remove();
            }
        }

        this.manageTaskList(modal);
    }

    /**
     * 任务列表管理 - 统一处理任务列表的创建、更新和事件绑定
     */
    manageTaskList(modal) {
        const existingContainer = modal.querySelector('.fs-task-list-container');
        const currentTaskCount = this.taskList.length;

        if (currentTaskCount === 0) {
            existingContainer?.remove();
            return;
        }

        const generateHtml = () => `
        <div class="fs-task-list-container">
            <button class="fs-task-toggle" id="fs-task-list-toggle">
                <span>任务队列 (${currentTaskCount})</span>
                <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <polyline points="6 9 12 15 18 9"></polyline>
                </svg>
            </button>
            <div class="fs-task-list" id="fs-task-list">
                ${this.taskList.map(task => {
            const isCurrentTask = this.currentTask && this.currentTask.id === task.id;
            const typeIcon = task.type === 'generate' ? 'generate' : task.type === 'save' ? 'save' : 'retry';
            return `
                        <div class="fs-task-item ${isCurrentTask ? 'current' : ''}" data-task-id="${task.id}">
                            <div class="fs-task-info">
                                <div class="fs-task-icon ${typeIcon}"></div>
                                <div>
                                    <div class="fs-task-name">${this.taskHandlers[task.type].description}</div>
                                    ${isCurrentTask ? '<div class="fs-task-status">执行中...</div>' : ''}
                                </div>
                            </div>
                            <button class="fs-task-remove" data-task-id="${task.id}" 
                                ${/*isCurrentTask ? 'disabled' : ''*/ ""}>
                                <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                    <line x1="18" y1="6" x2="6" y2="18"></line>
                                    <line x1="6" y1="6" x2="18" y2="18"></line>
                                </svg>
                            </button>
                        </div>
                    `;
        }).join('')}
                    </div>
                </div>
            `;

        const bindEvents = (container) => {
            const toggle = container.querySelector('#fs-task-list-toggle');
            const taskList = container.querySelector('#fs-task-list');

            toggle?.addEventListener('click', () => {
                const isShown = taskList.classList.toggle('show');
                toggle.classList.toggle('active', isShown);
                const svg = toggle.querySelector('svg');
                if (svg) {
                    svg.style.transform = isShown ? 'rotate(180deg)' : 'rotate(0deg)';
                }
            });

            container.querySelectorAll('.fs-task-remove').forEach(btn => {
                btn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    const taskId = btn.dataset.taskId;
                    if (this.currentTask && this.currentTask.id.toString() === taskId) {
                        this.showToast('正在中断任务', 'warning');
                        this.cancelCurrentTask();
                        return;
                    }
                    this.taskList = this.taskList.filter(task => task.id.toString() !== taskId);
                    this.manageTaskList(modal);
                    this.showToast('任务已取消', 'info');
                });
            });
        };

        if (!existingContainer) {
            const progressContent = modal.querySelector('.fs-progress-content');
            progressContent.insertAdjacentHTML('beforeend', generateHtml());
            bindEvents(modal.querySelector('.fs-task-list-container'));
        } else {
            const existingTaskItems = existingContainer.querySelectorAll('.fs-task-item');
            const hasCurrentTaskChanged = existingContainer.querySelector('.fs-task-item.current') ? !this.currentTask : !!this.currentTask;

            if (existingTaskItems.length !== currentTaskCount || hasCurrentTaskChanged) {
                const wasExpanded = existingContainer.querySelector('.fs-task-list').classList.contains('show');
                existingContainer.remove();

                const progressContent = modal.querySelector('.fs-progress-content');
                progressContent.insertAdjacentHTML('beforeend', generateHtml());
                const newContainer = modal.querySelector('.fs-task-list-container');
                bindEvents(newContainer);

                if (wasExpanded) {
                    const taskList = newContainer.querySelector('.fs-task-list');
                    const toggle = newContainer.querySelector('#fs-task-list-toggle');
                    taskList.classList.add('show');
                    toggle.classList.add('active');
                    const svg = toggle.querySelector('svg');
                    if (svg) svg.style.transform = 'rotate(180deg)';
                }
            } else {
                const toggleSpan = existingContainer.querySelector('#fs-task-list-toggle span:first-child');
                if (toggleSpan) toggleSpan.textContent = `任务队列 (${currentTaskCount})`;
            }
        }
    }

    // 隐藏进度条并删除浮动卡片
    hideProgressModal() {
        const modal = document.getElementById('fs-progress-modal');
        if (modal) modal.remove();
        this.removeMinimizedWidget();
        this.isProgressMinimized = false;
    }

    // 移除模态但保留 isProgressMinimized 标志(供最小化按钮调用)
    removeProgressModalAndKeepState() {
        const modal = document.getElementById('fs-progress-modal');
        if (modal) modal.remove();
    }

    // 创建或更新右下角最小化浮动进度条卡片
    updateMinimizedWidget(title = '正在处理...', percent = 0, desc = '', taskCount = 1) {
        let widget = document.getElementById(this.minimizeWidgetId);
        const badgeHtml = this.taskList.length >= 1 ? `<div class="fs-widget-badge">${this.taskList.length}</div>` : '';

        const html = `
        <div class="fs-widget-header">
            <div class="fs-widget-title">${title}${taskCount > 1 ? ` - 队列 ${taskCount}` : ''}</div>
            ${badgeHtml}
        </div>
        <div class="fs-widget-progress">
            <div class="fs-widget-bar">
                <div class="fs-widget-fill" style="width: ${percent}%"></div>
            </div>
            <div class="fs-widget-percent">${percent}%</div>
        </div>
        `;

        if (!widget) {
            widget = document.createElement('div');
            widget.id = this.minimizeWidgetId;
            widget.className = 'minimized-widget';
            widget.innerHTML = html;

            widget.addEventListener('mouseup', (e) => {
                e.stopPropagation();
                this.isProgressMinimized = false;
                this.removeMinimizedWidget();
                this.updateProgressModal(title, percent, desc, taskCount);
            });

            document.body.appendChild(widget);
        } else {
            widget.innerHTML = html;
        }
    }

    // 移除右下角浮动卡片
    removeMinimizedWidget() {
        const w = document.getElementById(this.minimizeWidgetId);
        if (w) w.remove();
    }

    /**
     * 任务函数 - 启动生成链接,UI层面的生成入口
     * 包括UI进度条显示和轮询
     * @param {*} fileSelectInfo - 选中文件信息,来自selector
     */
    async launchGenerateModal(fileSelectInfo, secondary = false) {
        const poll = this.startRollPolling("生成秒传链接");
        let shareLinkResult;
        if (secondary) {
            shareLinkResult = await this.shareLinkManager.generateSecondaryShareLink(fileSelectInfo, null, this.seedFilePathId);
        } else {
            shareLinkResult = await this.shareLinkManager.generateShareLink(fileSelectInfo);
        }
        if (!shareLinkResult[0]) {
            this.showToast(shareLinkResult[1] || "秒传链接生成失败", 'error');
            this.showAlertModal("error", "秒传链接生成失败", shareLinkResult[1] || "未知错误");
            this.stopRollPolling(poll);
            return null;
        }
        const shareLink = shareLinkResult[2];
        // 清除任务取消标志
        this.shareLinkManager.taskCancel = false;
        if (!shareLink) {
            this.showToast("没有选择文件", 'warning');
            this.stopRollPolling(poll);
            return;
        }
        this.stopRollPolling(poll);
        this.showCopyModal(shareLink, shareLinkResult[3] || [], secondary ? "二级链接" : "秒传链接");
        return shareLink;
    }

    async launchSecondaryGenerateModal(fileSelectInfo) {
        return this.launchGenerateModal(fileSelectInfo, true);
    }

    /**
     * 显示保存结果模态框
     * @param result - {success: [], failed: []}
     * @returns null
     */
    async showSaveResultsModal(result) {
        const totalCount = result.success.length + result.failed.length;
        const successCount = result.success.length;
        const failedCount = result.failed.length;

        // 成功的列表是后加的,先借用失败的样式了
        const successListHtml = successCount > 0 ? `
        <div style="margin-top: 20px;">
            <div style="font-size: 13px; font-weight: 500; color: var(--info-color); margin-bottom: 8px;">
                成功文件列表
            </div>
            <div class="fs-failed-list">
                ${result.success.map(fileInfo => `
                    <div class="fs-failed-item">
                        <div class="fs-failed-name">${fileInfo.fileName}</div>
                    </div>
                `).join('')}
            </div>
        </div>
        ` : '';

        const failedListHtml = failedCount > 0 ? `
        <div style="margin-top: 20px;">
            <div style="font-size: 13px; font-weight: 500; color: var(--danger-color); margin-bottom: 8px;">
                失败文件列表
            </div>
            <div class="fs-failed-list">
                ${result.failed.map(fileInfo => `
                    <div class="fs-failed-item">
                        <div class="fs-failed-name">${fileInfo.fileName}</div>
                        ${fileInfo.error ? `<div class="fs-failed-error">${fileInfo.error}</div>` : ''}
                    </div>
                `).join('')}
            </div>
        </div>
        ` : '';

        const modalOverlay = document.createElement('div');
        modalOverlay.className = 'fs-modal-overlay';
        modalOverlay.innerHTML = `
        <div class="modal" style="max-width: 500px;">
            <div class="fs-modal-header">
                <div class="fs-modal-title">
                    <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
                        <polyline points="22 4 12 14.01 9 11.01"></polyline>
                    </svg>
                    保存结果
                </div>
                <button class="fs-modal-close" onclick="this.closest('.fs-modal-overlay').remove()">
                    <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <line x1="18" y1="6" x2="6" y2="18"></line>
                        <line x1="6" y1="6" x2="18" y2="18"></line>
                    </svg>
                </button>
            </div>
            <div class="fs-modal-content fs-results-content">
                <div class="fs-results-stats">
                    <div class="fs-stat-card success">
                        <div class="fs-stat-value success">${successCount}</div>
                        <div class="fs-stat-label">成功</div>
                    </div>
                    <div class="fs-stat-card failed">
                        <div class="fs-stat-value failed">${failedCount}</div>
                        <div class="fs-stat-label">失败</div>
                    </div>
                </div>
                <div style="text-align: center; font-size: 13px; color: var(--text-secondary); margin: 20px 0;">
                    总计处理 <strong>${totalCount}</strong> 个文件
                </div>
                ${successListHtml}
                ${failedListHtml}
            </div>
            <div class="fs-modal-footer">
                <button class="btn fs-btn-outline" onclick="this.closest('.fs-modal-overlay').remove()">
                    关闭
                </button>
                ${failedCount > 0 ? `
                    <div class="dropdown">
                        <button class="btn fs-btn-secondary fs-dropdown-toggle">
                            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                <path d="M21.21 15.89A10 10 0 1 1 8 2.83"></path>
                                <path d="M22 12A10 10 0 0 0 12 2v10z"></path>
                            </svg>
                            操作
                        </button>
                        <div class="fs-dropdown-menu">
                            <div class="fs-dropdown-item" data-action="retry">
                                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                    <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
                                    <path d="M3 3v5h5"></path>
                                </svg>
                                重试失败
                            </div>
                            <div class="fs-dropdown-item" data-action="export">
                                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                                    <polyline points="7 10 12 15 17 10"></polyline>
                                    <line x1="12" y1="15" x2="12" y2="3"></line>
                                </svg>
                                导出失败链接
                            </div>
                        </div>
                    </div>
                ` : ''}
            </div>
        </div>
        `;

        if (failedCount > 0) {
            const dropdownItems = modalOverlay.querySelectorAll('.fs-dropdown-item');
            dropdownItems.forEach(item => {
                item.addEventListener('click', async () => {
                    const action = item.dataset.action;
                    modalOverlay.remove();

                    if (action === 'retry') {
                        this.addAndRunTask('retry', { fileList: result.failed });
                    } else if (action === 'export') {
                        const shareLinkResult = this.shareLinkManager.buildShareLink(result.failed, result.commonPath || '', false);
                        this.showCopyModal(shareLinkResult[2], shareLinkResult[3] || [], "导出失败链接");
                    }
                });
            });
        }

        modalOverlay.addEventListener('click', (e) => {
            if (e.target === modalOverlay) modalOverlay.remove();
        });

        document.body.appendChild(modalOverlay);
    }

    /**
     * 显示提示窗
     * @param {string} type - 提示类型: 'success' | 'error'
     * @param {string} title - 标题
     * @param {string} message - 消息内容
     * @param {Object} options - 配置选项
     * @param {string} options.confirmText - 确认按钮文字
     * @param {Function} options.onConfirm - 确认回调
     * @param {boolean} options.showCancel - 是否显示取消按钮
     * @param {string} options.cancelText - 取消按钮文字
     * @param {Function} options.onCancel - 取消回调
     * @param {number} options.autoClose - 自动关闭时间(毫秒)
     */
    async showAlertModal(type, title, message, options = {}) {
        const {
            confirmText = '确定',
            onConfirm = null,
            showCancel = false,
            cancelText = '取消',
            onCancel = null,
            autoClose = 0
        } = options;

        // 定义图标和颜色
        const iconConfig = {
            success: {
                icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
                <polyline points="22 4 12 14.01 9 11.01"></polyline>
                </svg>`,
                color: 'var(--secondary-color)',
                bgColor: 'rgba(16, 185, 129, 0.1)',
                borderColor: 'rgba(16, 185, 129, 0.2)'
            }, error: {
                icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <circle cx="12" cy="12" r="10"></circle>
                <line x1="15" y1="9" x2="9" y2="15"></line>
                <line x1="9" y1="9" x2="15" y2="15"></line>
                </svg>`,
                color: 'var(--danger-color)',
                bgColor: 'rgba(239, 68, 68, 0.1)',
                borderColor: 'rgba(239, 68, 68, 0.2)'
            }, warning: {
                icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
                <line x1="12" y1="9" x2="12" y2="13"></line>
                <line x1="12" y1="17" x2="12.01" y2="17"></line>
                </svg>`,
                color: 'var(--warning-color)',
                bgColor: 'rgba(245, 158, 11, 0.1)',
                borderColor: 'rgba(245, 158, 11, 0.2)'
            }, info: {
                icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <circle cx="12" cy="12" r="10"></circle>
                <line x1="12" y1="16" x2="12" y2="12"></line>
                <line x1="12" y1="8" x2="12.01" y2="8"></line>
                </svg>`,
                color: 'var(--info-color)',
                bgColor: 'rgba(59, 130, 246, 0.1)',
                borderColor: 'rgba(59, 130, 246, 0.2)'
            }
        };

        const config = iconConfig[type] || iconConfig.success;

        const modalOverlay = document.createElement('div');
        modalOverlay.className = 'fs-modal-overlay';
        modalOverlay.innerHTML = `
            <div class="modal" style="max-width: 420px;">
                <div class="fs-modal-header" style="border-bottom: none; padding-bottom: 0;">
                    <div class="fs-modal-title" style="justify-content: center; gap: 12px;">
                        <div style="width: 48px; height: 48px; border-radius: 50%; 
                            background: ${config.bgColor}; border: 1px solid ${config.borderColor};
                            display: flex; align-items: center; justify-content: center;
                            color: ${config.color};">
                            ${config.icon}
                        </div>
                    </div>
                    <button class="fs-modal-close" onclick="this.closest('.fs-modal-overlay').remove()">
                        <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                            <line x1="18" y1="6" x2="6" y2="18"></line>
                            <line x1="6" y1="6" x2="18" y2="18"></line>
                        </svg>
                    </button>
                </div>
                <div class="fs-modal-content" style="text-align: center; padding-top: 8px;">
                    <h3 style="margin: 0 0 12px 0; font-size: 18px; font-weight: 600; color: var(--text-primary);">
                        ${title}
                    </h3>
                    <div style="color: var(--text-secondary); font-size: 14px; line-height: 1.5; 
                        margin-bottom: 24px; word-break: break-all;">
                        ${message}
                    </div>
                </div>
                <div class="fs-modal-footer" style="justify-content: ${showCancel ? 'space-between' : 'center'};">
                    ${showCancel ? `
                        <button class="btn fs-btn-outline" id="cancelButton" style="min-width: 100px;">
                            ${cancelText}
                        </button>
                    ` : ''}
                    <button class="btn fs-btn-primary" id="confirmButton" 
                        style="min-width: 100px; background: ${config.color}; border-color: ${config.color};">
                        ${confirmText}
                    </button>
                </div>
            </div>
        `;

        // 确认按钮事件
        const confirmButton = modalOverlay.querySelector('#confirmButton');
        confirmButton.addEventListener('click', () => {
            modalOverlay.remove();
            if (onConfirm && typeof onConfirm === 'function') {
                onConfirm();
            }
        });

        // 取消按钮事件
        if (showCancel) {
            const cancelButton = modalOverlay.querySelector('#cancelButton');
            cancelButton.addEventListener('click', () => {
                modalOverlay.remove();
                if (onCancel && typeof onCancel === 'function') {
                    onCancel();
                }
            });
        }

        // 点击遮罩关闭
        modalOverlay.addEventListener('click', (e) => {
            if (e.target === modalOverlay) {
                modalOverlay.remove();
                if (onCancel && typeof onCancel === 'function') {
                    onCancel();
                }
            }
        });

        // 回车键确认
        const handleKeyPress = (e) => {
            if (e.key === 'Enter') {
                modalOverlay.remove();
                if (onConfirm && typeof onConfirm === 'function') {
                    onConfirm();
                }
            } else if (e.key === 'Escape') {
                modalOverlay.remove();
                if (onCancel && typeof onCancel === 'function') {
                    onCancel();
                }
            }
        };
        document.addEventListener('keydown', handleKeyPress);

        // 自动关闭
        if (autoClose > 0) {
            setTimeout(() => {
                if (modalOverlay.parentNode) {
                    modalOverlay.remove();
                    if (onConfirm && typeof onConfirm === 'function') {
                        onConfirm();
                    }
                }
            }, autoClose);
        }

        // 移除时清理事件监听
        const originalRemove = modalOverlay.remove;
        modalOverlay.remove = function () {
            document.removeEventListener('keydown', handleKeyPress);
            originalRemove.call(this);
        };

        document.body.appendChild(modalOverlay);

        // 自动聚焦确认按钮
        setTimeout(() => {
            confirmButton.focus();
        }, 100);
    }

    /*
        * 启动轮询刷新进度框
    */
    startRollPolling(title) {
        this.updateProgressModal(title, 0, "准备中...");
        this.shareLinkManager.progress = 0;
        return setInterval(() => {
            this.updateProgressModal(title, this.shareLinkManager.progress, this.shareLinkManager.progressDesc, this.taskList.length);
        }, 100);
    }

    /**
     *  停止轮询更新进度
     * @param {} poll
     */
    stopRollPolling(poll) {
        clearInterval(poll);
        this.hideProgressModal();
    }

    /**
     * 任务函数 - 启动从输入的内容解析并保存秒传链接,UI层面的保存入口,retry为是可以重试失败的文件
     * @param {*} content - 输入内容(秒传链接/JSON)
     */
    async launchSaveLink(content, retry = false) {
        const poll = this.startRollPolling("保存秒传链接");
        let saveResult;
        if (!retry) {
            saveResult = await this.shareLinkManager.saveShareLink(content);
        } else {
            saveResult = await this.shareLinkManager.retrySaveFailed(content);
        }
        // 清除任务取消标志
        this.shareLinkManager.taskCancel = false;
        this.stopRollPolling(poll);
        this.showSaveResultsModal(saveResult[2]);
        this.renewWebPageList();
        this.showToast(saveResult[0] ? "保存成功" : "保存失败", saveResult[0] ? 'success' : 'error');
    }

    async launchSaveSecondaryLink(content) {
        const poll = this.startRollPolling("保存秒传链接");
        let saveResult = await this.shareLinkManager.saveSecondaryShareLink(content, this.seedFilePathId);
        this.shareLinkManager.taskCancel = false;
        this.stopRollPolling(poll);
        if (!saveResult[0]) {
            this.showToast("保存失败 " + saveResult[1], 'error');
            this.showAlertModal('error', '保存失败', saveResult[1]);
            return;
        }
        this.showSaveResultsModal(saveResult[2]);
        this.renewWebPageList();
        this.showToast(saveResult[0] ? "保存成功" : "保存失败", saveResult[0] ? 'success' : 'error');

    }

    /**
     * 任务函数 - 启动从仅包含秒传链接文本内容保存秒传链接,UI层面的保存入口
     * @param {string} linkText
     * @param {string} fileName
     */
    async launchSaveLinkOnlyText(linkText, fileName) {
        // 链接格式校验
        const textType = this.shareLinkManager.linkChecker(linkText);
        if (!textType[0]) {
            this.showAlertModal('warning', '格式错误', '秒传链接格式无法识别,链接仍将被保存!');
        }
        const poll = this.startRollPolling("保存秒传链接");
        const saveResult = await this.shareLinkManager.saveShareLinkOnlyText(linkText, fileName);
        this.stopRollPolling(poll);
        this.showAlertModal(saveResult[0] ? 'success' : 'error', saveResult[0] ? '保存成功' : '保存失败', saveResult[1]);
        this.renewWebPageList();
        this.showToast(saveResult ? "保存成功" : "保存失败", saveResult ? 'success' : 'error');
    }


    /**
     * 任务函数 - 启动转换秒传链接格式,UI层面的转换入口
     * @param {string} content - 输入内容(秒传链接/JSON)
     */
    async launchConvert(content) {
        // 可以利用现有的复制模态框显示结果,要先转换成文本链接
        // if (this.shareLinkManager.)
        const textType = this.shareLinkManager.linkChecker(content);
        let shareLink;
        if (!textType[0]) {
            this.showAlertModal('error', '格式错误', '无法识别的秒传链接格式。');
            return;
        }
        if (textType[2] === 'json') {
            shareLink = this.shareLinkManager.jsonToTextShareLink(content)[2];
        } else if (textType[2] === 'text') {
            shareLink = this.shareLinkManager.textShareLinkToJson(content)[2];
            // shareLink = JSON.stringify(shareLinkDict, null, 2);
        }
        this.showCopyModal(shareLink, []);
    }

    async launchSaveFromFile(fileSelectInfo) {
        if (!fileSelectInfo || fileSelectInfo.length === 0) {
            this.showToast("没有选择文件", 'warning');
            return;
        }
        if (fileSelectInfo.length > 1) {
            this.showToast("暂时只能选择单个文件", 'warning');
            return;
        }
        const file = fileSelectInfo[0].FileId;
        // 先展开对话框
        this.updateProgressModal("读取文件中...", 0, "请稍候...");
        const fileContentRes = await this.shareLinkManager.getFileContentAsText(file);
        if (!fileContentRes[0]) {
            this.showToast("读取文件失败", 'error');
            return;
        }
        const content = fileContentRes[2];
        // 校验格式
        const textType = this.shareLinkManager.linkChecker(content);
        if (!textType[0]) {
            this.showAlertModal('error', '格式错误', '无法识别的秒传链接格式。');
            return;
        }
        // 启动保存
        this.launchSaveLink(content);
    }

    /**
     * 模拟点击刷新按钮,刷新页面文件列表
     */
    renewWebPageList() {
        // 刷新页面文件列表
        const renewButton = document.querySelector('.layout-operate-icon.mfy-tooltip svg');
        if (renewButton) {
            const clickEvent = new MouseEvent('click', {
                bubbles: true, cancelable: true
            });
            renewButton.dispatchEvent(clickEvent);
        }
    }

    /**
     * 显示输入模态框
     */
    async showInputModal(saveTask = 'save', canOnlyLink = true, buttonText = '保存') {
        const modalOverlay = document.createElement('div');
        modalOverlay.className = 'fs-modal-overlay';
        modalOverlay.innerHTML = `
        <div class="modal" style="max-width: 500px;">
            <div class="fs-modal-header">
                <div class="fs-modal-title">
                    <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                        <polyline points="14 2 14 8 20 8"></polyline>
                        <line x1="16" y1="13" x2="8" y2="13"></line>
                        <line x1="16" y1="17" x2="8" y2="17"></line>
                        <polyline points="10 9 9 9 8 9"></polyline>
                    </svg>
                    保存秒传链接
                </div>
                <button class="fs-modal-close" onclick="this.closest('.fs-modal-overlay').remove()">
                    <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <line x1="18" y1="6" x2="6" y2="18"></line>
                        <line x1="6" y1="6" x2="18" y2="18"></line>
                    </svg>
                </button>
            </div>
            <div class="fs-modal-content">
                <textarea id="saveText" placeholder="请输入或粘贴秒传链接,或将JSON文件拖拽到此处..."></textarea>
            </div>
            <div class="fs-modal-footer">
                <button class="btn fs-btn-secondary" id="saveButton">
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
                        <polyline points="17 21 17 13 7 13 7 21"></polyline>
                        <polyline points="7 3 7 8 15 8"></polyline>
                    </svg>
                ${buttonText}
                </button>
                ${canOnlyLink ? `
                <button class="btn fs-btn-primary" id="saveButtonOnlyLink">
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
                        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
                        <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
                    </svg>
                    仅保存链接
                </button>
                ` : ''}
                <button class="btn fs-btn-outline" id="selectFileButton">
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
                        <polyline points="13 2 13 9 20 9"></polyline>
                    </svg>
                    选择文件
                </button>
                <input type="file" class="fs-file-input" id="jsonFileInput" accept=".">
            </div>
        </div>
        `;
        const textarea = modalOverlay.querySelector('#saveText');
        const fileInput = modalOverlay.querySelector('#jsonFileInput');
        const selectFileBtn = modalOverlay.querySelector('#selectFileButton');

        this.setupFileDropAndInput(textarea, fileInput);

        selectFileBtn.addEventListener('click', () => {
            fileInput.click();
        });

        // 保存按钮事件绑定
        modalOverlay.querySelector('#saveButton').addEventListener('click', async () => {
            const content = textarea.value.trim();
            if (!content) {
                this.showToast("请输入秒传链接或导入JSON文件", 'warning');
                return;
            }
            modalOverlay.remove();
            this.addAndRunTask(saveTask, { content });
        });

        if (canOnlyLink) {
            // 仅保存链接按钮事件绑定
            modalOverlay.querySelector('#saveButtonOnlyLink').addEventListener('click', async () => {
                const content = textarea.value.trim();
                if (!content) {
                    this.showToast("请输入秒传链接或导入JSON文件", 'warning');
                    return;
                }
                modalOverlay.remove();
                this.addAndRunTask('saveOnlyLink', { content });
            });
        }


        modalOverlay.addEventListener('click', (e) => {
            if (e.target === modalOverlay) modalOverlay.remove();
        });


        document.body.appendChild(modalOverlay);

        setTimeout(() => {
            if (textarea) textarea.focus();
        }, 100);
    }

    // 处理文件拖拽和读取
    setupFileDropAndInput(textarea, fileInput) {
        // 拖拽事件
        textarea.addEventListener('dragover', (e) => {
            e.preventDefault();
            textarea.classList.add('drag-over');
        });

        textarea.addEventListener('dragleave', (e) => {
            e.preventDefault();
            textarea.classList.remove('drag-over');
        });

        textarea.addEventListener('drop', (e) => {
            e.preventDefault();
            textarea.classList.remove('drag-over');

            const files = e.dataTransfer.files;
            if (files.length > 0) {
                this.readJsonFile(files[0], textarea);
            }
        });

        // 文件选择事件
        fileInput.addEventListener('change', (e) => {
            const files = e.target.files;
            if (files.length > 0) {
                this.readJsonFile(files[0], textarea);
            }
        });
    }

    /**
     * 读取文件并将内容填充到文本区域
     * @param {*} file - 要读取的文件
     * @param {*} textarea - 目标文本区域
     * @returns
     */
    readJsonFile(file, textarea) {
        // 不再限制文件类型

        // if (!file.name.toLowerCase().endsWith('.json')) {
        //     this.showToast('请选择JSON文件', 'warning');
        //     return;
        // }

        // 限制文件大小
        if (file.size > this.maxTextFileSize) {
            this.showToast(`文件过大,最大支持 ${this.maxTextFileSize / (1024 * 1024)} MB,请检查是否选错文件`, 'error');
            this.showAlertModal('error', '文件过大', `所选文件大小为 ${(file.size / (1024 * 1024)).toFixed(2)} MB,超过最大支持 ${(this.maxTextFileSize / (1024 * 1024))} MB。请检查是否选错文件。
            如果需要导入更大文件,请修改允许的最大值`);
            return;
        }

        const reader = new FileReader();
        reader.onload = (e) => {
            textarea.value = e.target.result;
            this.showToast('文件导入成功 ✅', 'success');
        };
        reader.readAsText(file);
    }

    /**
     * 解析、添加并触发任务
     * @param taskType - 任务类型(generate/save/retry等)
     * @param params - 任务参数
     */
    addAndRunTask(taskType, params = {}) {
        const taskConfig = this.taskHandlers[taskType];

        if (!taskConfig) {
            UiManager_log.warn(`未知的 taskType: ${taskType}`);
            this.showToast(`未知的任务类型: ${taskType}`, 'error');
            return;
        }

        const taskData = taskConfig.addTask.call(this, params);
        if (!taskData) return;

        const taskId = ++this.taskIdCounter;
        const task = { id: taskId, ...taskData };
        this.taskList.push(task);
        this.runNextTask();
    }

    /**
     * 队列 - 运行下一个任务
     * @returns {null|void}
     */
    runNextTask() {
        if (this.isTaskRunning) return this.showToast("已添加到队列,稍后执行", 'info');
        if (this.taskList.length === 0) return null;
        // 找到第一个未执行的任务
        const task = this.taskList.find(t => !this.currentTask || t.id !== this.currentTask.id);
        if (!task) return null;
        // 标记当前任务
        this.currentTask = task;
        const taskConfig = this.taskHandlers[task.type];
        // 执行任务
        setTimeout(async () => {
            this.isTaskRunning = true;
            if (taskConfig && taskConfig.handler) {
                try {
                    await taskConfig.handler.call(this, task);
                } catch (error) {
                    UiManager_log.error(`任务${task.id}执行失败:`, error);
                    this.showAlertModal('error', '任务执行失败', `任务${task.id}执行过程中出现错误: ${error.message}`);
                    this.showToast(`任务${task.id}执行失败: ${error.message}`, 'error');
                }
            } else {
                this.showToast(`未知的任务类型: ${task.type}`, 'error');
            }
            this.isTaskRunning = false;
            // 任务完成,从列表中移除
            this.taskList = this.taskList.filter(t => t.id !== task.id);
            this.currentTask = null;
            this.runNextTask();
        }, 100);
        this.showToast(`任务${task.id}开始执行...`, 'info');
    }

    /** 任务取消
     * @returns {boolean}
     */
    cancelCurrentTask() {
        this.shareLinkManager.taskCancel = true;
        return true;
    }


    addButton(features, options = {}) {
        const buttonExist = document.querySelector('.fs-mfy-button-container');
        if (buttonExist) return;

        const container = document.querySelector('.home-operator-button-group');
        if (!container) return;

        const btnContainer = document.createElement('div');
        btnContainer.className = 'fs-mfy-button-container';

        const btn = document.createElement('button');
        // ant-btn css-1n9kme6 ant-btn-primary ant-btn-variant-solid ant-dropdown-trigger mfy-button upload-button
        btn.className = 'ant-btn css-1n9kme6 ant-btn-primary ant-btn-variant-solid ant-dropdown-trigger mfy-button upload-button fs-mfy-button upload-button'; // 利用现有样式
        btn.style = "background-color: #5ebf70;";
        btn.innerHTML = `${this.iconLibrary.transfer}<span>${options.buttonText || '秒传'}</span>`;

        const dropdown = document.createElement('div');
        dropdown.className = 'fs-mfy-dropdown';

        // 根据功能列表创建下拉项
        features.forEach(feature => {
            const icon = this.iconLibrary[feature.iconKey] || feature.iconKey || '';
            const itemElement = document.createElement('div');
            itemElement.className = 'fs-mfy-dropdown-item';
            itemElement.innerHTML = `${icon}${feature.text}`;

            itemElement.addEventListener('click', async (e) => {
                e.stopPropagation();
                if (feature.handler && typeof feature.handler === 'function') {
                    await feature.handler();
                }
                dropdown.style.display = 'none';
            });

            dropdown.appendChild(itemElement);
        });

        btnContainer.appendChild(btn);
        btnContainer.appendChild(dropdown);
        container.insertBefore(btnContainer, container.firstChild);

        // 下拉菜单交互逻辑
        btnContainer.addEventListener('mouseenter', () => {
            dropdown.style.display = 'block';
        });

        btnContainer.addEventListener('mouseleave', (e) => {
            setTimeout(() => {
                if (!btnContainer.matches(':hover') && !dropdown.matches(':hover')) {
                    dropdown.style.display = 'none';
                }
            }, 300);
        });
    }

}

;// ./src/index.js






initSettings();
const apiClient = new PanApiClient();
const selector = new TableRowSelector();
const shareLinkManager = new ShareLinkManager(apiClient);
const uiManager = new UiManager(shareLinkManager, selector, isFirstTime());

uiManager.init();

if (GlobalConfig.DEBUGMODE) {
    window._apiClient = apiClient;
    window._shareLinkManager = shareLinkManager;
    window._selector = selector;
    window._uiManager = uiManager;
}

/******/ })()
;