Greasy Fork

Greasy Fork is available in English.

123云盘秒传链接

123FastLink是一款适用于123网盘(123Pan)的用户脚本,可以从您选中的文件/文件夹或公开分享链接生成秒传信息,并支持通过秒传信息或导入JSON数据快速转存文件/文件夹到您的网盘。

当前为 2025-05-21 提交的版本,查看 最新版本

// ==UserScript==
// @name         123云盘秒传链接
// @namespace    http://tampermonkey.net/
// @version      1.1.6 // <-- Attempt to fix ETag format errors by padding/lowercasing hex
// @description  123FastLink是一款适用于123网盘(123Pan)的用户脚本,可以从您选中的文件/文件夹或公开分享链接生成秒传信息,并支持通过秒传信息或导入JSON数据快速转存文件/文件夹到您的网盘。
// @author        Gemini
// @match        *://*.123pan.com/*
// @match        *://*.123pan.cn/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=123pan.com
// @license      MIT
// @grant        GM_setClipboard
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // --- Constants and Configuration ---
    const SCRIPT_NAME = "123FastLink";
    const SCRIPT_VERSION = "1.1.58"; // Attempt to fix ETag format errors
    const LEGACY_FOLDER_LINK_PREFIX_V1 = "123FSLinkV1$"; // Old prefix
    const COMMON_PATH_LINK_PREFIX_V1 = "123FLCPV1$"; // Old prefix for common path

    // V2 Prefixes for links with Base62 encoded ETags
    const LEGACY_FOLDER_LINK_PREFIX_V2 = "123FSLinkV2$";
    const COMMON_PATH_LINK_PREFIX_V2 = "123FLCPV2$";

    const COMMON_PATH_DELIMITER = "%"; // Delimiter between common path and file data
    const BASE62_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

    const API_PATHS = {
        UPLOAD_REQUEST: "/b/api/file/upload_request",
        LIST_NEW: "/b/api/file/list/new",
        FILE_INFO: "/b/api/file/info",
        SHARE_LIST: "/b/api/share/get" // <-- New API Path for public shares
    };

    const DOM_SELECTORS = {
        TARGET_BUTTON_AREA: '.ant-dropdown-trigger.sysdiv.parmiryButton',
        FILE_ROW_SELECTOR: ".ant-table-row.ant-table-row-level-0.editable-row",
        FILE_CHECKBOX_SELECTOR: "input[type='checkbox']"
    };

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

    // --- API Helper Functions ---
    const apiHelper = {
        buildURL: (host, path, queryParams = {}) => { const queryString = new URLSearchParams(queryParams).toString(); return `${host}${path}${queryString ? '?' + queryString : ''}`; },
        sendRequest: async function(method, path, queryParams = {}, body = null, isPublicCall = false) { // <-- Added isPublicCall
            const config = {
                host: 'https://' + window.location.host,
                authToken: localStorage['authorToken'],
                loginUuid: localStorage['LoginUuid'],
                appVersion: '3',
                referer: document.location.href,
            };
            const headers = {
                'Content-Type': 'application/json;charset=UTF-8',
                'platform': 'web',
                'App-Version': config.appVersion,
                'Origin': config.host,
                'Referer': config.referer,
            };

            if (!isPublicCall) {
                if (config.authToken) headers['Authorization'] = 'Bearer ' + config.authToken;
                if (config.loginUuid) headers['LoginUuid'] = config.loginUuid;
            } else {
                // For public calls (isPublicCall = true):
                // - Do NOT send Authorization token.
                // - Do NOT send LoginUuid, as public APIs should not depend on user session.
                // if (config.loginUuid) headers['LoginUuid'] = config.loginUuid; // Keep this commented or remove
            }

            try {
                const urlToFetch = this.buildURL(config.host, path, queryParams);
                const response = await fetch(urlToFetch, {
                    method,
                    headers,
                    body: body ? JSON.stringify(body) : null,
                    credentials: 'include'
                });
                const responseText = await response.text();
                let responseData;
                try {
                    responseData = JSON.parse(responseText);
                } catch (e) {
                    if (!response.ok) throw new Error(`❗ HTTP ${response.status}: ${responseText || response.statusText}`);
                    throw new Error(`❗ 响应解析JSON失败: ${e.message}`);
                }

                if (responseData.code !== 0) {
                    const message = responseData.message || 'API业务逻辑错误';
                    const apiError = new Error(`❗ ${message}`);
                    if (typeof message === 'string' && (message.includes("频繁") || message.includes("操作过快") || message.includes("rate limit") || message.includes("too many requests"))) {
                        apiError.isRateLimit = true;
                    }
                    throw apiError;
                }
                return responseData;
            } catch (error) {
                if (!error.isRateLimit && !error.message?.startsWith("UserStopped")) {
                    // Optionally log non-rate-limit, non-user-stopped errors here if needed for debugging
                }
                throw error;
            }
        },
        createFolder: async function(parentId, folderName) { return coreLogic._executeApiWithRetries(() => this._createFolderInternal(parentId, folderName), `创建文件夹: ${folderName}`, coreLogic.currentOperationRateLimitStatus); },
        _createFolderInternal: async function(parentId, folderName) { if (parentId === undefined || parentId === null || isNaN(parseInt(parentId))) { throw new Error(`创建文件夹 "${folderName}" 失败:父文件夹ID无效 (${parentId})。`); } const requestBody = { driveId: 0, etag: "", fileName: folderName, parentFileId: parseInt(parentId, 10), size: 0, type: 1, NotReuse: true, RequestSource: null, duplicate: 1, event: "newCreateFolder", operateType: 1 }; const responseData = await this.sendRequest("POST", API_PATHS.UPLOAD_REQUEST, {}, requestBody); if (responseData?.data?.Info?.FileId !== undefined) return responseData.data.Info; throw new Error('创建文件夹失败或API响应缺少FileId'); },
        listDirectoryContents: async function(parentId, limit = 100) { return coreLogic._executeApiWithRetries(() => this._listDirectoryContentsInternal(parentId, limit), `列出目录ID: ${parentId}`, coreLogic.currentOperationRateLimitStatus); },
        _listDirectoryContentsInternal: async function(parentId, limit = 100) { if (parentId === undefined || parentId === null || isNaN(parseInt(parentId))) { throw new Error(`无效的文件夹ID: ${parentId},无法列出内容。`); } let allItems = []; let nextMarker = "0"; let currentPage = 1; do { const queryParams = { driveId: 0, limit: limit, next: nextMarker, orderBy: "file_name", orderDirection: "asc", parentFileId: parseInt(parentId, 10), trashed: false, SearchData: "", Page: currentPage, OnlyLookAbnormalFile: 0, event: "homeListFile", operateType: 4, inDirectSpace: false }; const responseData = await this.sendRequest("GET", API_PATHS.LIST_NEW, queryParams); if (responseData?.data?.InfoList) { const newItems = responseData.data.InfoList.map(item => ({ FileID: parseInt(item.FileId, 10) || NaN, FileName: item.FileName || "Unknown", Type: parseInt(item.Type, 10) || 0, Size: parseInt(item.Size, 10) || 0, Etag: item.Etag || "", ParentFileID: parseInt(item.ParentFileId, 10) })); allItems = allItems.concat(newItems); nextMarker = responseData.data.Next; currentPage++; } else { nextMarker = "-1"; } } while (nextMarker !== "-1" && nextMarker !== null && nextMarker !== undefined && String(nextMarker).trim() !== ""); return allItems; },
        getFileInfo: async function(idList) { return coreLogic._executeApiWithRetries(() => this._getFileInfoInternal(idList), `获取文件信息: ${idList.join(',')}`, coreLogic.currentOperationRateLimitStatus); },
        _getFileInfoInternal: async function(idList) { if (!idList || idList.length === 0) return { data: { infoList: [] } }; const requestBody = { fileIdList: idList.map(id => ({ fileId: String(id) })) }; const responseData = await this.sendRequest("POST", API_PATHS.FILE_INFO, {}, requestBody); if (responseData?.data?.infoList) { responseData.data.infoList = responseData.data.infoList.map(info => ({ ...info, FileID: parseInt(info.FileId || info.FileID, 10) || NaN, FileName: info.Name || info.FileName || "Unknown", Type: parseInt(info.Type || info.type, 10) || 0, Size: parseInt(info.Size || info.size, 10) || 0, Etag: info.Etag || info.etag || "" })); } return responseData; },
        rapidUpload: async function(etag, size, fileName, parentId) { return coreLogic._executeApiWithRetries(() => this._rapidUploadInternal(etag, size, fileName, parentId), `秒传: ${fileName}`, coreLogic.currentOperationRateLimitStatus); },
        _rapidUploadInternal: async function(etag, size, fileName, parentId) { if (parentId === undefined || parentId === null || isNaN(parseInt(parentId))) { throw new Error(`秒传文件 "${fileName}" 失败:父文件夹ID无效 (${parentId})。`); } const requestBody = { driveId: 0, etag: etag, fileName: fileName, parentFileId: parseInt(parentId, 10), size: parseInt(size, 10), type: 0, NotReuse: false, RequestSource: null, duplicate: 1, event: "rapidUpload", operateType: 1 }; const responseData = await this.sendRequest("POST", API_PATHS.UPLOAD_REQUEST, {}, requestBody); if (responseData?.data?.Info?.FileId !== undefined) return responseData.data.Info; throw new Error(responseData.message || '秒传文件失败或API响应异常'); },

        // --- New API helper for listing shared directory contents ---
        listSharedDirectoryContents: async function(parentId, shareKey, sharePwd, limit = 100) {
            return coreLogic._executeApiWithRetries(
                () => this._listSharedDirectoryContentsInternal(parentId, shareKey, sharePwd, limit),
                `列出分享目录ID: ${parentId} (ShareKey: ${shareKey.substring(0,4)}...)`,
                coreLogic.currentOperationRateLimitStatus,
                true // isPublicCall - This was already here, ensure sendRequest handles it as intended now
            );
        },
        _listSharedDirectoryContentsInternal: async function(parentId, shareKey, sharePwd, limit = 100) {
            if (parentId === undefined || parentId === null || isNaN(parseInt(parentId))) {
                throw new Error(`无效的分享文件夹ID: ${parentId},无法列出内容。`);
            }
            if (!shareKey) throw new Error("ShareKey 不能为空。");

            let allItems = [];
            let nextMarker = "0"; // API for share might use 'next' like user files, or rely solely on 'Page'
            let currentPage = 1;   // Python script uses 'Page'

            do {
                const queryParams = {
                    limit: limit,
                    next: nextMarker, // Send 'next' as it's used in user file listing, API might ignore if not applicable
                    orderBy: "file_name", // Defaulting to filename, asc. Python script used file_id, desc. Name is more user-friendly if API supports.
                    orderDirection: "asc",
                    parentFileId: parseInt(parentId, 10),
                    Page: currentPage,
                    shareKey: shareKey,
                    // SharePwd can be empty string if no password
                };
                if (sharePwd) queryParams.SharePwd = sharePwd;


                // Pass isPublicCall = true to sendRequest
                const responseData = await this.sendRequest("GET", API_PATHS.SHARE_LIST, queryParams, null, true);

                if (responseData?.data?.InfoList) {
                    const newItems = responseData.data.InfoList.map(item => ({
                        FileID: parseInt(item.FileId, 10) || NaN,
                        FileName: item.FileName || "Unknown",
                        Type: parseInt(item.Type, 10) || 0, // 0 for file, 1 for folder
                        Size: parseInt(item.Size, 10) || 0,
                        Etag: item.Etag || "",
                        ParentFileID: parseInt(item.ParentFileId, 10) // This might be relative to share's root or a global ID
                    }));
                    allItems = allItems.concat(newItems);
                    nextMarker = responseData.data.Next; // Capture 'Next' marker if API provides it
                    currentPage++;
                } else {
                    // If InfoList is missing or empty, and it's the first page, it could be an error or empty share.
                    // If not first page, it's end of list.
                    if (currentPage === 1 && !responseData?.data?.InfoList) {
                         // Check for specific error messages if any, otherwise assume empty or access issue.
                         if(responseData.message && responseData.code !== 0) throw new Error(`API错误: ${responseData.message}`);
                         // else, it's likely an empty directory or end of list on first try.
                    }
                    nextMarker = "-1"; // Force stop
                }
            } while (nextMarker !== "-1" && nextMarker !== null && nextMarker !== undefined && String(nextMarker).trim() !== "");
            return allItems;
        },
    };

    // --- Process State & UI Manager ---
    const processStateManager = { _userRequestedStop: false, _modalStopButtonId: 'fl-modal-stop-btn', reset: function() { this._userRequestedStop = false; }, requestStop: function() { this._userRequestedStop = true; const btn = document.getElementById(this._modalStopButtonId); if(btn){btn.textContent = "正在停止..."; btn.disabled = true;} console.log(`[${SCRIPT_NAME}] User requested stop.`); }, isStopRequested: function() { return this._userRequestedStop; }, getStopButtonId: function() { return this._modalStopButtonId; }, updateProgressUI: function(processed, total, successes, failures, currentFileName, extraStatus = "") { const bar = document.querySelector('.fastlink-progress-bar'); if (bar) bar.style.width = `${total > 0 ? Math.round((processed / total) * 100) : 0}%`; const statTxt = document.querySelector('.fastlink-status p:first-child'); if (statTxt) statTxt.textContent = `处理中: ${processed} / ${total} 项 (预估)`; const sucCnt = document.querySelector('.fastlink-stats .success-count'); if (sucCnt) sucCnt.textContent = `✅ 成功:${successes}`; const failCnt = document.querySelector('.fastlink-stats .failed-count'); if (failCnt) failCnt.textContent = `❌ 失败:${failures}`; const curFile = document.querySelector('.fastlink-current-file .file-name'); if (curFile) curFile.textContent = currentFileName ? `📄 ${currentFileName}` : "准备中..."; const extraEl = document.querySelector('.fastlink-status .extra-status-message'); if (extraEl) { extraEl.textContent = extraStatus; extraEl.style.display = extraStatus ? 'block' : 'none';} }, appendLogMessage: function(message, isError = false) { const logArea = document.querySelector('.fastlink-status'); if (logArea) { const p = document.createElement('p'); p.className = isError ? 'error-message' : 'info-message'; p.innerHTML = message; const extraStatusSibling = logArea.querySelector('.extra-status-message'); if (extraStatusSibling) logArea.insertBefore(p, extraStatusSibling.nextSibling); else logArea.appendChild(p); logArea.scrollTop = logArea.scrollHeight; } } };

    // --- Core Logic ---
    const coreLogic = {
        currentOperationRateLimitStatus: { consecutiveRateLimitFailures: 0 },
        _executeApiWithRetries: async function(apiFunctionExecutor, itemNameForLog, rateLimitStatusRef, isPublicCallForSendRequest = false) { // Added isPublicCallForSendRequest
            let generalErrorRetries = 0;
            while (generalErrorRetries <= RETRY_AND_DELAY_CONFIG.GENERAL_API_MAX_RETRIES) {
                if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                let rateLimitRetriesForCurrentGeneralAttempt = 0;
                while (rateLimitRetriesForCurrentGeneralAttempt <= RETRY_AND_DELAY_CONFIG.RATE_LIMIT_MAX_ITEM_RETRIES) {
                    if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                    try {
                        // Pass isPublicCallForSendRequest to the actual API executor if it's a direct call to one of the apiHelper methods that now accepts it.
                        // The new apiHelper.listSharedDirectoryContents passes `true` directly to its internal `sendRequest` call.
                        // Other calls like apiHelper.getFileInfo will use the default `false` for `isPublicCall` in `sendRequest`.
                        const result = await apiFunctionExecutor();
                        rateLimitStatusRef.consecutiveRateLimitFailures = 0;
                        return result;
                    } catch (error) {
                        if (processStateManager.isStopRequested()) throw error;
                        if (error.isRateLimit) {
                            rateLimitStatusRef.consecutiveRateLimitFailures++;
                            const rlRetryAttemptDisplay = rateLimitRetriesForCurrentGeneralAttempt + 1;
                            const currentFileEl = document.querySelector('.fastlink-current-file .file-name');
                            if(currentFileEl) processStateManager.appendLogMessage(`⏳ ${currentFileEl.textContent}: 操作频繁 (RL ${rlRetryAttemptDisplay}/${RETRY_AND_DELAY_CONFIG.RATE_LIMIT_MAX_ITEM_RETRIES + 1})`, true);
                            if (rateLimitRetriesForCurrentGeneralAttempt >= RETRY_AND_DELAY_CONFIG.RATE_LIMIT_MAX_ITEM_RETRIES) {
                                processStateManager.appendLogMessage(`❌ ${itemNameForLog}: 已达当前常规尝试的最大API限流重试次数。`, true);
                                throw error;
                            }
                            rateLimitRetriesForCurrentGeneralAttempt++;
                            if (rateLimitStatusRef.consecutiveRateLimitFailures >= RETRY_AND_DELAY_CONFIG.RATE_LIMIT_GLOBAL_PAUSE_TRIGGER_FAILURES) {
                                processStateManager.appendLogMessage(`[全局暂停] API持续频繁,暂停 ${RETRY_AND_DELAY_CONFIG.RATE_LIMIT_GLOBAL_PAUSE_DURATION_MS / 1000} 秒...`, true);
                                const extraStatusEl = document.querySelector('.fastlink-status .extra-status-message');
                                if(extraStatusEl) extraStatusEl.textContent = `全局暂停中... ${RETRY_AND_DELAY_CONFIG.RATE_LIMIT_GLOBAL_PAUSE_DURATION_MS / 1000}s`;
                                await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.RATE_LIMIT_GLOBAL_PAUSE_DURATION_MS));
                                if(extraStatusEl) extraStatusEl.textContent = "";
                                rateLimitStatusRef.consecutiveRateLimitFailures = 0;
                                rateLimitRetriesForCurrentGeneralAttempt = 0;
                            } else {
                                await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.RATE_LIMIT_ITEM_RETRY_DELAY_MS));
                            }
                        } else {
                            const genRetryAttemptDisplay = generalErrorRetries + 1;
                            processStateManager.appendLogMessage(`❌ ${itemNameForLog}: ${error.message} (常规重试 ${genRetryAttemptDisplay}/${RETRY_AND_DELAY_CONFIG.GENERAL_API_MAX_RETRIES + 1})`, true);
                            generalErrorRetries++;
                            if (generalErrorRetries > RETRY_AND_DELAY_CONFIG.GENERAL_API_MAX_RETRIES) {
                                throw error;
                            }
                            await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.GENERAL_API_RETRY_DELAY_MS));
                            break;
                        }
                    }
                }
            }
            throw new Error(`[${SCRIPT_NAME}] 所有API重试均失败: ${itemNameForLog}`);
        },
        getSelectedFileIds: () => {
            return Array.from(document.querySelectorAll(DOM_SELECTORS.FILE_ROW_SELECTOR))
                .filter(row => (row.querySelector(DOM_SELECTORS.FILE_CHECKBOX_SELECTOR) || {}).checked)
                .map(row => String(row.getAttribute('data-row-key')))
                .filter(id => id != null);
        },

        // --- MODIFICATION START: Improved getCurrentDirectoryId ---
        getCurrentDirectoryId: () => {
            const url = window.location.href;
            const homeFilePathMatch = url.match(/[?&]homeFilePath=([^&]*)/);

            if (homeFilePathMatch) {
                let filePathIds = homeFilePathMatch[1];
                if (filePathIds && filePathIds !== "") {
                    if (filePathIds.includes(',')) {
                        const idsArray = filePathIds.split(',');
                        return idsArray[idsArray.length - 1]; // Return the last ID for nested folders
                    } else {
                        return filePathIds; // Single ID
                    }
                } else {
                    return "0"; // homeFilePath is present but empty, signifies root
                }
            }

            // Regex to capture various ID patterns. Order implies priority.
            const regexes = [
                /fid=(\d+)/, // 1. fid=ID (query parameter)
                /#\/list\/folder\/(\d+)/, // 2. #/list/folder/ID (hash navigation)
                /\/drive\/(?:folder\/)?(\d+)/, // 3. /drive/[folder/]ID (drive paths)
                /\/s\/[a-zA-Z0-9_-]+\/(\d+)/, // 4. /s/SHARE_ID/FOLDER_ID (share view)
                /(?:\/|^)(\d+)(?=[\/?#]|$)/ // 5. /FOLDER_ID or just FOLDER_ID if it's the whole path part
                                          // Matches /123, /123?, /123#, or 123 at path end.
                                          // Ensures it's a full segment.
            ];

            for (const regex of regexes) {
                const match = url.match(regex);
                if (match && match[1]) {
                    // For /drive/0 (or other patterns that might resolve to ID "0"),
                    // treat as root unless it's a specific non-root context.
                    if (match[1] === "0") {
                        // If current URL is literally like ".../drive/0" or ".../s/blah/0", it's specific.
                        // But if it's just a generic ID '0' from a broad regex match, prefer root fallback.
                        // The regex /\/drive\/(?:folder\/)?(\d+)/ specifically can match /drive/0.
                        if (regex.source === String(/\/drive\/(?:folder\/)?(\d+)/) && url.includes("/drive/0")) {
                             return "0"; // Explicit /drive/0 is root
                        }
                        // For other regexes, if '0' is matched, it *could* be a folder named "0".
                        // However, "0" is conventionally the root ID.
                        // We'll let it pass here, but the root checks later are a safeguard.
                        // If a folder is genuinely named '0', this should capture it.
                        // If it's a misinterpretation of a root URL as ID '0', the default is also '0'.
                        // This is fine.
                    }
                    return match[1];
                }
            }

            // Fallback to "0" for root or unrecognized structures
            const lowerUrl = url.toLowerCase();
            if (lowerUrl.includes("/drive/0") ||
                lowerUrl.endsWith("/drive") || lowerUrl.endsWith("/drive/") ||
                lowerUrl.match(/^https?:\/\/[^\/]+\/?([#?].*)?$/) || // domain.com or domain.com/
                lowerUrl.endsWith(".123pan.com") || lowerUrl.endsWith(".123pan.cn") ||
                lowerUrl.endsWith(".123pan.com/") || lowerUrl.endsWith(".123pan.cn/")
            ) {
                return "0";
            }

            try {
                const pathname = new URL(url).pathname;
                if (pathname === '/' || pathname.toLowerCase() === '/drive/' || pathname.toLowerCase() === '/index.html') {
                    return "0";
                }
            } catch(e) { /*ignore invalid URL for pathname */ }

            // console.warn(`[${SCRIPT_NAME}] Directory ID not reliably determined for ${url}, defaulting to root (0). Pathname: ${new URL(url).pathname}`);
            return "0";
        },
        // --- MODIFICATION END: Improved getCurrentDirectoryId ---

        // --- MODIFICATION START: Link Shortening Logic ---
        _findLongestCommonPrefix: function(paths) {
            if (!paths || paths.length === 0) return "";
            if (paths.length === 1 && paths[0].includes('/')) { // Single item in a folder path
                 const lastSlash = paths[0].lastIndexOf('/');
                 if (lastSlash > -1) return paths[0].substring(0, lastSlash + 1);
                 return ""; // Single file at root of selection
            }
            if (paths.length === 1 && !paths[0].includes('/')) return ""; // Single file, no path

            const sortedPaths = [...paths].sort();
            const firstPath = sortedPaths[0];
            const lastPath = sortedPaths[sortedPaths.length - 1];
            let i = 0;
            while (i < firstPath.length && firstPath.charAt(i) === lastPath.charAt(i)) {
                i++;
            }
            let prefix = firstPath.substring(0, i);
            // Ensure prefix ends with a slash if it's a directory prefix
            if (prefix.includes('/')) {
                prefix = prefix.substring(0, prefix.lastIndexOf('/') + 1);
            } else {
                // If no slash, it means the common part is a file/folder name itself.
                // This is only a valid "common prefix" if all paths either are this prefix
                // or start with this prefix + "/"
                if (!paths.every(p => p === prefix || p.startsWith(prefix + "/"))) {
                    return ""; // Not a valid common directory/file prefix
                }
                // If it is valid, and it's a folder-like prefix, add a slash if needed for consistency
                // Example: selected "FolderA", files are "File1", "File2" -> paths "FolderA/File1", "FolderA/File2"
                // Common prefix would be "FolderA/".
                // If selected "FileA", "FileB" -> paths "FileA", "FileB", common prefix ""
                // If a single folder "MyFolder" is selected, paths are like "MyFolder/file1.txt"
                // Common prefix will be "MyFolder/"
            }
            // Only return prefix if it's of meaningful length (e.g., more than just "/")
            // and actually reduces path length for most items.
            return (prefix.length > 1 && prefix.endsWith('/')) ? prefix : "";
        },
        // --- MODIFICATION END: Link Shortening Logic ---

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

            processStateManager.reset();
            this.currentOperationRateLimitStatus.consecutiveRateLimitFailures = 0;
            let allFileEntriesData = []; // Store as {etag, size, fullPath} objects
            let processedAnyFolder = false;

            let totalDiscoveredItemsForProgress = selectedItemIds.length;
            let itemsProcessedForProgress = 0;
            let successes = 0, failures = 0;
            let jsonDataForExport = null; // To store data for JSON export

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

            async function processSingleItem(itemId, currentRelativePath) {
                if (processStateManager.isStopRequested()) throw new Error("UserStopped");

                const baseItemName = `${currentRelativePath || '根目录'}/${itemId}`;
                processStateManager.updateProgressUI(itemsProcessedForProgress, totalDiscoveredItemsForProgress, successes, failures, baseItemName, "获取信息...");

                let itemDetails;
                try {
                    const itemInfoResponse = await apiHelper.getFileInfo([String(itemId)]);
                    if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                    if (!itemInfoResponse?.data?.infoList?.length) throw new Error(`项目 ${itemId} 信息未找到`);
                    itemDetails = itemInfoResponse.data.infoList[0];
                    itemsProcessedForProgress++;
                } catch (e) {
                    if (processStateManager.isStopRequested()) throw e;
                    failures++;
                    itemsProcessedForProgress++;
                    processStateManager.appendLogMessage(`❌ 获取项目 "${baseItemName}" 详情最终失败: ${e.message}`, true);
                    processStateManager.updateProgressUI(itemsProcessedForProgress, totalDiscoveredItemsForProgress, successes, failures, baseItemName, "获取信息失败");
                    return;
                }

                if (isNaN(itemDetails.FileID)) {
                    failures++;
                    processStateManager.appendLogMessage(`❌ 项目 "${itemDetails.FileName || itemId}" FileID无效`, true);
                    processStateManager.updateProgressUI(itemsProcessedForProgress, totalDiscoveredItemsForProgress, successes, failures, baseItemName);
                    return;
                }
                const cleanName = (itemDetails.FileName || "Unknown").replace(/[#$%\/]/g, "_").replace(new RegExp(COMMON_PATH_DELIMITER.replace(/[.*+?^${}()|[\\\]\\\\]/g, '\\\\$&'), 'g'), '_');

                const itemDisplayPath = `${currentRelativePath ? currentRelativePath + '/' : ''}${cleanName}`;
                processStateManager.updateProgressUI(itemsProcessedForProgress, totalDiscoveredItemsForProgress, successes, failures, itemDisplayPath);

                if (itemDetails.Type === 0) { // File
                    if (itemDetails.Etag && itemDetails.Size !== undefined) {
                        allFileEntriesData.push({ etag: itemDetails.Etag, size: itemDetails.Size, fullPath: itemDisplayPath });
                        successes++;
                        processStateManager.appendLogMessage(`✔️ 文件: ${itemDisplayPath}`);
                    } else {
                        failures++;
                        processStateManager.appendLogMessage(`❌ 文件 "${itemDisplayPath}" 缺少Etag或大小`, true);
                    }
                } else if (itemDetails.Type === 1) { // Folder
                    processedAnyFolder = true;
                    processStateManager.appendLogMessage(`📁 扫描文件夹: ${itemDisplayPath}`);
                    processStateManager.updateProgressUI(itemsProcessedForProgress, totalDiscoveredItemsForProgress, successes, failures, itemDisplayPath, "列出内容...");
                    try {
                        const contents = await apiHelper.listDirectoryContents(itemDetails.FileID);
                        if (processStateManager.isStopRequested()) throw new Error("UserStopped");

                        totalDiscoveredItemsForProgress += contents.length;

                        for (const contentItem of contents) {
                            if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                            if (isNaN(contentItem.FileID)) {
                                itemsProcessedForProgress++;
                                failures++;
                                processStateManager.appendLogMessage(`❌ 文件夹 "${itemDisplayPath}" 内发现无效项目ID`, true);
                                continue;
                            }
                            await processSingleItem(contentItem.FileID, itemDisplayPath);
                            await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS / 2));
                        }
                    } catch (e) {
                        if (processStateManager.isStopRequested()) throw e;
                        processStateManager.appendLogMessage(`❌ 处理文件夹 "${itemDisplayPath}" 内容最终失败: ${e.message}`, true);
                        processStateManager.updateProgressUI(itemsProcessedForProgress, totalDiscoveredItemsForProgress, successes, failures, itemDisplayPath, "列出内容失败");
                    }
                }
                await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS));
            }

            try {
                processStateManager.updateProgressUI(itemsProcessedForProgress, totalDiscoveredItemsForProgress, successes, failures, "准备开始...");
                for (let i = 0; i < selectedItemIds.length; i++) {
                    if (processStateManager.isStopRequested()) break;
                    await processSingleItem(selectedItemIds[i], "");
                }
            } catch (e) {
                if (e.message === "UserStopped") processStateManager.appendLogMessage("🛑 用户已停止操作。", true);
                else { processStateManager.appendLogMessage(`SYSTEM ERROR: ${e.message}`, true); console.error("Error during generation:", e); }
            }

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

            const totalTime = Math.round((Date.now() - startTime) / 1000);
            let summary;
            if (processStateManager.isStopRequested()) summary = `<div class="fastlink-result"><h3>🔴 操作已停止</h3><p>部分项目可能未处理</p><p>⏱️ 耗时: ${totalTime} 秒</p></div>`;
            else if (!allFileEntriesData.length && failures > 0) summary = `<div class="fastlink-result"><h3>😢 生成失败</h3><p>未能提取有效文件信息 (${successes} 成功, ${failures} 失败)</p><p>⏱️ 耗时: ${totalTime} 秒</p></div>`;
            else if (!allFileEntriesData.length) summary = `<div class="fastlink-result"><h3>🤔 无文件</h3><p>未选中文件或文件夹为空</p></div>`;
            else {
                let link = "";
                const allPaths = allFileEntriesData.map(entry => entry.fullPath);
                const commonPrefix = this._findLongestCommonPrefix(allPaths);

                let useV2Format = true;
                const processedEntries = allFileEntriesData.map(entry => {
                    const etagConversion = hexToOptimizedEtag(entry.etag);
                    if (!etagConversion.useV2) {
                        useV2Format = false; // If any ETag cannot be optimized for V2, fallback whole link to V1
                    }
                    return {
                        ...entry,
                        processedEtag: etagConversion.useV2 ? etagConversion.optimized : entry.etag // Store the one to be used
                    };
                });
                // If any etag failed V2, allFileEntriesData should use original etags for V1 link.
                // The check for useV2Format will handle this.

                if (commonPrefix && (processedAnyFolder || allPaths.some(p => p.includes('/')))) {
                    const fileStrings = processedEntries.map(entry =>
                        `${useV2Format ? entry.processedEtag : entry.etag}#${entry.size}#${entry.fullPath.substring(commonPrefix.length)}`
                    );
                    link = (useV2Format ? COMMON_PATH_LINK_PREFIX_V2 : COMMON_PATH_LINK_PREFIX_V1) + commonPrefix + COMMON_PATH_DELIMITER + fileStrings.join('$');
                } else {
                    // Use old format (or V2 file-only if applicable)
                    const fileStrings = processedEntries.map(entry =>
                        `${useV2Format ? entry.processedEtag : entry.etag}#${entry.size}#${entry.fullPath}`
                    );
                    link = fileStrings.join('$');
                    // If it's a folder structure (even single folder) or contains paths, use folder prefixes
                    if (processedAnyFolder || allPaths.some(p => p.includes('/'))) {
                         link = (useV2Format ? LEGACY_FOLDER_LINK_PREFIX_V2 : LEGACY_FOLDER_LINK_PREFIX_V1) + link;
                    } else if (useV2Format) {
                        // This case implies individual files, no common prefix, no folders.
                        // To signify V2 ETags, we might need a simple V2 file prefix if not covered by LEGACY_FOLDER_LINK_PREFIX_V2.
                        // For now, LEGACY_FOLDER_LINK_PREFIX_V2 implicitly covers this if only files are selected and processedAnyFolder is false.
                        // Let's be explicit: if NOT a folder structure, and V2, use a distinct file-only V2 prefix or ensure LEGACY_FOLDER_LINK_PREFIX_V2 is okay.
                        // The current structure with processedAnyFolder should correctly apply LEGACY_FOLDER_LINK_PREFIX_V2/V1.
                        // If !processedAnyFolder AND !allPaths.some(p => p.includes('/')), it's just files.
                        // The current logic: it just joins them. It needs a prefix if V2.
                        // To keep it simple: if V2 is active, even for non-folder, non-common-path files, we'll use LEGACY_FOLDER_LINK_PREFIX_V2.
                        // This simplifies parsing logic: any V2 prefix means Base62 ETags.
                        if(!link.startsWith(LEGACY_FOLDER_LINK_PREFIX_V2) && !link.startsWith(COMMON_PATH_LINK_PREFIX_V2) && useV2Format){
                           // If it's just files and V2, and no other prefix was added, use the V2 legacy one.
                           // This might occur if commonPrefix is empty AND processedAnyFolder is false.
                           link = LEGACY_FOLDER_LINK_PREFIX_V2 + link;
                        }
                    }
                }

                let exportJsonDataToUse = jsonDataForExport; // Use pre-calculated if available
                if (!exportJsonDataToUse) { // If called directly or jsonDataForExport wasn't populated
                     const commonPathForCurrentExport = (commonPrefix && (processedAnyFolder || allPaths.some(p => p.includes('/')))) ? commonPrefix : "";
                     exportJsonDataToUse = {
                        scriptVersion: SCRIPT_VERSION,
                        exportVersion: "1.0",
                        usesBase62EtagsInExport: useV2Format,
                        commonPath: commonPathForCurrentExport,
                        files: allFileEntriesData.map(entry => {
                            const etagInLink = useV2Format ? hexToOptimizedEtag(entry.etag).optimized : entry.etag;
                            const relativePath = commonPathForCurrentExport ? entry.fullPath.substring(commonPathForCurrentExport.length) : entry.fullPath;
                            return { path: relativePath, size: String(entry.size), etag: etagInLink };
                        })
                    };
                }


                if (useV2Format) processStateManager.appendLogMessage('💡 使用V2链接格式 (Base62 ETags) 生成。');
                else processStateManager.appendLogMessage('ℹ️ 使用V1链接格式 (标准 ETags) 生成。');

                const totalSize = allFileEntriesData.reduce((acc, entry) => acc + Number(entry.size), 0);
                const formattedTotalSize = formatBytes(totalSize);
                summary = `<div class="fastlink-result"><h3>🎉 生成成功</h3><p>✅ 文件数量: ${successes} 个 (${failures > 0 ? '❌ ' : ''}项目处理失败 ${failures})</p><p>💾 总大小: ${formattedTotalSize}</p><p>⏱️ 耗时: ${totalTime} 秒</p><textarea class="fastlink-link-text" readonly>${link}</textarea></div>`;
                uiManager.showModal("🎉 秒传链接已生成", summary, 'showLink', true, link, exportJsonDataToUse); return link;
            }
            uiManager.updateModalContent(summary); uiManager.enableModalCloseButton(true); return "";
        },

        parseShareLink: (shareLink) => {
            let commonBasePath = "";
            let isCommonPathFormat = false;
            let isV2EtagFormat = false; // Flag to indicate Base62 ETags

            // Check for V2 common path first
            if (shareLink.startsWith(COMMON_PATH_LINK_PREFIX_V2)) {
                isCommonPathFormat = true;
                isV2EtagFormat = true;
                shareLink = shareLink.substring(COMMON_PATH_LINK_PREFIX_V2.length);
            } else if (shareLink.startsWith(COMMON_PATH_LINK_PREFIX_V1)) {
                isCommonPathFormat = true;
                // isV2EtagFormat remains false
                shareLink = shareLink.substring(COMMON_PATH_LINK_PREFIX_V1.length);
            }

            if (isCommonPathFormat) {
                const delimiterPos = shareLink.indexOf(COMMON_PATH_DELIMITER);
                if (delimiterPos > -1) {
                    commonBasePath = shareLink.substring(0, delimiterPos);
                    shareLink = shareLink.substring(delimiterPos + 1);
                } else { // Malformed link
                    console.error("Malformed common path link: delimiter not found after prefix.");
                    isCommonPathFormat = false; // Revert, treat as non-common path or fail
                    // Attempt to re-evaluate original shareLink for legacy prefixes
                    // This part needs to be careful not to re-parse if already stripped.
                    // For simplicity, if delimiter is missing, it's an error for common path.
                    // Let's assume for now the link is invalid if common path prefix is there but no delimiter.
                }
            } else { // Not a common path format, check legacy V2 then V1
                if (shareLink.startsWith(LEGACY_FOLDER_LINK_PREFIX_V2)) {
                    isV2EtagFormat = true;
                    shareLink = shareLink.substring(LEGACY_FOLDER_LINK_PREFIX_V2.length);
                } else if (shareLink.startsWith(LEGACY_FOLDER_LINK_PREFIX_V1)) {
                    // isV2EtagFormat remains false
                    shareLink = shareLink.substring(LEGACY_FOLDER_LINK_PREFIX_V1.length);
                }
                // If no prefix is matched at all, isV2EtagFormat remains false (plain files, V1 ETags)
            }

            return shareLink.split('$').map(sLink => {
                const parts = sLink.split('#');
                if (parts.length >= 3) {
                    let etag = parts[0];
                    try {
                        etag = optimizedEtagToHex(parts[0], isV2EtagFormat);
                    } catch (e) {
                        console.error(`[${SCRIPT_NAME}] Error decoding ETag for V2 link: ${parts[0]}, ${e.message}`);
                        return null; // Skip this file if ETag decoding fails for V2
                    }

                    let filePath = parts.slice(2).join('#');
                    if (isCommonPathFormat && commonBasePath) { // Ensure commonBasePath is not empty
                        filePath = commonBasePath + filePath;
                    }
                    return { etag: etag, size: parts[1], fileName: filePath };
                }
                return null;
            }).filter(i => i);
        },

        transferFromShareLink: async function(shareLink) {
            if (!shareLink?.trim()) { uiManager.showAlert("链接为空"); return; }
            const filesToProcess = this.parseShareLink(shareLink); // This now handles V1/V2 ETags
            if (!filesToProcess.length) { uiManager.showAlert("无法解析链接或链接中无有效文件信息"); return; }

            // Determine if it's likely a folder structure for UI/logging hints
            const isFolderStructureHint = shareLink.startsWith(LEGACY_FOLDER_LINK_PREFIX_V1) ||
                                       shareLink.startsWith(COMMON_PATH_LINK_PREFIX_V1) ||
                                       shareLink.startsWith(LEGACY_FOLDER_LINK_PREFIX_V2) ||
                                       shareLink.startsWith(COMMON_PATH_LINK_PREFIX_V2) ||
                                       filesToProcess.some(f => f.fileName.includes('/'));

            await this._executeActualFileTransfer(filesToProcess, isFolderStructureHint, "链接转存");
        },

        transferImportedJsonData: async function(jsonData) {
            if (!jsonData || typeof jsonData !== 'object') {
                uiManager.showAlert("JSON数据无效"); return;
            }
            const { scriptVersion, exportVersion, usesBase62EtagsInExport, commonPath, files } = jsonData;

            // Basic validation
            if (!files || !Array.isArray(files) || files.length === 0) {
                uiManager.showAlert("JSON文件中没有有效的文件条目。"); return;
            }
            if (typeof usesBase62EtagsInExport !== 'boolean') {
                // Attempt to infer if not present for older exports, assume true if etags look like base62
                // For now, strict check. If problematic, can add inference logic.
                processStateManager.appendLogMessage("[警告] JSON文件缺少或无效的 'usesBase62EtagsInExport' 标志。将尝试根据ETag格式推断。", true);
                 // Do not return yet, let it try with a default or user can cancel if things look wrong.
            }

            processStateManager.appendLogMessage(`[导入] JSON包含 ${files.length} 个条目。公共路径: '${commonPath || "(无)"}', Base62 ETags (声明): ${usesBase62EtagsInExport === undefined ? '未声明' : usesBase62EtagsInExport}`);

            let preprocessingFailedItems = [];
            const filesToProcess = files.map(fileFromJson => {
                if (!fileFromJson || typeof fileFromJson.path !== 'string' || !fileFromJson.size || !fileFromJson.etag) {
                    const errorMsg = "条目无效 (缺少 path, size, or etag)";
                    console.warn(`[${SCRIPT_NAME}] 跳过无效的文件条目:`, fileFromJson, errorMsg);
                    // Ensure originalEntry is captured even if fileFromJson itself is null/undefined partially
                    const entryForLog = fileFromJson || {};
                    preprocessingFailedItems.push({ fileName: entryForLog.path || "未知文件(数据缺失)", error: errorMsg, originalEntry: entryForLog });
                    return null;
                }
                let finalEtag;
                try {
                    // If usesBase62EtagsInExport is undefined, infer based on typical Base62 ETag patterns (alphanumeric, shorter than 32 hex chars for MD5)
                    // This is a heuristic. True V1 ETags are hex (0-9, a-f, A-F) and typically 32 chars for MD5.
                    let attemptDecode = usesBase62EtagsInExport;
                    if (usesBase62EtagsInExport === undefined) {
                        const isLikelyHex = /^[0-9a-fA-F]+$/.test(fileFromJson.etag);
                        if (isLikelyHex && fileFromJson.etag.length === 32) attemptDecode = false; // Likely a V1 hex ETag
                        else if (!isLikelyHex || fileFromJson.etag.length < 32) attemptDecode = true; // Likely V2 or malformed, try decoding
                        else attemptDecode = false; // Default to false if unsure and long hex
                        processStateManager.appendLogMessage(`[导入推断] 文件 '${fileFromJson.path.substring(0,30)}...' ETag '${fileFromJson.etag.substring(0,10)}...', usesBase62EtagsInExport未声明,推断为: ${attemptDecode}`);
                    }
                    finalEtag = attemptDecode ? optimizedEtagToHex(fileFromJson.etag, true) : fileFromJson.etag;
                } catch (e) {
                    const errorMsg = `ETag解码失败 (${fileFromJson.etag}): ${e.message}`;
                    processStateManager.appendLogMessage(`❌ ${errorMsg} 文件: ${fileFromJson.path}`, true);
                    preprocessingFailedItems.push({ fileName: fileFromJson.path, error: errorMsg, originalEntry: fileFromJson });
                    return null;
                }

                const fullFileName = commonPath ? commonPath + fileFromJson.path : fileFromJson.path;
                return { etag: finalEtag, size: String(fileFromJson.size), fileName: fullFileName, originalEntry: fileFromJson };
            }).filter(f => f !== null);

            if (preprocessingFailedItems.length > 0) {
                processStateManager.appendLogMessage(`[导入注意] ${preprocessingFailedItems.length} 个条目在预处理阶段失败,将不会被尝试转存。详情见操作结束后的问题列表。`, true);
            }

            if (!filesToProcess.length && preprocessingFailedItems.length > 0) {
                // All items failed in preprocessing
                uiManager.showModal("⚠️ JSON导入预处理失败",
                    `所有 ${preprocessingFailedItems.length} 个文件条目在导入预处理阶段即发生错误,无法继续转存。<br>
                     <div id="fastlink-permanent-failures-log" style="display: block; margin-top: 10px; text-align: left; max-height: 150px; overflow-y: auto; border: 1px solid #ddd; padding: 5px; font-size: 0.85em;">
                         <h4>预处理失败项目:</h4>
                         <div id="fastlink-failures-list">
                            ${preprocessingFailedItems.map(pf => `<p style="margin:2px 0;">📄 <span style="font-weight:bold;">${pf.fileName}</span>: <span style="color:red;">${pf.error}</span></p>`).join('')}
                         </div>
                     </div>`,
                    'info_with_buttons', true, null, null, preprocessingFailedItems); // Pass failures for potential copy log button
                return;
            } else if (!filesToProcess.length) {
                 uiManager.showAlert("JSON文件中解析后无有效文件可转存(所有条目均无效或解码失败)。");
                 return;
            }

            const isFolderStructureHint = !!commonPath || filesToProcess.some(f => f.fileName.includes('/'));
            await this._executeActualFileTransfer(filesToProcess, isFolderStructureHint, "文件导入", preprocessingFailedItems);
        },

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

            processStateManager.reset();
            this.currentOperationRateLimitStatus.consecutiveRateLimitFailures = 0;
            let allFileEntriesData = []; // Store as {etag, size, fullPath} objects
            let processedAnyFolder = false; // Will always be true if we process subfolders

            let totalDiscoveredItemsForProgress = 1; // Start with 1 for the initial root scan
            let itemsProcessedForProgress = 0;
            let successes = 0, failures = 0;
            let jsonDataForExport = null;

            uiManager.showModal("从公开分享生成", `
                <div class="fastlink-progress-container"><div class="fastlink-progress-bar" style="width: 0%"></div></div>
                <div class="fastlink-status">
                    <p>🔍 正在分析分享链接 (Key: ${shareKey.substring(0,8)}...)...</p>
                    <p class="extra-status-message" style="color: #ff7f50; display: none;"></p>
                </div>
                <div class="fastlink-stats"><span class="success-count">✅ 成功:0</span><span class="failed-count">❌ 失败:0</span></div>
                <div class="fastlink-current-file"><p class="file-name">准备开始...</p></div>`, 'progress_stoppable', false);
            const startTime = Date.now();

            async function _fetchSharedItemsRecursive(currentSharedParentId, currentRelativePath) {
                if (processStateManager.isStopRequested()) throw new Error("UserStopped");

                const baseItemName = `${currentRelativePath || '分享根目录'}/ID:${currentSharedParentId}`;
                processStateManager.updateProgressUI(itemsProcessedForProgress, totalDiscoveredItemsForProgress, successes, failures, baseItemName, "获取分享内容...");
                itemsProcessedForProgress++; // Count this folder processing attempt

                let contents;
                try {
                    // Note: listSharedDirectoryContents already uses _executeApiWithRetries
                    contents = await apiHelper.listSharedDirectoryContents(currentSharedParentId, shareKey, sharePwd);
                    if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                } catch (e) {
                    if (processStateManager.isStopRequested()) throw e;
                    failures++;
                    processStateManager.appendLogMessage(`❌ 获取分享目录 "${baseItemName}" 内容失败: ${e.message}`, true);
                    processStateManager.updateProgressUI(itemsProcessedForProgress, totalDiscoveredItemsForProgress, successes, failures, baseItemName, "获取分享内容失败");
                    return;
                }

                totalDiscoveredItemsForProgress += contents.length -1; // Adjust total: -1 because current folder was already counted. Add new items.
                                                                      // Simpler: totalDiscoveredItemsForProgress = successes + failures + items_in_queue_for_folders

                for (const item of contents) {
                    if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                    if (isNaN(item.FileID)) {
                        failures++;
                        processStateManager.appendLogMessage(`❌ 分享内发现无效项目ID: ${item.FileName}`, true);
                        // itemsProcessedForProgress++; // Don't double count if it was part of a folder that failed
                        continue;
                    }

                    const cleanName = (item.FileName || "Unknown").replace(/[#$%\/]/g, "_").replace(new RegExp(COMMON_PATH_DELIMITER.replace(/[.*+?^${}()|[\\\]\\\\]/g, '\\\\$&'), 'g'), '_');
                    const itemDisplayPath = `${currentRelativePath ? currentRelativePath + '/' : ''}${cleanName}`;
                    processStateManager.updateProgressUI(itemsProcessedForProgress, totalDiscoveredItemsForProgress, successes, failures, itemDisplayPath);


                    if (item.Type === 0) { // File
                        if (item.Etag && item.Size !== undefined) {
                            allFileEntriesData.push({ etag: item.Etag, size: item.Size, fullPath: itemDisplayPath });
                            successes++;
                            processStateManager.appendLogMessage(`✔️ 文件 (分享): ${itemDisplayPath}`);
                        } else {
                            failures++;
                            processStateManager.appendLogMessage(`❌ 分享文件 "${itemDisplayPath}" 缺少Etag或大小`, true);
                        }
                        itemsProcessedForProgress++; // Count file processing
                    } else if (item.Type === 1) { // Folder
                        processedAnyFolder = true;
                        processStateManager.appendLogMessage(`📁 扫描分享文件夹: ${itemDisplayPath}`);
                        // itemsProcessedForProgress++; // Folder itself is "processed" when its contents start fetching.
                                                     // The itemsProcessedForProgress in updateProgressUI is more like "tasks started"
                        await _fetchSharedItemsRecursive(item.FileID, itemDisplayPath);
                    }
                    await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS / 2)); // Proactive delay between items
                }
                await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS)); // Proactive delay after folder
            }

            try {
                processStateManager.updateProgressUI(0, 1, 0, 0, "准备开始从分享链接生成...");
                await _fetchSharedItemsRecursive(startParentFileId, "");
            } catch (e) {
                if (e.message === "UserStopped") processStateManager.appendLogMessage("🛑 用户已停止操作。", true);
                else { processStateManager.appendLogMessage(`SYSTEM ERROR during public share processing: ${e.message}`, true); console.error("Error during public share generation:", e); }
            }

            // Final progress update to reflect all items processed
            processStateManager.updateProgressUI(Math.max(itemsProcessedForProgress, successes + failures), totalDiscoveredItemsForProgress, successes, failures, "处理完成", "");


            const totalTime = Math.round((Date.now() - startTime) / 1000);
            let summary;
            if (processStateManager.isStopRequested()) summary = `<div class="fastlink-result"><h3>🔴 操作已停止</h3><p>部分项目可能未处理</p><p>⏱️ 耗时: ${totalTime} 秒</p></div>`;
            else if (!allFileEntriesData.length && failures > 0) summary = `<div class="fastlink-result"><h3>😢 生成失败</h3><p>未能从分享链接提取有效文件信息 (${successes} 成功, ${failures} 失败)</p><p>⏱️ 耗时: ${totalTime} 秒</p></div>`;
            else if (!allFileEntriesData.length) summary = `<div class="fastlink-result"><h3>🤔 无文件</h3><p>分享链接中未找到文件或文件夹为空</p></div>`;
            else {
                // Re-use existing link generation logic
                // This part is duplicated from generateShareLink - consider refactoring into a common function if more similarities arise.
                let link = "";
                const allPaths = allFileEntriesData.map(entry => entry.fullPath);
                const commonPrefix = this._findLongestCommonPrefix(allPaths);

                let useV2Format = true;
                const processedEntries = allFileEntriesData.map(entry => {
                    const etagConversion = hexToOptimizedEtag(entry.etag);
                    if (!etagConversion.useV2) useV2Format = false;
                    return { ...entry, processedEtag: etagConversion.useV2 ? etagConversion.optimized : entry.etag };
                });

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

                const commonPathForExport = (commonPrefix && (processedAnyFolder || allPaths.some(p => p.includes('/')))) ? commonPrefix : "";
                jsonDataForExport = {
                    scriptVersion: SCRIPT_VERSION,
                    exportVersion: "1.0",
                    usesBase62EtagsInExport: useV2Format,
                    commonPath: commonPathForExport,
                    files: allFileEntriesData.map(entry => {
                        const etagInLink = useV2Format ? hexToOptimizedEtag(entry.etag).optimized : entry.etag;
                        const relativePath = commonPathForExport ? entry.fullPath.substring(commonPathForExport.length) : entry.fullPath;
                        return { path: relativePath, size: String(entry.size), etag: etagInLink };
                    })
                };

                if (useV2Format) processStateManager.appendLogMessage('💡 使用V2链接格式 (Base62 ETags) 生成。');
                else processStateManager.appendLogMessage('ℹ️ 使用V1链接格式 (标准 ETags) 生成。');

                const totalSize = allFileEntriesData.reduce((acc, entry) => acc + Number(entry.size), 0);
                const formattedTotalSize = formatBytes(totalSize);
                summary = `<div class="fastlink-result"><h3>🎉 从分享生成成功</h3><p>✅ 文件数量: ${successes} 个 (${failures > 0 ? '❌ ' : ''}项目处理失败 ${failures})</p><p>💾 总大小: ${formattedTotalSize}</p><p>⏱️ 耗时: ${totalTime} 秒</p><textarea class="fastlink-link-text" readonly>${link}</textarea></div>`;
                uiManager.showModal("🎉 秒传链接已生成 (来自分享)", summary, 'showLink', true, link, jsonDataForExport); return link;
            }
            uiManager.updateModalContent(summary); uiManager.enableModalCloseButton(true); return "";
        },


        _executeActualFileTransfer: async function(filesToProcess, isFolderStructureHint, operationTitle = "转存", initialPreprocessingFailures = []) {
            processStateManager.reset();
            this.currentOperationRateLimitStatus.consecutiveRateLimitFailures = 0;
            let permanentlyFailedItems = [...initialPreprocessingFailures];

            let rootDirId = this.getCurrentDirectoryId();
            if (rootDirId === null || isNaN(parseInt(rootDirId))) {
                uiManager.showAlert("无法确定当前目标目录ID。将尝试转存到根目录。");
                rootDirId = "0";
            }
            rootDirId = parseInt(rootDirId);
            // console.log(`[${SCRIPT_NAME}] ${operationTitle}: Transferring to directory ID: ${rootDirId}`);

            const initialModalTitle = `⚙️ ${operationTitle}状态 (${filesToProcess.length} 项)`;
            uiManager.showModal(initialModalTitle, `
                <div class="fastlink-progress-container"><div class="fastlink-progress-bar" style="width: 0%"></div></div>
                <div class="fastlink-status">
                    <p>🚀 准备${operationTitle} ${filesToProcess.length} 个文件到目录ID ${rootDirId}</p>
                    <p class="extra-status-message" style="color: #ff7f50; display: none;"></p>
                </div>
                <div class="fastlink-stats">
                    <span class="success-count">✅ 成功:0</span>
                    <span class="failed-count">❌ 失败:0</span>
                </div>
                <div class="fastlink-current-file">
                    <p class="file-name">准备开始...</p>
                </div>
                <div id="fastlink-permanent-failures-log" style="display: none; margin-top: 10px; text-align: left; max-height: 100px; overflow-y: auto; border: 1px solid #ddd; padding: 5px; font-size: 0.85em;">
                    <h4>永久失败项目:</h4>
                    <div id="fastlink-failures-list"></div>
                </div>
            `, 'progress_stoppable', false);

            let successes = 0, failures = 0; // These are per-attempt failures, not permanent ones initially
            const folderCache = {};
            const startTime = Date.now();

            for (let i = 0; i < filesToProcess.length; i++) {
                if (processStateManager.isStopRequested()) break;
                const file = filesToProcess[i];
                // Ensure file object has an 'error' property from previous failed attempts if this is a retry run
                const originalFileNameForLog = file.fileName || "未知文件";
                let currentAttemptError = null;

                if (!file || !file.fileName || !file.etag || !file.size) {
                    failures++; // Counts as an attempt failure for this run
                    const missingDataError = `跳过无效文件数据 (索引 ${i}): ${originalFileNameForLog}`;
                    processStateManager.appendLogMessage(`❌ ${missingDataError}`, true);
                    permanentlyFailedItems.push({ ...file, fileName: originalFileNameForLog, error: "无效文件数据" });
                    processStateManager.updateProgressUI(i + 1, filesToProcess.length, successes, failures, "无效数据");
                    continue;
                }

                processStateManager.updateProgressUI(i, filesToProcess.length, successes, failures, file.fileName, "");

                let effectiveParentId = rootDirId;
                let actualFileName = file.fileName;

                try {
                    if (file.fileName.includes('/')) {
                        const pathParts = file.fileName.split('/');
                        actualFileName = pathParts.pop();
                        if (!actualFileName && pathParts.length > 0 && file.fileName.endsWith('/')) {
                             processStateManager.appendLogMessage(`⚠️ 文件路径 "${file.fileName}" 可能表示一个目录,而不是文件。跳过。`, true);
                             failures++; // Counts as an attempt failure for this run
                             permanentlyFailedItems.push({ ...file, error: "路径表示目录" });
                             continue;
                        }
                        let parentIdForPathSegment = rootDirId;
                        let currentCumulativePath = "";

                        for (let j = 0; j < pathParts.length; j++) {
                            if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                            const part = pathParts[j];
                            if (!part) continue;

                            currentCumulativePath = j === 0 ? part : `${currentCumulativePath}/${part}`;
                            processStateManager.updateProgressUI(i, filesToProcess.length, successes, failures, file.fileName, `检查/创建路径: ${currentCumulativePath}`);

                            if (folderCache[currentCumulativePath]) {
                                parentIdForPathSegment = folderCache[currentCumulativePath];
                            } else {
                                let existingFolderId = null;
                                const dirContents = await apiHelper.listDirectoryContents(parentIdForPathSegment, 500);
                                if (processStateManager.isStopRequested()) throw new Error("UserStopped");

                                const foundFolder = dirContents.find(it => it.Type === 1 && it.FileName === part && !isNaN(it.FileID));
                                if (foundFolder) {
                                    existingFolderId = foundFolder.FileID;
                                }

                                if (existingFolderId) {
                                    parentIdForPathSegment = existingFolderId;
                                } else {
                                    processStateManager.updateProgressUI(i, filesToProcess.length, successes, failures, file.fileName, `创建文件夹: ${currentCumulativePath}`);
                                    const createdFolder = await apiHelper.createFolder(parentIdForPathSegment, part);
                                    if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                                    parentIdForPathSegment = parseInt(createdFolder.FileId);
                                }
                                folderCache[currentCumulativePath] = parentIdForPathSegment;
                                await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS));
                            }
                        }
                        effectiveParentId = parentIdForPathSegment;
                    }

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

                    processStateManager.updateProgressUI(i, filesToProcess.length, successes, failures, actualFileName, `秒传到ID: ${effectiveParentId}`);
                    await apiHelper.rapidUpload(file.etag, file.size, actualFileName, effectiveParentId);
                    if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                    successes++;
                    processStateManager.appendLogMessage(`✔️ 文件: ${file.fileName}`);

                } catch (e) {
                    currentAttemptError = e.message; // Capture error for this attempt
                    if (processStateManager.isStopRequested()) break;
                    failures++; // Counts as an attempt failure for this run
                    processStateManager.appendLogMessage(`❌ 文件 "${actualFileName}" (原始: ${originalFileNameForLog}) 失败: ${e.message}`, true);
                    permanentlyFailedItems.push({ ...file, fileName: originalFileNameForLog, error: e.message }); // Add to permanent failures
                    processStateManager.updateProgressUI(i + 1, filesToProcess.length, successes, failures, actualFileName, "操作失败");
                }
                await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS));
            }
             processStateManager.updateProgressUI(filesToProcess.length, filesToProcess.length, successes, failures, "处理完成", "");

            const totalTime = Math.round((Date.now() - startTime) / 1000);
            let resultEmoji = successes > 0 && permanentlyFailedItems.length === 0 ? '🎉' : (successes > 0 ? '🎯' : '😢');
            if (processStateManager.isStopRequested()) resultEmoji = '🔴';

            let finalUserMessage = processStateManager.isStopRequested() ? "操作已由用户停止" : `${operationTitle}完成`;
            if (!processStateManager.isStopRequested() && permanentlyFailedItems.length > 0) {
                finalUserMessage = `${operationTitle}部分完成或预处理失败,共 ${permanentlyFailedItems.length} 个文件有问题。`;
            }

            let summary = `
                <div class="fastlink-result">
                    <h3>${resultEmoji} ${finalUserMessage}</h3>
                    <p>✅ 成功转存: ${successes} 个文件</p>
                    <p>❌ 转存尝试失败: ${failures} 个文件</p>
                    <p>📋 总计问题文件 (含预处理): ${permanentlyFailedItems.length} 个</p>
                    <p>⏱️ 耗时: ${totalTime} 秒</p>
                    ${!processStateManager.isStopRequested() && successes > 0 ? '<p>📢 请手动刷新页面查看已成功转存的结果</p>' : ''}
                </div>
            `;

            uiManager.updateModalContent(summary); // Update with basic summary first

            // Now, if there are permanent failures, update the modal again to show them and offer retry options
            if (permanentlyFailedItems.length > 0 && !processStateManager.isStopRequested()) {
                const failuresLogDiv = document.getElementById('fastlink-failures-list');
                const permanentFailuresDiv = document.getElementById('fastlink-permanent-failures-log');
                if (failuresLogDiv && permanentFailuresDiv) {
                    failuresLogDiv.innerHTML = ''; // Clear previous logs if any (e.g., during retry)
                    permanentlyFailedItems.forEach(pf => {
                        const p = document.createElement('p');
                        p.style.margin = '2px 0';
                        p.innerHTML = `📄 <span style="font-weight:bold;">${pf.fileName}</span>: <span style="color:red;">${pf.error || '未知错误'}</span>`;
                        failuresLogDiv.appendChild(p);
                    });
                    permanentFailuresDiv.style.display = 'block';
                }
                // Add Retry and Copy Log buttons to the modal
                const modalInstance = uiManager.getModalElement();
                if (modalInstance) {
                    let buttonsDiv = modalInstance.querySelector('.fastlink-modal-buttons');
                    if(buttonsDiv) { // Clear existing buttons except maybe a Stop button if that's how it's structured
                        buttonsDiv.innerHTML = ''; // Clear to re-add
                    } else {
                        buttonsDiv = document.createElement('div');
                        buttonsDiv.className = 'fastlink-modal-buttons';
                        const contentArea = modalInstance.querySelector(`#${uiManager.MODAL_CONTENT_ID}`);
                        if(contentArea) contentArea.appendChild(buttonsDiv);
                    }

                    const retryBtn = document.createElement('button');
                    retryBtn.id = 'fl-m-retry-failed';
                    retryBtn.className = 'confirm-btn';
                    retryBtn.textContent = `🔁 重试失败项 (${permanentlyFailedItems.length})`;
                    retryBtn.onclick = () => {
                        // Disable buttons, re-show stop button (or handle in showModal)
                        this._executeActualFileTransfer(permanentlyFailedItems, isFolderStructureHint, operationTitle + " - 重试");
                    };
                    buttonsDiv.appendChild(retryBtn);

                    const copyLogBtn = document.createElement('button');
                    copyLogBtn.id = 'fl-m-copy-failed-log';
                    copyLogBtn.className = 'copy-btn'; // Using copy-btn style
                    copyLogBtn.style.marginLeft = '10px';
                    copyLogBtn.textContent = '复制问题日志';
                    copyLogBtn.onclick = () => {
                        const logText = permanentlyFailedItems.map(pf =>
                            `文件: ${pf.fileName || (pf.originalEntry && pf.originalEntry.path) || '未知路径'}\n` +
                            // Display original etag and size if available from originalEntry, especially for preprocessing errors
                            `${(pf.originalEntry && pf.originalEntry.etag) ? ('原始ETag: ' + pf.originalEntry.etag + '\n') : (pf.etag ? '处理后ETag: ' + pf.etag + '\n' : '')}` +
                            `${(pf.originalEntry && pf.originalEntry.size) ? ('大小: ' + pf.originalEntry.size + '\n') : (pf.size ? '大小: ' + pf.size + '\n' : '')}` +
                            `错误: ${pf.error || '未知错误'}`
                        ).join('\n\n');
                        GM_setClipboard(logText);
                        uiManager.showAlert("问题文件日志已复制到剪贴板!", 1500);
                    };
                    buttonsDiv.appendChild(copyLogBtn);

                    const closeBtn = document.createElement('button');
                    closeBtn.id = 'fl-m-final-close';
                    closeBtn.className = 'cancel-btn';
                    closeBtn.textContent = '关闭';
                    closeBtn.style.marginLeft = '10px';
                    closeBtn.onclick = () => uiManager.hideModal();
                    buttonsDiv.appendChild(closeBtn);
                }
                 uiManager.enableModalCloseButton(false); // Ensure close button added above is the only way, or manage through showModal flags
            } else {
                 uiManager.enableModalCloseButton(true); // Normal close if no permanent failures or stopped
            }
        }
    };

    // --- UI Manager ---
    const uiManager = { modalElement: null, dropdownMenuElement: null, STYLE_ID: 'fastlink-dynamic-styles', MODAL_CONTENT_ID: 'fastlink-modal-content-area',
    _downloadToFile: function(content, filename, contentType) {
        const a = document.createElement('a');
        const blob = new Blob([content], { type: contentType });
        a.href = URL.createObjectURL(blob);
        a.download = filename;
        a.click();
        URL.revokeObjectURL(a.href);
    },
    applyStyles: function() { if (document.getElementById(this.STYLE_ID)) return; GM_addStyle(`.fastlink-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background-color:white;padding:20px;border-radius:8px;box-shadow:0 0 15px rgba(0,0,0,.3);z-index:10001;width:420px;text-align:center}.fastlink-modal-title{font-size:18px;font-weight:700;margin-bottom:15px}.fastlink-modal-content textarea,.fastlink-modal-content div[contenteditable]{width:100%;min-height:80px;max-height:200px;overflow-y:auto;margin-bottom:15px;padding:8px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box;white-space:pre-wrap;word-wrap:break-word}.fastlink-modal-content .fastlink-link-text{width:calc(100% - 16px)!important;min-height:80px;margin-bottom:0!important}.fastlink-modal-input{width:calc(100% - 16px);padding:8px;margin-bottom:10px;border:1px solid #ccc;border-radius:4px}.fastlink-modal-buttons button{padding:8px 15px;margin:0 5px;border-radius:4px;cursor:pointer;border:1px solid transparent;font-size:14px}.fastlink-modal-buttons .confirm-btn{background-color:#28a745;color:#fff}.fastlink-modal-buttons .confirm-btn:disabled{background-color:#94d3a2;cursor:not-allowed}.fastlink-modal-buttons .cancel-btn,.fastlink-modal-buttons .close-btn{background-color:#6c757d;color:#fff}.fastlink-modal-buttons .stop-btn{background-color:#dc3545;color:#fff}.fastlink-modal-buttons .copy-btn{background-color:#007bff;color:#fff}.fastlink-modal-buttons .export-btn{background-color:#ffc107;color:#212529;margin-left:10px}.fastlink-file-input-container{margin-top:10px;margin-bottom:5px;text-align:left}.fastlink-file-input-container label{margin-right:5px;font-size:0.9em;}.fastlink-file-input-container input[type="file"]{font-size:0.9em;max-width:250px;}.fastlink-progress-container{width:100%;height:10px;background-color:#f0f0f0;border-radius:5px;margin:10px 0 15px;overflow:hidden}.fastlink-progress-bar{height:100%;background-color:#1890ff;transition:width .3s ease}.fastlink-status{text-align:left;margin-bottom:10px;max-height:150px;overflow-y:auto;border:1px solid #eee;padding:5px;font-size:.9em}.fastlink-status p{margin:3px 0;line-height:1.3}.fastlink-stats{display:flex;justify-content:space-between;margin:10px 0;border-top:1px solid #eee;border-bottom:1px solid #eee;padding:5px 0}.fastlink-current-file{background-color:#f9f9f9;padding:5px;border-radius:4px;margin:5px 0;min-height:1.5em;word-break:break-all}.error-message{color:#d9534f;font-size:.9em}.info-message{color:#28a745;font-size:.9em}.fastlink-result{text-align:center}.fastlink-result h3{font-size:18px;margin:5px 0 15px}.fastlink-result p{margin:8px 0}#fastlink-dropdown-menu-container{position:absolute;background:#fff;border:1px solid #ccc;padding:2px;box-shadow:0 4px 6px rgba(0,0,0,.1);margin-top:5px;z-index:10000} .fastlink-drag-drop-area{border:2px dashed #ccc;padding:10px;transition: border-color .3s ease;} .fastlink-drag-drop-area.drag-over-active{border-color:#007bff;} `); }, createDropdownButton: function() { const existingButtons = document.querySelectorAll('.fastlink-main-button-container'); existingButtons.forEach(btn => btn.remove()); const targetElement = document.querySelector(DOM_SELECTORS.TARGET_BUTTON_AREA); if (targetElement && targetElement.parentNode) { const buttonContainer = document.createElement('div'); buttonContainer.className = 'fastlink-main-button-container ant-dropdown-trigger sysdiv parmiryButton'; buttonContainer.style.borderRight = '0.5px solid rgb(217, 217, 217)'; buttonContainer.style.cursor = 'pointer'; buttonContainer.style.marginLeft = '20px'; buttonContainer.innerHTML = `<span role="img" aria-label="menu" class="anticon anticon-menu" style="margin-right: 6px;"><svg viewBox="64 64 896 896" focusable="false" data-icon="menu" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M120 300h720v60H120zm0 180h720v60H120zm0 180h720v60H120z"></path></svg></span> 秒传 `;
            const dropdownMenu = document.createElement('div');
            dropdownMenu.id = 'fastlink-dropdown-menu-container';
            dropdownMenu.style.display = 'none';
            // --- MODIFICATION: Added "从公开分享生成" menu item ---
            dropdownMenu.innerHTML = `
                <ul class="ant-dropdown-menu ant-dropdown-menu-root ant-dropdown-menu-vertical ant-dropdown-menu-light" role="menu" tabindex="0" data-menu-list="true" style="border-radius: 10px;">
                    <li id="fastlink-generateShare" class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" style="padding: 5px 12px;">🔗 生成链接 (选中项)</li>
                    <li id="fastlink-generateFromPublicShare" class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" style="padding: 5px 12px;">🌐 从公开分享生成</li>
                    <li id="fastlink-receiveDirect" class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" style="padding: 5px 12px;">📥 链接/文件转存</li>
                </ul>`;
            this.dropdownMenuElement = dropdownMenu;
            buttonContainer.addEventListener('click', (e) => { e.stopPropagation(); dropdownMenu.style.display = dropdownMenu.style.display === 'none' ? 'block' : 'none'; });
            document.addEventListener('click', (e) => { if (this.dropdownMenuElement && !buttonContainer.contains(e.target) && !this.dropdownMenuElement.contains(e.target)) { if (this.dropdownMenuElement.style.display !== 'none') this.dropdownMenuElement.style.display = 'none'; } });
            dropdownMenu.querySelector('#fastlink-generateShare').addEventListener('click', async (e) => { e.stopPropagation(); dropdownMenu.style.display = 'none'; await coreLogic.generateShareLink(); });

            // --- MODIFICATION: Event listener for new menu item ---
            dropdownMenu.querySelector('#fastlink-generateFromPublicShare').addEventListener('click', (e) => {
                e.stopPropagation();
                dropdownMenu.style.display = 'none';
                this.showModal("🌐 从公开分享生成链接", "", 'inputPublicShare');
            });

            dropdownMenu.querySelector('#fastlink-receiveDirect').addEventListener('click', (e) => { e.stopPropagation(); dropdownMenu.style.display = 'none'; this.showModal("📥 文件转存/粘贴链接", "", 'inputLink'); }); // Updated title
            targetElement.parentNode.insertBefore(buttonContainer, targetElement.nextSibling);
            buttonContainer.appendChild(dropdownMenu);
            console.log(`[${SCRIPT_NAME}] 秒传按钮已添加。`);
            return true;
        } else {
            console.warn(`[${SCRIPT_NAME}] 目标按钮区域 '${DOM_SELECTORS.TARGET_BUTTON_AREA}' 未找到。`);
            return false;
        }
    },
    showModal: function(title, content, type = 'info', closable = true, pureLinkForClipboard = null, jsonDataForExport = null, preprocessingFailuresForLog = null) {
        if (this.modalElement) this.modalElement.remove();
        this.modalElement = document.createElement('div');
        this.modalElement.className = 'fastlink-modal';
        let htmlContent = `<div class="fastlink-modal-title">${title}</div><div id="${this.MODAL_CONTENT_ID}" class="fastlink-modal-content">`;
        if (type === 'inputLink') {
            htmlContent += `<div id="fl-m-drop-area" class="fastlink-drag-drop-area">`; // Wrapper for drag-drop
            htmlContent += `<textarea id="fl-m-link-input" class="fastlink-modal-input" placeholder="🔗 粘贴秒传链接 或 📂 将文件拖放到此处..." style="min-height: 60px;">${content|| ''}</textarea>`;
            htmlContent += `<div id="fl-m-file-drop-status" style="font-size:0.9em; color:#28a745; margin-top:5px; margin-bottom:5px; min-height:1.2em;"></div>`; // Status message div
            htmlContent += `<div class="fastlink-file-input-container">`;
            htmlContent += `<label for="fl-m-file-input">或通过选择文件导入:</label>`;
            htmlContent += `<input type="file" id="fl-m-file-input" accept=".json,.123fastlink,.txt" class="fastlink-modal-file-input">`;
            htmlContent += `</div></div>`; // Close drop-area
        } else if (type === 'inputPublicShare') { // --- MODIFICATION: New modal type for public share input ---
            htmlContent += `<input type="text" id="fl-m-public-share-key" class="fastlink-modal-input" placeholder="🔑 分享Key (链接中 /s/ 后部分, 如 xxxx-yyyy)">`;
            htmlContent += `<input type="text" id="fl-m-public-share-pwd" class="fastlink-modal-input" placeholder="🔒 提取码 (如有)">`;
            htmlContent += `<input type="text" id="fl-m-public-share-fid" class="fastlink-modal-input" value="0" placeholder="📁 起始文件夹ID (默认0为根目录)">`;
        } else htmlContent += content;
        htmlContent += `</div><div class="fastlink-modal-buttons">`;
        if (type === 'inputLink') {
            htmlContent += `<button id="fl-m-confirm" class="confirm-btn">➡️ 转存</button><button id="fl-m-cancel" class="cancel-btn">取消</button>`;
        } else if (type === 'inputPublicShare') { // --- MODIFICATION: Buttons for public share modal ---
            htmlContent += `<button id="fl-m-generate-public" class="confirm-btn">✨ 生成</button><button id="fl-m-cancel" class="cancel-btn">取消</button>`;
        } else if (type === 'showLink') {
            htmlContent += `<button id="fl-m-copy" class="copy-btn">📋 复制链接</button>`;
            if (jsonDataForExport) {
                htmlContent += `<button id="fl-m-export-json" class="export-btn">📄 导出到文件</button>`;
            }
            htmlContent += `<button id="fl-m-cancel" class="close-btn">关闭</button>`;
        } else if (type === 'progress_stoppable') {
            htmlContent += `<button id="${processStateManager.getStopButtonId()}" class="stop-btn">🛑 停止</button><button id="fl-m-cancel" class="close-btn" disabled>关闭</button>`;
        } else if (type === 'info_with_buttons' && preprocessingFailuresForLog && preprocessingFailuresForLog.length > 0) {
            // Special case for showing only preprocessing failures with a copy log button
            htmlContent += `<button id="fl-m-copy-preprocessing-log" class="copy-btn">📋 复制日志</button>`;
            htmlContent += `<button id="fl-m-cancel" class="close-btn" style="margin-left:10px;">关闭</button>`;
        } else {
            htmlContent += `<button id="fl-m-cancel" class="close-btn">关闭</button>`;
        }
        htmlContent += `</div>`;
        this.modalElement.innerHTML = htmlContent;
        document.body.appendChild(this.modalElement);

        const confirmBtn = this.modalElement.querySelector('#fl-m-confirm');
        const copyBtn = this.modalElement.querySelector('#fl-m-copy');
        const cancelBtn = this.modalElement.querySelector('#fl-m-cancel');
        const stopBtn = this.modalElement.querySelector(`#${processStateManager.getStopButtonId()}`);
        const exportJsonBtn = this.modalElement.querySelector('#fl-m-export-json');
        const generatePublicBtn = this.modalElement.querySelector('#fl-m-generate-public'); // --- MODIFICATION: Button for public share generation ---
        const copyPreprocessingLogBtn = this.modalElement.querySelector('#fl-m-copy-preprocessing-log');

        if(confirmBtn){ // Handles 'inputLink' (transfer)
            confirmBtn.onclick = async () => {
                const linkInputEl = this.modalElement.querySelector(`#fl-m-link-input`);
                const fileInputEl = this.modalElement.querySelector(`#fl-m-file-input`);
                let link = linkInputEl ? linkInputEl.value.trim() : null;
                // File input can be set by direct selection or by drag-and-drop
                let file = fileInputEl && fileInputEl.files && fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;

                confirmBtn.disabled = true;
                if(cancelBtn) cancelBtn.disabled = true;

                if (file) { // File takes precedence
                    processStateManager.appendLogMessage(`ℹ️ 从文件 "${file.name}" 导入...`);
                    try {
                        const fileContent = await file.text();
                        const jsonData = JSON.parse(fileContent);
                        await coreLogic.transferImportedJsonData(jsonData);
                    } catch (e) {
                        console.error(`[${SCRIPT_NAME}] 文件导入失败:`, e);
                        processStateManager.appendLogMessage(`❌ 文件导入失败: ${e.message}`, true);
                        uiManager.showError(`文件读取或解析失败: ${e.message}`);
                    }
                } else if (link) {
                    await coreLogic.transferFromShareLink(link);
                } else {
                    this.showAlert("请输入链接或选择/拖放文件");
                }

                if(this.modalElement && confirmBtn){ // Check if modal still exists
                     confirmBtn.disabled = false;
                     if(cancelBtn) cancelBtn.disabled = false;
                }
            };
        }

        // --- MODIFICATION START: Drag and Drop for 'inputLink' modal ---
        if (type === 'inputLink') {
            const dropArea = this.modalElement.querySelector('#fl-m-drop-area');
            const fileInputEl = this.modalElement.querySelector(`#fl-m-file-input`);
            const linkInputEl = this.modalElement.querySelector('#fl-m-link-input');
            const statusDiv = this.modalElement.querySelector('#fl-m-file-drop-status');

            if (dropArea && fileInputEl && linkInputEl && statusDiv) {
                // Event listener for text input to clear file selection
                linkInputEl.addEventListener('input', () => {
                    if (linkInputEl.value.trim() !== '') {
                        if (fileInputEl.files && fileInputEl.files.length > 0) {
                            fileInputEl.value = ''; // Clears the file list
                        }
                        statusDiv.textContent = '';
                    }
                });

                // Event listener for file selection via button
                fileInputEl.addEventListener('change', () => {
                    if (fileInputEl.files && fileInputEl.files.length > 0) {
                        statusDiv.textContent = `已选中文件: ${fileInputEl.files[0].name}。请点击下方"转存"按钮。`;
                        if(linkInputEl) linkInputEl.value = ''; // Clear link input
                    } else {
                        statusDiv.textContent = '';
                    }
                });

                // Drag and drop event listeners for the drop area
                ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
                    dropArea.addEventListener(eventName, (e) => {
                        e.preventDefault();
                        e.stopPropagation();
                    }, false);
                });

                ['dragenter', 'dragover'].forEach(eventName => {
                    dropArea.addEventListener(eventName, () => {
                        dropArea.classList.add('drag-over-active');
                    }, false);
                });

                ['dragleave', 'drop'].forEach(eventName => {
                    dropArea.addEventListener(eventName, () => {
                        dropArea.classList.remove('drag-over-active');
                    }, false);
                });

                dropArea.addEventListener('drop', (e) => {
                    const dt = e.dataTransfer;
                    if (dt && dt.files && dt.files.length > 0) {
                        const droppedFile = dt.files[0];
                        if (droppedFile.name.endsWith('.json') || droppedFile.name.endsWith('.123fastlink') || droppedFile.name.endsWith('.txt') || droppedFile.type === 'application/json' || droppedFile.type === 'text/plain') {
                            try {
                                const dataTransfer = new DataTransfer();
                                dataTransfer.items.add(droppedFile);
                                fileInputEl.files = dataTransfer.files;

                                if (statusDiv) statusDiv.textContent = `已拖放文件: ${droppedFile.name}。请点击下方"转存"按钮。`;
                                if (linkInputEl) linkInputEl.value = '';

                            } catch (err) {
                                console.error("Error creating DataTransfer or setting files:", err);
                                if (statusDiv) statusDiv.textContent = "处理拖放文件时发生错误。";
                                else this.showError("处理拖放文件时发生错误。");
                            }
                        } else {
                            if (statusDiv) statusDiv.textContent = "文件类型无效。请拖放 .json, .123fastlink, 或 .txt 文件。";
                            else this.showError("文件类型无效。请拖放 .json, .123fastlink, 或 .txt 文件。");
                        }
                    }
                }, false);
            }
        }
        // --- MODIFICATION END: Drag and Drop for 'inputLink' modal ---

        if(generatePublicBtn){ // --- MODIFICATION: Event listener for public share generation button ---
            generatePublicBtn.onclick = async () => {
                const shareKeyEl = this.modalElement.querySelector('#fl-m-public-share-key');
                const sharePwdEl = this.modalElement.querySelector('#fl-m-public-share-pwd');
                const shareFidEl = this.modalElement.querySelector('#fl-m-public-share-fid');

                const rawShareKeyInput = shareKeyEl ? shareKeyEl.value.trim() : null;
                const sharePwd = sharePwdEl ? sharePwdEl.value.trim() : null; // Can be empty
                const shareFid = shareFidEl ? shareFidEl.value.trim() : "0";

                let finalShareKey = rawShareKeyInput;
                if (rawShareKeyInput && rawShareKeyInput.includes('/s/')) {
                    try {
                        const url = new URL(rawShareKeyInput);
                        const pathSegments = url.pathname.split('/');
                        const sIndex = pathSegments.indexOf('s');
                        if (sIndex !== -1 && pathSegments.length > sIndex + 1) {
                            finalShareKey = pathSegments[sIndex + 1];
                        } else {
                            // Fallback for non-standard URL structures or if key is directly after /s/
                            let pathAfterS = rawShareKeyInput.substring(rawShareKeyInput.lastIndexOf('/s/') + 3);
                            finalShareKey = pathAfterS.split('/')[0];
                        }
                    } catch (e) {
                        // If URL parsing fails, fallback to simpler string manipulation
                        // This handles cases where input might not be a full valid URL but contains /s/
                        let pathAfterS = rawShareKeyInput.substring(rawShareKeyInput.lastIndexOf('/s/') + 3);
                        finalShareKey = pathAfterS.split('/')[0];
                        console.warn(`[${SCRIPT_NAME}] Share key input parsing as URL failed, used string manipulation. Input: ${rawShareKeyInput}, Error: ${e.message}`);
                    }
                }


                if (!finalShareKey) {
                    this.showAlert("请输入有效的分享Key或分享链接。");
                    return;
                }
                if (isNaN(parseInt(shareFid))) {
                     this.showAlert("起始文件夹ID必须是数字。");
                     return;
                }
                generatePublicBtn.disabled = true;
                if(cancelBtn) cancelBtn.disabled = true;

                await coreLogic.generateLinkFromPublicShare(finalShareKey, sharePwd, shareFid);

                if(this.modalElement && generatePublicBtn){ // Check if modal still exists
                     generatePublicBtn.disabled = false;
                     if(cancelBtn) cancelBtn.disabled = false;
                }
            };
        }
        if(copyBtn){ copyBtn.onclick = () => { const textToCopy = pureLinkForClipboard || this.modalElement.querySelector('.fastlink-link-text')?.value; if (textToCopy) { GM_setClipboard(textToCopy); this.showAlert("已复制到剪贴板!");} else this.showError("无法找到链接文本。"); };}
        if(exportJsonBtn && jsonDataForExport){
            exportJsonBtn.onclick = () => {
                try {
                    this._downloadToFile(JSON.stringify(jsonDataForExport, null, 2), `123FastLink_${Date.now()}.json`, 'application/json');
                    this.showAlert("JSON文件已开始下载!");
                } catch (e) {
                    console.error(`[${SCRIPT_NAME}] 导出JSON失败:`, e);
                    this.showError(`导出JSON失败: ${e.message}`);
                }
            };
        }
        if(cancelBtn && (closable || type === 'progress_stoppable')){
            cancelBtn.onclick = () => this.hideModal();
        }
        if(stopBtn){
            stopBtn.onclick = () => processStateManager.requestStop();
        }
        if(!closable && cancelBtn && type !== 'progress_stoppable') cancelBtn.disabled = true;
        if(copyPreprocessingLogBtn && preprocessingFailuresForLog) {
            copyPreprocessingLogBtn.onclick = () => {
                const logText = preprocessingFailuresForLog.map(pf =>
                    `文件: ${pf.fileName || (pf.originalEntry && pf.originalEntry.path) || '未知路径'}\n` +
                    `${(pf.originalEntry && pf.originalEntry.etag) ? ('原始ETag: ' + pf.originalEntry.etag + '\n') : ''}` +
                    `${(pf.originalEntry && pf.originalEntry.size) ? ('大小: ' + pf.originalEntry.size + '\n') : ''}` +
                    `错误: ${pf.error || '未知错误'}`
                ).join('\n\n');
                GM_setClipboard(logText);
                this.showAlert("预处理失败日志已复制到剪贴板!", 1500);
            };
        }
    },
    enableModalCloseButton: function(enable = true) {
        if (this.modalElement) {
            const closeBtn = this.modalElement.querySelector('#fl-m-cancel.close-btn');
            if (closeBtn) closeBtn.disabled = !enable;
            const stopBtn = this.modalElement.querySelector(`#${processStateManager.getStopButtonId()}`);
            if (stopBtn) stopBtn.disabled = true; // Always disable stop button when enabling close button
        }
    },
    updateModalContent: function(newContent) {
        if (this.modalElement) {
            const ca = this.modalElement.querySelector(`#${this.MODAL_CONTENT_ID}`);
            if (ca) {
                if (ca.tagName === 'TEXTAREA' || ca.hasAttribute('contenteditable')) ca.value = newContent;
                else ca.innerHTML = newContent;
                ca.scrollTop = ca.scrollHeight;}
        }
    },
    hideModal: function() {
        if (this.modalElement) {
            this.modalElement.remove();
            this.modalElement = null;
        }
    },
    showAlert: function(message, duration = 2000) {
        this.showModal("ℹ️ 提示", message, 'info');
        setTimeout(() => {
            if (this.modalElement && this.modalElement.querySelector('.fastlink-modal-title')?.textContent === "ℹ️ 提示") this.hideModal();
        }, duration);
    },
    showError: function(message, duration = 3000) {
        this.showModal("⚠️ 错误", `<span style="color: red;">${message}</span>`, 'info');
        setTimeout(() => {
            if (this.modalElement && this.modalElement.querySelector('.fastlink-modal-title')?.textContent === "⚠️ 错误") this.hideModal();
        }, duration);
    },
    getModalElement: function() { return this.modalElement; },
    };

    // --- Initialization ---
    function initialize() {
        console.log(`[${SCRIPT_NAME}] ${SCRIPT_VERSION} 初始化...`);
        uiManager.applyStyles();
        let loadAttempts = 0;
        const maxAttempts = 10;
        function tryAddButton() {
            loadAttempts++;
            const pageSeemsReady = document.querySelector(DOM_SELECTORS.TARGET_BUTTON_AREA) || document.querySelector('.Header_header__A5PFb');
            if (pageSeemsReady) {
                if (document.querySelector('.fastlink-main-button-container')) return; // Already added
                if (uiManager.createDropdownButton()) return; // Successfully added
            }
            if (loadAttempts < maxAttempts) {
                const delay = loadAttempts < 3 ? 1500 : 3000;
                setTimeout(tryAddButton, delay);
            } else {
                console.warn(`[${SCRIPT_NAME}] 达到最大尝试次数,未能添加按钮。`);
            }
        }

        const observer = new MutationObserver((mutations, obs) => {
            const targetAreaExists = !!document.querySelector(DOM_SELECTORS.TARGET_BUTTON_AREA);
            const ourButtonExists = !!document.querySelector('.fastlink-main-button-container');
            if (targetAreaExists && !ourButtonExists) {
                loadAttempts = 0; // Reset attempts if target area appears
                setTimeout(tryAddButton, 700); // Give a bit of time for other scripts to settle
            }
        });
        observer.observe(document.documentElement, { childList: true, subtree: true });
        setTimeout(tryAddButton, 500); // Initial attempt
    }

    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        setTimeout(initialize, 300); // Ensure DOM is fully parsed and some scripts might have run
    } else {
        window.addEventListener('DOMContentLoaded', () => setTimeout(initialize, 300));
    }

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

    function bigIntToBase62(num) {
        if (typeof num !== 'bigint') throw new Error("Input must be a BigInt for Base62 conversion.");
        if (num === 0n) return BASE62_CHARS[0];
        let base62 = "";
        let n = num; // ETags are positive, no need to check sign explicitly
        while (n > 0n) {
            base62 = BASE62_CHARS[Number(n % 62n)] + base62;
            n = n / 62n;
        }
        return base62;
    }

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

    function hexToOptimizedEtag(hexEtag) { // Tries to convert to Base62
        if (!isValidHex(hexEtag) || hexEtag.length === 0) { // Also handle empty etag as non-convertible to V2
            return { original: hexEtag, optimized: null, useV2: false }; // Cannot convert to Base62
        }
        try {
            const bigIntValue = BigInt('0x' + hexEtag);
            const base62Value = bigIntToBase62(bigIntValue);
            // Optional: Only use base62 if it's actually shorter, though for typical ETags it will be.
            // And ensure it's not empty in case of some weird input like "0" hex.
            if (base62Value.length > 0 && base62Value.length < hexEtag.length) {
                 return { original: hexEtag, optimized: base62Value, useV2: true };
            }
            return { original: hexEtag, optimized: hexEtag, useV2: false }; // Not shorter or empty, use original
        } catch (e) {
            console.warn(`[${SCRIPT_NAME}] Failed to convert ETag "${hexEtag}" to Base62: ${e.message}. Using original.`);
            return { original: hexEtag, optimized: null, useV2: false }; // Error during conversion
        }
    }

    function optimizedEtagToHex(optimizedEtag, isV2Etag) {
        if (!isV2Etag) return optimizedEtag; // It's already hex (or original format)
        if (typeof optimizedEtag !== 'string' || optimizedEtag.length === 0) {
             throw new Error("V2 ETag cannot be empty for hex conversion.");
        }
        try {
            const bigIntValue = base62ToBigInt(optimizedEtag);
            let hex = bigIntValue.toString(16).toLowerCase(); // Convert to hex and ensure lowercase

            // Attempt to pad to 32 characters if it seems like an MD5 ETag.
            // Common Base62 length for a 32-char hex MD5 is around 21-22 characters.
            // This is a heuristic. Some ETags might not be MD5 and might have different lengths.
            if (hex.length < 32 && optimizedEtag.length >= 21 && optimizedEtag.length <= 22) {
                hex = hex.padStart(32, '0');
            }
            return hex;
        } catch (e) {
            throw new Error(`Failed to convert Base62 ETag "${optimizedEtag}" to Hex: ${e.message}`);
        }
    }
    // --- End Base62 Helper Functions ---

})();

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