123FastLink

Creat and save 123pan instant links.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         123FastLink
// @namespace    http://tampermonkey.net/
// @version      2025.11.22.1
// @description  Creat and save 123pan instant links.
// @author       Baoqing
// @author       Chaofan
// @author       lipkiat
// @match        *://*.123pan.com/*
// @match        *://*.123pan.cn/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=123pan.com
// @grant        none
// ==/UserScript==


(function () {
    'use strict';
    const GlobalConfig = {
        scriptVersion: "3.0.3",
        usesBase62EtagsInExport: true,
        getFileListPageDelay: 500,
        getFileInfoBatchSize: 100,
        getFileInfoDelay: 200,
        getFolderInfoDelay: 300,
        saveLinkDelay: 100,
        mkdirDelay: 100,
        scriptName: "123FASTLINKV3",
        COMMON_PATH_LINK_PREFIX_V2: "123FLCPV2$"
    };

    // 1. 123云盘API通信类
    class PanApiClient {
        constructor() {
            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.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) {
                console.error('[123FASTLINK] [PanApiClient]', '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);
            //console.log("[123FASTLINK] [PanApiClient]", "获取文件列表:", data.data.InfoList);
            console.log("[123FASTLINK] [PanApiClient]", "获取文件列表 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
            console.log("[123FASTLINK] [PanApiClient]", "开始获取文件列表,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 uploadRequest(fileInfo) {
            try {
                const response = await this.sendRequest('POST', '/b/api/file/upload_request', {}, JSON.stringify({
                    ...fileInfo, RequestSource: null
                }));
                const reuse = response['data']['Reuse'];
                console.log('[123FASTLINK] [PanApiClient]', 'reuse:', reuse);
                if (response['code'] !== 0) {
                    return [false, response['message']];
                }
                if (!reuse) {
                    console.error('[123FASTLINK] [PanApiClient]', '保存文件失败:', fileInfo.fileName, 'response:', response);
                    return [false, "未能实现秒传"];
                } else {
                    return [true, null];
                }
            } catch (error) {
                console.error('[123FASTLINK] [PanApiClient]', '上传请求失败:', error);
                return [false, '请求失败'];
            }
        }

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

        // 获取文件
        async getFile(fileInfo, parentFileId) {
            if (!parentFileId) {
                parentFileId = await this.getParentFileId();
            }
            return await this.uploadRequest({
                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) {
                console.error('[123FASTLINK] [PanApiClient]', '创建文件夹失败:', error);
                return {
                    'folderFileId': null, 'folderName': folderName, 'success': false
                };
            }
            console.log('[123FASTLINK] [PanApiClient]', '创建文件夹 ID:', folderFileId);
            return {
                'folderFileId': folderFileId, 'folderName': folderName, 'success': true
            };
        }
    }

    // 2. 选中文件管理类
    class TableRowSelector {
        constructor() {
            this.selectedRowKeys = [];
            this.unselectedRowKeys = [];
            this.isSelectAll = false;
            this._inited = false;
        }

        init() {
            if (this._inited) return;
            this._inited = true;

            // 保存原始 createElement 方法
            const originalCreateElement = document.createElement;
            const self = this;
            document.createElement = function (tagName, options) {
                const element = originalCreateElement.call(document, tagName, options);
                if (!(tagName.toLowerCase() === 'input')) {
                    return element;
                }
                const observer = new MutationObserver(() => {
                    if (element.classList.contains('ant-checkbox-input')) {
                        if (
                            // 检查是否为全选框并绑定事件
                            element.getAttribute('aria-label') === 'Select all'
                        ) {
                            // 新建全选框,新页面,清除已选择
                            self.unselectedRowKeys = [];
                            self.selectedRowKeys = [];
                            self.isSelectAll = false;

                            self._bindSelectAllEvent(element);
                            console.log('[123FASTLINK] [Selector] 已为全选框绑定事件');
                        } else {
                            {
                                const input = element
                                input.addEventListener('click', function () {
                                    const rowKey = input.closest('.ant-table-row').getAttribute('data-row-key');
                                    if (self.isSelectAll) {
                                        if (!this.checked) {
                                            if (!self.unselectedRowKeys.includes(rowKey)) {
                                                self.unselectedRowKeys.push(rowKey);
                                            }
                                        } else {
                                            const idx = self.unselectedRowKeys.indexOf(rowKey);
                                            if (idx > -1) {
                                                self.unselectedRowKeys.splice(idx, 1);
                                            }
                                        }
                                    } else {
                                        if (this.checked) {
                                            if (!self.selectedRowKeys.includes(rowKey)) {
                                                self.selectedRowKeys.push(rowKey);
                                            }
                                        } else {
                                            const idx = self.selectedRowKeys.indexOf(rowKey);
                                            if (idx > -1) {
                                                self.selectedRowKeys.splice(idx, 1);
                                            }
                                        }
                                    }
                                    self._outputSelection();
                                });
                            }
                        }
                    }
                    observer.disconnect();
                });
                observer.observe(element, {
                    attributes: true,
                    attributeFilter: ['class', 'aria-label']
                });
                return element;
            };
            console.log('[123FASTLINK] [Selector] HOOK已激活');
        }

        _bindSelectAllEvent(checkbox) {
            if (checkbox.dataset.selectAllBound) return;
            checkbox.dataset.selectAllBound = 'true';
            checkbox.addEventListener('click', () => {
                if (checkbox.checked) {
                    this.isSelectAll = true;
                    this.unselectedRowKeys = [];
                    this.selectedRowKeys = [];
                } else {
                    this.isSelectAll = false;
                    this.selectedRowKeys = [];
                    this.unselectedRowKeys = [];
                }
                this._outputSelection();
            });
        }

        _outputSelection() {
            if (this.isSelectAll) {
                if (this.unselectedRowKeys.length === 0) {
                    console.log('全选');
                } else {
                    console.log('全选,反选这些:', this.unselectedRowKeys);
                }
            } else {
                console.log('当前选中:', this.selectedRowKeys);
            }
        }

        getSelection() {
            return {
                isSelectAll: this.isSelectAll,
                selectedRowKeys: [...this.selectedRowKeys],
                unselectedRowKeys: [...this.unselectedRowKeys]
            };
        }
    }

    // 3. 秒传链接生成/转存类
    class ShareLinkManager {
        constructor(apiClient) {
            this.apiClient = apiClient;
            // this.selector = selector;
            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 = [];
            // 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;
        }

        /**
         * 递归获取指定文件夹ID下的所有文件信息
         * @param {*} parentFileId
         * @param folderName
         * @param {*} total 仅用来计算进度
         */
        async _getAllFileInfoByFolderId(parentFileId, folderName = '', total) {
            //console.log("[123FASTLINK] [ShareLinkManager]", 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);
            console.log("[123FASTLINK] [ShareLinkManager]", "获取文件列表,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) {
                    console.error('[123FASTLINK] [ShareLinkManager]', '获取文件信息失败:', 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
            }));
        }

        /**
         * 获取this.fileInfoList的公共路径
         * @returns this.commonPath / commonPath
         */
        async _getCommonPath() {
            if (!this.fileInfoList || this.fileInfoList.length === 0) return '';

            // 提取所有路径并转换为目录组件数组
            const pathArrays = this.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 {*} fileSelectionDetails - 来自selector.getSelection()
         * @returns  - 文件信息在this.fileInfoList里
         * @returns  - this.commonPath-公共路径
         * @returns  - boolean - 是否成功获取到文件
         */
        async _getSelectedFilesInfo(fileSelectionDetails) {
            this.fileInfoList = [];
            if (!fileSelectionDetails.isSelectAll && fileSelectionDetails.selectedRowKeys.length === 0) {
                return false;
            }
            let fileSelectFolderInfoList = [];
            if (fileSelectionDetails.isSelectAll) {
                this.progress = 10;
                this.progressDesc = "正在递归获取选择的文件..."
                let allFileInfo = (await this.apiClient.getFileList(await this.apiClient.getParentFileId())).data.InfoList.map(file => ({
                    fileName: file.FileName, etag: file.Etag, size: file.Size, type: file.Type, fileId: file.FileId
                }));
                // 分开处理文件和文件夹
                let fileInfo = allFileInfo.filter(file => file.type !== 1);
                // 剔除反选的文件,并添加文件夹名称
                fileInfo.filter(file => !fileSelectionDetails.unselectedRowKeys.includes(file.fileId.toString())).forEach(file => {
                    file.path = file.fileName;
                });
                // 方便后面继续添加
                this.fileInfoList.push(...fileInfo);
                fileSelectFolderInfoList = allFileInfo.filter(file => file.type === 1).filter(file => !fileSelectionDetails.unselectedRowKeys.includes(file.fileId.toString()));
            } else {
                // 未全选
                let fileSelectIdList = fileSelectionDetails.selectedRowKeys;
                if (!fileSelectIdList.length) {
                    this.progress = 100;
                    this.progressDesc = "未选择文件";
                    return false;
                }
                // 获取文件信息

                const allFileInfo = await this._getFileInfoBatch(fileSelectIdList);
                const fileInfo = allFileInfo.filter(info => info.type !== 1);
                fileInfo.forEach(file => {
                    file.path = file.fileName;
                });
                this.fileInfoList.push(...fileInfo);
                fileSelectFolderInfoList = allFileInfo.filter(info => info.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; // 已经获取的文件保留
                }

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

            return true;
        }

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

            // 获取选中的文件(文件夹)的详细信息
            // this.fileInfoList, this.commonPath
            const result = await this._getSelectedFilesInfo(fileSelectionDetails);
            if (!result) return '';
            //// if (hasFolder) alert("文件夹暂时无法秒传,将被忽略");
            this.progressDesc = "秒传链接生成完成";
            return this.buildShareLink(this.fileInfoList, this.commonPath);
        }

        /**
         * 拼接链接
         * @param {*} fileInfoList - {etag: string, size: number, path: string, fileName: string}
         */
        buildShareLink(fileInfoList, commonPath) {
            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 shareLink;
        }

        /**
         * 解析秒传链接
         * @param {*} shareLink     秒传链接
         * @param {*} InputUsesBase62  输入是否使用Base62
         * @param {*} outputUsesBase62 输出是否使用Base62
         * @returns {Array} - {etag: string, size: number, path: string, fileName: string}
         */
        _parseShareLink(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 {
                    console.error('[123FASTLINK] [ShareLinkManager]', '不支持的公共路径格式', commonPathLinkPrefix);
                    return "[123FASTLINK] [ShareLinkManager] 不支持的公共路径格式:" + commonPathLinkPrefix;
                }

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

            const shareLinkList = Array.from(shareFileInfo.replace(/\r?\n/g, '$').split('$'));
            this.commonPath = commonPath;
            return shareLinkList.map(singleShareLink => {
                const singleFileInfoList = singleShareLink.split('#');
                if (singleFileInfoList.length < 3) return null;
                return {
                    etag: InputUsesBase62 ? (outputUsesBase62 ? singleFileInfoList[0] : this._base62ToHex(singleFileInfoList[0])) : (outputUsesBase62 ? this._hexToBase62(singleFileInfoList[0]) : singleFileInfoList[0]),
                    size: singleFileInfoList[1],
                    path: singleFileInfoList[2],
                    fileName: singleFileInfoList[2].split('/').pop()
                };
            }).filter(Boolean);
        }

        /**
         * 先创建文件夹,给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) {
            const total = shareFileList.length;
            // 文件夹创建,并为shareFileList添加parentFolderId------------------------------------
            // 记录文件夹(path)
            this.progressDesc = `正在创建文件夹...`;
            let folder = {};
            // 如果存在commonPath,先创建文件夹
            const rootFolderId = await this.apiClient.getParentFileId();
            if (this.commonPath) {
                const commonPathParts = this.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[this.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++;
                    console.error('[123FASTLINK] [ShareLinkManager]', '保存文件失败:', fileInfo.fileName);
                    fileInfo.error = reuse[1];
                    failedList.push(fileInfo);
                }
                completed++;
                console.log('[123FASTLINK] [ShareLinkManager]', '已保存:', fileInfo.fileName);
                this.progress = Math.round((completed / total) * 100);
                this.progressDesc = `正在保存第 ${completed} / ${total} 个文件...`;
            }
            // this.progress = 100;
            // this.progressDesc = "保存完成";
            return {
                success: successList, failed: failedList, commonPath: this.commonPath
            };
        }

        /**
         * 保存秒传链接
         */
        async saveTextShareLink(shareLink) {
            const shareFileList = this._parseShareLink(shareLink);
            return this._saveFileList(await this._makeDirForFiles(shareFileList));
        }

        /**
         * 保存JSON格式的秒传链接
         * @param {string} jsonContent
         * @returns {Promise<object>} - 保存结果
         */
        async saveJsonShareLink(jsonContent) {
            const shareFileList = this._parseJsonShareLink(jsonContent);
            return this._saveFileList(await this._makeDirForFiles(shareFileList));
        }

        async saveShareLink(content) {
            let saveResult = { success: [], failed: [] };
            try {
                // 尝试作为JSON解析
                const jsonData = this.safeParse(content);
                if (jsonData) {
                    saveResult = await this.saveJsonShareLink(jsonData);
                } else {
                    // 作为普通秒传链接处理
                    saveResult = await this.saveTextShareLink(content);
                    console.log('保存结果:', saveResult);
                }
            } catch (error) {
                console.error('保存失败:', error);
                saveResult = { success: [], failed: [] };
            }
            return saveResult;
        }

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

        // -------------------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} - {etag: string, size: number, path: string, fileName: string}
         */
        _parseJsonShareLink(jsonData) {
            this.commonPath = jsonData['commonPath'] || '';
            const shareFileList = jsonData['files'];
            if (jsonData['usesBase62EtagsInExport']) {
                shareFileList.forEach(file => {
                    file.etag = this._base62ToHex(file.etag);
                });
            }
            shareFileList.forEach(file => {
                file.fileName = file.path.split('/').pop();
            });
            return shareFileList;
        }

        // 格式化文件大小
        _formatSize(size) {
            if (size < 1024) return size + ' B';
            if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB';
            if (size < 1024 * 1024 * 1024) return (size / 1024 / 1024).toFixed(2) + ' MB';
            return (size / 1024 / 1024 / 1024).toFixed(2) + ' GB';
        }

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

        /**
         * 将秒传链接转换为JSON格式
         * @param {*} shareLink
         * @returns
         */
        shareLinkToJson(shareLink) {
            const fileInfo = this._parseShareLink(shareLink);
            if (fileInfo.length === 0) {
                console.error('[123FASTLINK] [ShareLinkManager]', '解析秒传链接失败:', shareLink);
                return {
                    error: '解析秒传链接失败'
                };
            }
            if (this.usesBase62EtagsInExport) {
                fileInfo.forEach(f => {
                    f.etag = this._hexToBase62(f.etag);
                });
            }
            const totalSize = fileInfo.reduce((sum, f) => sum + Number(f.size), 0);
            return {
                scriptVersion: this.scriptVersion,
                exportVersion: "1.0",
                usesBase62EtagsInExport: this.usesBase62EtagsInExport,
                commonPath: this.commonPath,
                totalFilesCount: fileInfo.length,
                totalSize,
                formattedTotalSize: this._formatSize(totalSize),
                files: fileInfo.map(f => ({
                    // 去掉fileName
                    ...f, fileName: undefined
                }))
            };
        }
    }

    // 4. UI管理类
    class UiManager {
        constructor(shareLinkManager, selector) {
            this.shareLinkManager = shareLinkManager;
            this.selector = selector;
            this.isProgressMinimized = false;
            this.minimizeWidgetId = 'progress-minimize-widget';
            // this.currentShareLink = ''; // 存储当前秒传链接
            // taskList = [{id: string, type: 'generate'|'save', params: {}}]
            this.taskList = []; // 任务列表
            this.isTaskRunning = false; // 任务是否在运行
            this.taskIdCounter = 0; // 任务ID计数器
            this.currentTask = null; // 当前正在执行的任务
            // this.taskCancel = false; // 取消当前任务的请求标志
        }

        /**
         * 初始化UI管理器,插入样式表,设置按钮事件
         */
        init() {

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

            window.addEventListener('load', () => {
                this.insertStyle();
                this.addButton();
            });

            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);
        }

        /**
         * 插入样式表
         */
        insertStyle() {
            if (!document.getElementById("modal-style")) {
                let style = document.createElement("style");
                style.id = "modal-style";
                style.innerHTML = `
                .modal-overlay { display: flex; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(4px); justify-content: center; align-items: center; z-index: 10000; animation: fadeIn 0.3s ease-out; }
                .modal { background: #fff; padding: 32px; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15), 0 8px 16px rgba(0, 0, 0, 0.1); text-align: center; width: 480px; max-width: 90vw; max-height: 90vh; overflow: hidden; position: relative; border: 1px solid rgba(255, 255, 255, 0.2); animation: modalSlideIn 0.3s ease-out; }
                .close-btn { position: absolute; top: 16px; right: 16px; background: transparent; border: none; font-size: 24px; color: #999; cursor: pointer; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; }
                .close-btn:hover { background: rgba(244, 67, 54, 0.1); color: #f44336; transform: scale(1.1); }
                .modal textarea { width: 100%; padding: 16px; margin: 0 0 24px 0; border: 2px solid #e1e5e9; border-radius: 12px; resize: vertical; min-height: 120px; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 14px; line-height: 1.5; background: #fafbfc; transition: all 0.3s ease; box-sizing: border-box; outline: none; }
                .modal textarea:focus { border-color: #4CAF50; background: #ffffff; box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1); transform: translateY(-2px); }
                .modal textarea.drag-over { border-color: #4CAF50; background: #f0f8f0; }
                .copy-btn { background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); color: white; border: none; padding: 14px 32px; cursor: pointer; border-radius: 8px; font-size: 16px; font-weight: 500; min-width: 120px; position: relative; overflow: hidden; transition: all 0.3s ease; box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3); }
                .copy-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(76, 175, 80, 0.4); }
                .copy-btn:active { transform: translateY(0); box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3); }
                .button-group { display: flex; gap: 12px; align-items: center; justify-content: center; position: relative; }
                .copy-dropdown { position: relative; display: inline-block; }
                .copy-dropdown-menu {position: absolute; top: 100%; left: 0; background: #fff; border: 1px solid #e1e5e9; border-radius: 10px; box-shadow: 0 4px 16px rgba(0,0,0,0.12); min-width: 120px; z-index: 10001; margin-top: 6px; padding: 0; display: none;}
                .copy-dropdown:hover .copy-dropdown-menu, .copy-dropdown-menu:hover { display: block !important; }
                .copy-dropdown-menu { bottom: 100% !important; top: auto !important; margin-bottom: 6px !important; margin-top: 0 !important; }
                .copy-dropdown-menu::before { content: ''; position: absolute; bottom: -6px; left: 0; width: 100%; height: 6px; background: transparent; }
                .copy-dropdown-item { padding: 10px 18px; cursor: pointer; font-size: 14px; border-bottom: 1px solid #f0f0f0; background: #fff; transition: background 0.2s;}
                .copy-dropdown-item:last-child { border-bottom: none; }
                .copy-dropdown-item:hover { background: #e8f5e9; color: #388e3c;}
                .copy-dropdown-item:first-child { border-radius: 10px 10px 0 0; }
                .copy-dropdown-item:last-child { border-radius: 0 0 10px 10px; }
                .export-btn { background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%); color: white; border: none; padding: 14px 24px; cursor: pointer; border-radius: 8px; font-size: 16px; font-weight: 500; min-width: 100px; transition: all 0.3s ease; box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3); }
                .export-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(33, 150, 243, 0.4); }
                .file-input-btn { background: linear-gradient(135deg, #FF9800 0%, #F57C00 100%); color: white; border: none; padding: 14px 24px; cursor: pointer; border-radius: 8px; font-size: 16px; font-weight: 500; min-width: 100px; transition: all 0.3s ease; box-shadow: 0 4px 12px rgba(255, 152, 0, 0.3); }
                .file-input-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(255, 152, 0, 0.4); }
                .file-input { display: none; }
                .toast { position: fixed; top: 20px; right: 20px; background: #fff; color: #333; padding: 12px 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 10002; font-size: 14px; max-width: 300px; animation: toastSlideIn 0.3s ease-out; }
                .toast.success { border-left: 4px solid #4CAF50; }
                .toast.error { border-left: 4px solid #f44336; }
                .toast.warning { border-left: 4px solid #ff9800; }
                .toast.info { border-left: 4px solid #2196F3; }
                .progress-minimize-btn{position:absolute;left:-10px;top:-10px;width:30px;height:30px;border-radius:50%;background:#ffc504;color:#000000ff;border:none;display:flex;align-items:center;justify-content:center;font-weight:700;cursor:pointer;box-shadow:0 2px 6px rgba(0,0,0,0.15);z-index:10003}.progress-minimize-btn:hover{transform:scale(1.05)}
                .minimized-widget{position:fixed;right:20px;bottom:20px;width:220px;background:#fff;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,0.18);padding:10px 12px;z-index:10005;display:flex;align-items:center;gap:10px;cursor:pointer}
                .minimized-widget .mini-bar{flex:1}
                .minimized-widget .mini-title{font-size:12px;color:#333;margin-bottom:6px}
                .minimized-widget .mini-progress{height:8px;background:#eee;border-radius:6px;overflow:hidden}
                .minimized-widget .mini-progress>i{display:block;height:100%;background:#4CAF50;width:0%;transition:width 0.2s}
                .minimized-widget .mini-percent{font-size:12px;color:#666;width:36px;text-align:right}
                .toast-shake {animation: toastShake 0.4s cubic-bezier(.36,.07,.19,.97) both, toastSlideIn 0.3s ease-out;}
                #progress-title { margin-bottom:16px; font-size:18px; word-wrap: break-word; word-break: break-all; white-space: pre-wrap; }
                #progress-desc { margin-top:8px; font-size:13px; color:#888; word-wrap: break-word; word-break: break-all; white-space: pre-wrap; line-height: 1.4; }
                .task-list-container { margin-top: 16px; }
                .task-list-toggle { background: transparent; border: 1px solid #ddd; color: #666; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 12px; width: 100%; text-align: left; display: flex; align-items: center; justify-content: space-between; transition: all 0.2s; }
                .task-list-toggle:hover { background: #f5f5f5; border-color: #bbb; }
                .task-list-toggle.active { background: #f0f8ff; border-color: #4CAF50; }
                .task-list { max-height: 120px; overflow-y: auto; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 6px 6px; background: #fafafa; display: none; }
                .task-list.show { display: block; }
                .task-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-bottom: 1px solid #eee; font-size: 13px; }
                .task-item:last-child { border-bottom: none; }
                .task-item .task-name { color: #333; flex: 1; }
                .task-item .task-remove { background: #ff4757; color: white; border: none; border-radius: 4px; padding: 2px 6px; font-size: 11px; cursor: pointer; transition: background 0.2s; }
                .task-item .task-remove:hover { background: #ff3742; }
                @keyframes toastShake { 10%, 90% { transform: translateX(-2px); } 20%, 80% { transform: translateX(4px); } 30%, 50%, 70% { transform: translateX(-8px); } 40%, 60% { transform: translateX(8px); } 100% { transform: translateX(0); }
                `;
                document.head.appendChild(style);
            }
        }

        /**
         * 显示提示消息(右上角)
         * @param {*} message
         * @param {*} type
         * @param {*} duration
         */
        showToast(message, type = 'info', duration = 3000) {
            // this.insertStyle();
            const toast = document.createElement('div');
            toast.className = `toast ${type} toast-shake`; // 添加 toast-shake 类
            toast.textContent = message;
            document.body.appendChild(toast);

            setTimeout(() => {
                toast.style.animation = 'toastSlideOut 0.3s ease-out forwards';
                setTimeout(() => {
                    if (toast.parentNode) {
                        toast.parentNode.removeChild(toast);
                    }
                }, 300);
            }, duration);
        }

        /**
         * 显示复制弹窗
         * @param {*} defaultText
         */
        showCopyModal(defaultText = "") {
            // this.insertStyle();
            // this.currentShareLink = defaultText;
            // let existingModal = document.getElementById('modal');
            // if (existingModal) existingModal.remove();

            // 获取文件名列表
            let fileListHtml = '';
            if (Array.isArray(this.shareLinkManager.fileInfoList) && this.shareLinkManager.fileInfoList.length > 0) {
                fileListHtml = `<div style="max-height:120px;overflow-y:auto;background:#f8f8f8;border-radius:6px;padding:8px 10px;margin-bottom:16px;text-align:left;font-size:13px;">
                    <div style='color:#888;margin-bottom:4px;'>文件列表(共${this.shareLinkManager.fileInfoList.length}个):</div>
                    ${this.shareLinkManager.fileInfoList.map(f => `<div style='color:#333;word-break:break-all;margin:2px 0;'>${f.path}</div>`).join('')}
                </div>`;
            }

            let modalOverlay = document.createElement('div');
            modalOverlay.className = 'modal-overlay';
            modalOverlay.id = 'modal';
            modalOverlay.innerHTML = `
                <div class="modal">
                    <button class="close-btn" onclick="document.getElementById('modal').remove()">×</button>
                    <h3>🚀 秒传链接</h3>
                    ${fileListHtml}
                    <textarea id="copyText" placeholder="请输入或粘贴秒传链接...">${defaultText}</textarea>
                    <div class="button-group">
                        <div class="copy-dropdown">
                            <button class="copy-btn" id="massageboxButton">
                                复制 ▼
                            </button>
                            <div class="copy-dropdown-menu">
                                <div class="copy-dropdown-item" data-type="json">复制JSON</div>
                                <div class="copy-dropdown-item" data-type="text">复制纯文本</div>
                            </div>
                        </div>
                        <button class="export-btn" id="exportJsonButton">导出JSON</button>
                    </div>
                </div>
            `;
            // 导出JSON按钮事件
            modalOverlay.querySelector('#exportJsonButton').addEventListener('click', (e) => {
                e.stopPropagation();
                this.exportJson();
            })
            // 下拉菜单悬停控制
            const dropdown = modalOverlay.querySelector('.copy-dropdown');
            const dropdownMenu = modalOverlay.querySelector('.copy-dropdown-menu');
            let hideTimer;

            // 鼠标进入下拉容器时显示菜单
            dropdown.addEventListener('mouseenter', () => {
                clearTimeout(hideTimer);
                dropdownMenu.style.display = 'block';
            });

            // 鼠标离开下拉容器时延迟隐藏菜单
            dropdown.addEventListener('mouseleave', () => {
                hideTimer = setTimeout(() => {
                    dropdownMenu.style.display = 'none';
                }, 300); // 300ms延迟,给用户足够时间移动鼠标
            });

            // 鼠标进入菜单时取消隐藏
            dropdownMenu.addEventListener('mouseenter', () => {
                clearTimeout(hideTimer);
            });

            // 鼠标离开菜单时隐藏
            dropdownMenu.addEventListener('mouseleave', () => {
                hideTimer = setTimeout(() => {
                    dropdownMenu.style.display = 'none';
                }, 100); // 稍微延迟避免误触
            });

            // 复制按钮点击直接复制纯文本
            modalOverlay.querySelector('#massageboxButton').addEventListener('click', (e) => {
                e.stopPropagation();
                this.copyContent('text');
            });

            // 点击菜单项复制对应类型
            modalOverlay.querySelectorAll('.copy-dropdown-item').forEach(item => {
                item.addEventListener('click', (e) => {
                    e.stopPropagation();
                    const type = item.dataset.type;
                    this.copyContent(type);
                    clearTimeout(hideTimer);
                    dropdownMenu.style.display = 'none'; // 点击后隐藏菜单
                });
            });

            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 === 'json') {
                try {
                    const jsonData = this.shareLinkManager.shareLinkToJson(contentToCopy);
                    contentToCopy = JSON.stringify(jsonData, null, 2);
                } catch (error) {
                    this.showToast('转换JSON失败: ' + error.message, 'error');
                    return;
                }
            }

            navigator.clipboard.writeText(contentToCopy).then(() => {
                this.showToast(`已成功复制${type === 'json' ? 'JSON' : '纯文本'}到剪贴板 📋`, '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;
            }

            try {
                const jsonData = this.shareLinkManager.shareLinkToJson(shareLink);
                const jsonContent = JSON.stringify(jsonData, null, 2);
                const filename = this.getExportFilename(shareLink);

                this.downloadJsonFile(jsonContent, filename);
                this.showToast('JSON文件导出成功 📁', 'success');
            } catch (error) {
                this.showToast('导出失败: ' + error.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);
        }

        // 获取文件名用于JSON导出
        getExportFilename(shareLink) {
            if (this.shareLinkManager.commonPath) {
                const commonPath = this.shareLinkManager.commonPath.replace(/\/$/, ''); // 去除末尾斜杠
                return `${commonPath}.json`;
            }
            const lines = shareLink.trim().split('\n').filter(Boolean);
            if (lines.length === 0) return 'export.json';
            const firstLine = lines[0];
            const parts = firstLine.split('#');
            if (parts.length >= 3) {
                const fileName = parts[2];
                const baseName = fileName.split('/').pop().split('.')[0] || 'export';
                return `${baseName}.json`;
            }
            return 'export.json';
        }

        /**
         * 显示或更新进度模态框
         * @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('progress-modal');
            if (!modal) {
                modal = document.createElement('div');
                modal.id = 'progress-modal';
                modal.style = 'position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:10001;background:rgba(0,0,0,0.3);display:flex;align-items:center;justify-content:center;';
                modal.innerHTML = `
                    <div id="progress-card" style="position:relative;background:#fff;padding:32px 48px;border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,0.15);min-width:320px;max-width:320px;text-align:center;">
                        <button class="progress-minimize-btn" title="最小化">−</button>
                        <div id="progress-title" style="margin-bottom:16px;font-size:18px;word-wrap:break-word;word-break:break-all;white-space:pre-wrap;">${title + (taskCount > 1 ? ` - 队列 ${taskCount}` : '')}</div>
                        <div style="background:#eee;border-radius:8px;overflow:hidden;height:18px;">
                            <div id="progress-bar" style="background:#4CAF50;height:18px;width:${percent}%;transition:width 0.2s;"></div>
                        </div>
                        <div id="progress-percent" style="margin-top:8px;font-size:14px;">${percent}%</div>
                        <div id="progress-desc" style="margin-top:8px;font-size:13px;color:#888;word-wrap:break-word;word-break:break-all;white-space:pre-wrap;line-height:1.4;">${desc}</div>
                    </div>
                `;
                document.body.appendChild(modal);

                // 绑定最小化按钮事件(点击后移除模态并创建右下角浮动卡片)
                const btn = modal.querySelector('.progress-minimize-btn');
                if (!btn.dataset.bound) {
                    btn.dataset.bound = 'true';
                    btn.addEventListener('click', (e) => {
                        e.stopPropagation();
                        this.isProgressMinimized = true;
                        // 读取当前进度显示到浮动卡片
                        const curTitle = modal.querySelector('#progress-title')?.innerText || title + (taskCount > 1 ? ` - 队列 ${taskCount}` : '');
                        const curPercent = parseInt(modal.querySelector('#progress-percent')?.innerText || percent) || 0;
                        const curDesc = modal.querySelector('#progress-desc')?.innerText || desc;
                        this.removeProgressModalAndKeepState();
                        this.updateMinimizedWidget(curTitle, curPercent, curDesc, taskCount);
                    });
                }
            } else {
                const titleElement = modal.querySelector('#progress-title');
                const descElement = modal.querySelector('#progress-desc');

                titleElement.innerText = title + (taskCount > 1 ? ` - 队列 ${taskCount}` : '');
                titleElement.style.cssText = 'margin-bottom:16px;font-size:18px;word-wrap:break-word;word-break:break-all;white-space:pre-wrap;line-height:1.4;';

                modal.querySelector('#progress-bar').style.width = percent + '%';
                modal.querySelector('#progress-percent').innerText = percent + '%';

                descElement.innerText = desc;
                descElement.style.cssText = 'margin-top:8px;font-size:13px;color:#888;word-wrap:break-word;word-break:break-all;white-space:pre-wrap;line-height:1.4;';
            }
            // 更新任务列表
            this.manageTaskList(modal);
        }


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

            // 没有任务时删除任务列表
            if (currentTaskCount === 0) {
                existingContainer?.remove();
                return;
            }

            // 生成任务列表HTML
            const generateHtml = () => `
                <div class="task-list-container">
                    <button class="task-list-toggle" id="task-list-toggle">
                        <span>任务队列 (${currentTaskCount})</span>
                        <span>▼</span>
                    </button>
                    <div class="task-list" id="task-list">
                        ${this.taskList.map(task => {
                const isCurrentTask = this.currentTask && this.currentTask.id === task.id;
                const taskStatus = isCurrentTask ? ' (执行中)' : '';
                const taskClass = isCurrentTask ? ' style="background: #e8f5e8; border-left: 3px solid #4CAF50;"' : '';
                return `
                                <div class="task-item" data-task-id="${task.id}"${taskClass}>
                                    <span class="task-name">${task.type === 'generate' ? '链接生成' : '链接转存'}${taskStatus}</span>
                                    <button class="task-remove" data-task-id="${task.id}">删除</button>
                                </div>
                            `;
                //<button class="task-remove" data-task-id="${task.id}" ${isCurrentTask ? 'disabled style="opacity: 0.5; cursor: not-allowed;"' : ''}>删除</button>
            }).join('')}
                    </div>
                </div>
            `;

            // 绑定事件
            const bindEvents = (container) => {
                const toggle = container.querySelector('#task-list-toggle');
                const taskList = container.querySelector('#task-list');

                // 切换展开/收起
                toggle?.addEventListener('click', () => {
                    const isShown = taskList.classList.toggle('show');
                    toggle.classList.toggle('active', isShown);
                    toggle.querySelector('span:last-child').textContent = isShown ? '▲' : '▼';
                });

                // 删除任务
                container.querySelectorAll('.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 progressDesc = modal.querySelector('#progress-desc');
                progressDesc.insertAdjacentHTML('afterend', generateHtml());
                bindEvents(modal.querySelector('.task-list-container'));
            } else {
                // 检查是否需要重建(任务数量变化或当前任务状态变化)
                const existingTaskItems = existingContainer.querySelectorAll('.task-item');
                const hasCurrentTaskChanged = existingContainer.querySelector('.task-item[style*="background: #e8f5e8"]') ? !this.currentTask : !!this.currentTask;

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

                    const progressDesc = modal.querySelector('#progress-desc');
                    progressDesc.insertAdjacentHTML('afterend', generateHtml());
                    const newContainer = modal.querySelector('.task-list-container');
                    bindEvents(newContainer);

                    // 恢复展开状态
                    if (wasExpanded) {
                        const taskList = newContainer.querySelector('.task-list');
                        const toggle = newContainer.querySelector('#task-list-toggle');
                        taskList.classList.add('show');
                        toggle.classList.add('active');
                        toggle.querySelector('span:last-child').textContent = '▲';
                    }
                } else {
                    // 只更新计数
                    const toggleSpan = existingContainer.querySelector('#task-list-toggle span:first-child');
                    if (toggleSpan) toggleSpan.textContent = `任务队列 (${currentTaskCount})`;
                }
            }
        }

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

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

        // 创建或更新右下角最小化浮动进度条卡片
        updateMinimizedWidget(title = '正在处理...', percent = 0, desc = '', taskCount = 1) {
            let widget = document.getElementById(this.minimizeWidgetId);
            // 红点提示,仅在剩余任务数>=2时显示
            let redDotHtml = '';
            if (this.taskList.length >= 1) {
                redDotHtml = `<button class="mini-red-dot" style="position:absolute;left:-8px;top:-8px;width:22px;height:22px;background:#f44336;color:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;z-index:2;box-shadow:0 2px 6px rgba(0,0,0,0.12);">${this.taskList.length}</button>`;
            }
            const html = `
                ${redDotHtml}
                <div class="mini-bar">
                    <div class="mini-title">${title + (taskCount > 1 ? ` - 队列 ${taskCount}` : '')}</div>
                    <div class="mini-progress"><i style="width:${percent}%"></i></div>
                </div>
                <div class="mini-percent">${percent}%</div>
            `;
            if (!widget) {
                widget = document.createElement('div');
                widget.id = this.minimizeWidgetId;
                widget.className = 'minimized-widget';
                widget.innerHTML = html;
                // 修复点击不灵敏:用mousedown替换click,并阻止冒泡
                widget.addEventListener('mousedown', (e) => {
                    e.stopPropagation();
                    e.preventDefault();
                    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 launchProgressModal(fileSelectInfo) {
            // 轮询进度
            const mgr = this.shareLinkManager;
            // this.showProgressModal("生成秒传链接", 0, "准备中...");
            mgr.progress = 0;
            const poll = setInterval(() => {
                this.updateProgressModal("生成秒传链接", mgr.progress, mgr.progressDesc, this.taskList.length);
                if (mgr.progress > 100) {
                    clearInterval(poll);
                    setTimeout(() => this.hideProgressModal(), 500);
                }
            }, 500);

            const shareLink = await mgr.generateShareLink(fileSelectInfo);

            // 清除任务取消标志
            this.shareLinkManager.taskCancel = false;

            if (!shareLink) {
                this.showToast("没有选择文件", 'warning');
                clearInterval(poll);
                return;
            }
            clearInterval(poll);
            this.hideProgressModal();
            this.showCopyModal(shareLink);
        }

        /**
         * 显示保存结果模态框
         * @param result - {success: [], failed: []}
         * @returns {Promise<void>}
         */
        async showSaveResultsModal(result) {
            // this.insertStyle();
            // let existingModal = document.getElementById('results-modal');
            // if (existingModal) existingModal.remove();
            const totalCount = result.success.length + result.failed.length;
            const successCount = result.success.length;
            const failedCount = result.failed.length;
            let failedListHtml = '';
            if (failedCount > 0) {
                failedListHtml = `
                    <div style="margin-top: 12px; color: #f44336; font-size: 14px;">
                        <div style="margin-bottom: 6px;">失败文件列表:</div>
                        <div style="max-height: 160px; overflow-y: auto; background: #f5f5f5; border-radius: 4px; padding: 8px;">
                            ${result.failed.map(fileInfo => `<div style="font-size: 13px; color: #b71c1c; margin: 2px 0;">${fileInfo.fileName} ${fileInfo.error ? `<span style="color: #f44336;">(${fileInfo.error})</span>` : ''}</div>`).join('')}
                        </div>
                    </div>
                `;
            }
            let modalOverlay = document.createElement('div');
            modalOverlay.className = 'modal-overlay';
            modalOverlay.id = 'results-modal';
            modalOverlay.innerHTML = `
                <div class="modal">
                    <button class="close-btn" onclick="document.getElementById('results-modal').remove()">×</button>
                    <h3>📊 保存结果</h3>
                    <div style="margin: 20px 0; text-align: left;">
                        <div style="font-size: 16px; margin-bottom: 16px;">
                            <span style="color: #666;">总计:</span><strong>${totalCount}</strong> 个文件
                        </div>
                        <div style="font-size: 16px; margin-bottom: 8px; color: #4CAF50;">
                            ✅ 成功:<strong>${successCount}</strong> 个
                        </div>
                        <div style="font-size: 16px; margin-bottom: 8px; color: ${failedCount > 0 ? '#f44336' : '#666'};">
                            ${failedCount > 0 ? '❌' : '✅'} 失败:<strong>${failedCount}</strong> 个
                        </div>
                        ${failedListHtml}
                    </div>
                    <div class="button-group">
                        <button class="copy-btn" onclick="document.getElementById('results-modal').remove()">确定</button>
                        ${failedCount > 0 ? `
                <div class="copy-dropdown">
                    <button class="file-input-btn" id="retryButton">重试失败 ▼</button>
                    <div class="copy-dropdown-menu">
                        <div class="copy-dropdown-item" data-action="retry">重试失败</div>
                        <div class="copy-dropdown-item" data-action="export">导出失败链接</div>
                    </div>
                </div>` : ''}
                    </div>
                </div>
            `;

            // 添加重试和导出按钮事件
            if (failedCount > 0) {
                const dropdownMenu = modalOverlay.querySelector('.copy-dropdown-menu');
                dropdownMenu.querySelectorAll('.copy-dropdown-item').forEach(item => {
                    item.addEventListener('click', async () => {
                        const action = item.dataset.action;
                        if (action === 'retry') {
                            document.getElementById('results-modal').remove();
                            this.addAndRunTask('retry', { fileList: result.failed });
                        } else if (action === 'export') {
                            const shareLink = this.shareLinkManager.buildShareLink(result.failed, result.commonPath || '');
                            this.showCopyModal(shareLink);
                        }
                    });
                });
            }

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

            document.body.appendChild(modalOverlay);
        }

        /**
         * 任务函数 - 启动从输入的内容解析并保存秒传链接,UI层面的保存入口,retry为是可以重试失败的文件
         * @param {*} content - 输入内容(秒传链接/JSON)
         */
        async launchSaveLink(content, retry = false) {
            this.updateProgressModal("保存秒传链接", 0, "准备中...");
            this.shareLinkManager.progress = 0;
            const poll = setInterval(() => {
                this.updateProgressModal("保存秒传链接", this.shareLinkManager.progress, this.shareLinkManager.progressDesc, this.taskList.length);
                // 正常情况下不主动清除
                if (this.shareLinkManager.progress > 100) {
                    clearInterval(poll);
                }
            }, 100);
            let saveResult;
            if (!retry) {
                saveResult = await this.shareLinkManager.saveShareLink(content);
            } else {
                saveResult = await this.shareLinkManager.retrySaveFailed(content);
            }
            // 清除任务取消标志
            this.shareLinkManager.taskCancel = false;

            clearInterval(poll);
            this.hideProgressModal();
            this.showSaveResultsModal(saveResult);
            this.renewWebPageList();
            this.showToast(saveResult ? "保存成功" : "保存失败", saveResult ? 'success' : 'error');
        }

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

        /**
         * 显示输入模态框
         */
        async showInputModal() {
            // this.insertStyle();
            let existingModal = document.getElementById('save-modal');
            if (existingModal) existingModal.remove();

            let modalOverlay = document.createElement('div');
            modalOverlay.className = 'modal-overlay';
            modalOverlay.id = 'save-modal';
            modalOverlay.innerHTML = `
                <div class="modal">
                    <button class="close-btn" onclick="document.getElementById('save-modal').remove()">×</button>
                    <h3>📥 保存秒传链接</h3>
                    <textarea id="saveText" placeholder="请输入或粘贴秒传链接,或拖入JSON文件导入..."></textarea>
                    <div class="button-group">
                        <button class="copy-btn" id="saveButton">保存</button>
                        <button class="file-input-btn" id="selectFileButton">选择JSON</button>
                        <input type="file" class="file-input" id="jsonFileInput" accept=".json">
                    </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 = document.getElementById("saveText").value;
                if (!content.trim()) {
                    this.showToast("请输入秒传链接或导入JSON文件", 'warning');
                    return;
                }

                modalOverlay.remove();

                this.addAndRunTask('save', { content });
            });

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

            document.body.appendChild(modalOverlay);
            setTimeout(() => {
                const textarea = modalOverlay.querySelector('#saveText');
                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);
                }
            });
        }

        /**
         * 读取JSON文件并将内容填充到文本区域
         * @param {*} file - 要读取的文件
         * @param {*} textarea - 目标文本区域
         * @returns
         */
        readJsonFile(file, textarea) {
            if (!file.name.toLowerCase().endsWith('.json')) {
                this.showToast('请选择JSON文件', 'warning');
                return;
            }

            const reader = new FileReader();
            reader.onload = (e) => {
                try {
                    const jsonContent = e.target.result;
                    const jsonData = JSON.parse(jsonContent);

                    if (this.shareLinkManager.validateJson(jsonData)) {
                        // const shareLink = this.shareLinkManager.jsonToShareLink(jsonData);
                        textarea.value = jsonContent;
                        this.showToast('JSON文件导入成功 ✅', 'success');
                    } else {
                        this.showToast('无效的JSON格式', 'error');
                    }
                } catch (error) {
                    this.showToast('JSON文件解析失败: ' + error.message, 'error');
                }
            };
            reader.readAsText(file);
        }

        /**
         * 队列 - 运行下一个任务
         * @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;

            // 执行任务
            setTimeout(async () => {
                this.isTaskRunning = true;
                if (task.type === 'generate') {
                    // 生成秒传链接
                    await this.launchProgressModal(task.params.fileSelectInfo);
                } else if (task.type === 'save') {
                    // 保存秒传链接
                    await this.launchSaveLink(task.params.content);
                } else if (task.type === 'retry') {
                    // 重试任务
                    await this.launchSaveLink(task.params.fileList, true);
                }
                this.isTaskRunning = false;
                // 任务完成,从列表中移除
                this.taskList = this.taskList.filter(t => t.id !== task.id);
                this.currentTask = null;
                this.runNextTask();
            }, 100);
            this.showToast(`任务${task.id}开始执行...`, 'info');
        }

        /**
         * 解析、添加并触发任务
         * @param taskType  - 任务类型(generate/save/retry)
         * @param params - 任务参数
         */
        addAndRunTask(taskType, params = {}) {
            const taskId = ++this.taskIdCounter;
            if (taskType === 'generate') {
                // 获取选中文件
                const fileSelectInfo = this.selector.getSelection();
                if (!fileSelectInfo || fileSelectInfo.length === 0) {
                    this.showToast("请先选择文件", 'warning');
                    return;
                }
                this.taskList.push({ id: taskId, type: 'generate', params: { fileSelectInfo } });
            } else if (taskType === 'save') {
                this.taskList.push({ id: taskId, type: 'save', params: { content: params.content } });
            } else if (taskType === 'retry') {
                this.taskList.push({ id: taskId, type: 'retry', params: { fileList: params.fileList } });
            }
            this.runNextTask();
        }

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

        addButton() {
            const buttonExist = document.querySelector('.mfy-button-container');
            if (buttonExist) return;
            const isFilePage = window.location.pathname === "/" && (window.location.search === "" || window.location.search.includes("homeFilePath"));
            if (!isFilePage) return;
            const container = document.querySelector('.home-operator-button-group');
            if (!container) return;
            const btnContainer = document.createElement('div');
            btnContainer.className = 'mfy-button-container';
            btnContainer.style.position = 'relative';
            btnContainer.style.display = 'inline-block';
            const btn = document.createElement('button');
            btn.className = 'ant-btn css-dev-only-do-not-override-168k93g ant-btn-default ant-btn-color-default ant-btn-variant-outlined mfy-button create-button';
            btn.style.background = "#4CAF50";
            btn.style.color = "#fff";
            btn.style.border = "none";
            btn.innerHTML = `<svg x="1753345987410" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2781" width="16" height="16"><path d="M395.765333 586.570667h-171.733333c-22.421333 0-37.888-22.442667-29.909333-43.381334L364.768 95.274667A32 32 0 0 1 394.666667 74.666667h287.957333c22.72 0 38.208 23.018667 29.632 44.064l-99.36 243.882666h187.050667c27.509333 0 42.186667 32.426667 24.042666 53.098667l-458.602666 522.56c-22.293333 25.408-63.626667 3.392-54.976-29.28l85.354666-322.421333z" fill="#ffffff" p-id="2782"></path></svg><span>秒传</span>`;
            const dropdown = document.createElement('div');
            dropdown.className = 'mfy-dropdown';
            dropdown.style.display = 'none';
            dropdown.style.position = 'absolute';
            dropdown.style.top = 'calc(100% + 5px)';
            dropdown.style.left = '0';
            dropdown.style.backgroundColor = '#fff';
            dropdown.style.border = '1px solid #d9d9d9';
            dropdown.style.borderRadius = '10px';
            dropdown.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
            dropdown.style.zIndex = '1000';
            dropdown.style.minWidth = '120px';
            dropdown.style.overflow = 'hidden';
            dropdown.innerHTML = `
                <div class="mfy-dropdown-item" data-action="generate">生成秒传链接</div>
                <div class="mfy-dropdown-item" data-action="save">保存秒传链接</div>
            `;
            const style = document.createElement('style');
            style.textContent = `
                .mfy-button-container:hover .mfy-dropdown { display: block !important; }
                .mfy-dropdown-item { padding: 8px 12px; cursor: pointer; transition: background 0.3s; font-size: 14px; }
                .mfy-dropdown-item:hover { background-color: #f5f5f5; }
                .mfy-dropdown::before { content: ''; position: absolute; top: -5px; left: 0; width: 100%; height: 5px; background: transparent; }
            `;
            document.head.appendChild(style);
            btnContainer.appendChild(btn);
            btnContainer.appendChild(dropdown);
            container.insertBefore(btnContainer, container.firstChild);
            dropdown.querySelectorAll('.mfy-dropdown-item').forEach(item => {
                item.addEventListener('click', async () => {
                    const action = item.dataset.action;
                    if (action === 'generate') {
                        await this.addAndRunTask('generate');
                    } else if (action === 'save') {
                        await this.showInputModal();
                    }
                    dropdown.style.display = 'none';
                });
            });
            btnContainer.addEventListener('mouseenter', function () {
                dropdown.style.display = 'block';
            });
            btnContainer.addEventListener('mouseleave', function () {
                let timer;
                clearTimeout(timer);
                timer = setTimeout(() => {
                    dropdown.style.display = 'none';
                }, 300);
            });
        }
    }

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

    selector.init();
    uiManager.init();


})();