Greasy Fork

Greasy Fork is available in English.

123云盘秒传链接

123FastLink是一款适用于123网盘(123Pan) 的秒传链接生成与转存的用户脚本。

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

// ==UserScript==
// @name         123云盘秒传链接
// @namespace    http://tampermonkey.net/
// @version      1.1.47
// @description  123FastLink是一款适用于123网盘(123Pan) 的秒传链接生成与转存的用户脚本。
// @author        Gemini
// @match        *://*.123pan.com/*
// @match        *://*.123pan.cn/*
// @icon         https://www.123pan.com/favicon.ico
// @license      MIT
// @grant        GM_setClipboard
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // --- Constants and Configuration ---
    const SCRIPT_NAME = "123FastLink";
    const SCRIPT_VERSION = "1.1.47";
    const FOLDER_LINK_PREFIX = "123FSLinkV1$";

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

    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 Specific
        RATE_LIMIT_ITEM_RETRY_DELAY_MS: 5000,    // Pause 5s after a rate-limit error for an item
        RATE_LIMIT_MAX_ITEM_RETRIES: 2,          // Max rate-limit retries for an item *within one general attempt*
        RATE_LIMIT_GLOBAL_PAUSE_TRIGGER_FAILURES: 3, // Consecutive items hitting rate-limit for global pause
        RATE_LIMIT_GLOBAL_PAUSE_DURATION_MS: 30000, // Global pause 30s

        // General API Call Retries (for non-rate-limit errors like network issues)
        GENERAL_API_RETRY_DELAY_MS: 3000,        // Pause 3s before retrying a general error
        GENERAL_API_MAX_RETRIES: 2,              // Retry up to 2 times for general errors

        // Proactive Delay (to stay under QPS limits)
        // QPS 5 = 200ms/request. Set slightly higher for safety.
        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) {
            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',
                'Authorization': 'Bearer ' + config.authToken,
                'platform': 'web',
                'App-Version': config.appVersion,
                'LoginUuid': config.loginUuid,
                'Origin': config.host,
                'Referer': config.referer,
            };

            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) {
                // console.error(`[${SCRIPT_NAME} API Err] ${method} ${path}`, error); // Logged by caller's retry logic
                if (!error.isRateLimit && !error.message?.startsWith("UserStopped")) {
                     // uiManager.showError(error.message || '未知网络错误'); // Avoid too many popups during retries
                }
                throw error;
            }
        },
        createFolder: async function(parentId, folderName) { /* ... (no change) ... */ 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) { /* ... (no change) ... */ 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) { /* ... (no change) ... */ 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) { /* ... (no change) ... */ 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响应异常');
        },
    };

    // --- 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; // Assumes pre-formatted
                const extraStatusSibling = logArea.querySelector('.extra-status-message');
                if (extraStatusSibling) logArea.insertBefore(p, extraStatusSibling.nextSibling); // Insert after extra status
                else logArea.appendChild(p);
                logArea.scrollTop = logArea.scrollHeight;
            }
        }
    };

    // --- Core Logic ---
    const coreLogic = {
        currentOperationRateLimitStatus: { consecutiveRateLimitFailures: 0 }, // Shared status for an ongoing operation

        _executeApiWithRetries: async function(apiFunctionExecutor, itemNameForLog, rateLimitStatusRef) {
            let generalErrorRetries = 0;
            while (generalErrorRetries <= RETRY_AND_DELAY_CONFIG.GENERAL_API_MAX_RETRIES) {
                if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                let rateLimitRetriesForCurrentGeneralAttempt = 0;
                while (rateLimitRetriesForCurrentGeneralAttempt <= RETRY_AND_DELAY_CONFIG.RATE_LIMIT_MAX_ITEM_RETRIES) {
                    if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                    try {
                        const result = await apiFunctionExecutor();
                        rateLimitStatusRef.consecutiveRateLimitFailures = 0;
                        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 { // General error
                            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: () => { /* ... (no change) ... */ 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); },
        getCurrentDirectoryId: () => { /* ... (no change) ... */ const url = window.location.href; const match = url.match(/fid=(\d+)|#\/list\/folder\/(\d+)|drive\/(\d+)/); if (match) return match[1] || match[2] || match[3] || "0"; return url.includes("/drive") || url.endsWith(".com/") || url.endsWith(".cn/") ? "0" : "0"; },

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

            processStateManager.reset();
            this.currentOperationRateLimitStatus.consecutiveRateLimitFailures = 0; // Reset for this operation
            let allFileEntries = [], processedAnyFolder = false;
            const totalItemsToScan = selectedItemIds.length;
            let itemsScanned = 0, successes = 0, failures = 0;

            uiManager.showModal("生成秒传链接", `
                <div class="fastlink-progress-container"><div class="fastlink-progress-bar" style="width: 0%"></div></div>
                <div class="fastlink-status">
                    <p>🔍 正在分析选中的 ${totalItemsToScan} 个项目...</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(itemsScanned, totalItemsToScan, successes, failures, baseItemName, "获取信息...");

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

                if (isNaN(itemDetails.FileID)) {
                    failures++; processStateManager.appendLogMessage(`❌ 项目 "${itemDetails.FileName || itemId}" FileID无效`, true); return;
                }
                const cleanName = (itemDetails.FileName || "Unknown").replace(/[#$]/g, "_");
                const itemDisplayPath = `${currentRelativePath ? currentRelativePath + '/' : ''}${cleanName}`;
                processStateManager.updateProgressUI(itemsScanned, totalItemsToScan, successes, failures, itemDisplayPath);

                if (itemDetails.Type === 0) { // File
                    if (itemDetails.Etag && itemDetails.Size !== undefined) {
                        allFileEntries.push([itemDetails.Etag, itemDetails.Size, itemDisplayPath].join('#'));
                        successes++; processStateManager.appendLogMessage(`✔️ 文件: ${itemDisplayPath}`);
                    } else {
                        failures++; processStateManager.appendLogMessage(`❌ 文件 "${itemDisplayPath}" 缺少Etag或大小`, true);
                    }
                } else if (itemDetails.Type === 1) { // Folder
                    processedAnyFolder = true;
                    processStateManager.appendLogMessage(`📁 扫描文件夹: ${itemDisplayPath}`);
                    processStateManager.updateProgressUI(itemsScanned, totalItemsToScan, successes, failures, itemDisplayPath, "列出内容...");
                    try {
                        const contents = await apiHelper.listDirectoryContents(itemDetails.FileID); // Uses _executeApiWithRetries
                        if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                        for (const contentItem of contents) {
                            if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                            if (isNaN(contentItem.FileID)) 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;
                        failures++;
                        processStateManager.appendLogMessage(`❌ 处理文件夹 "${itemDisplayPath}" 内容最终失败: ${e.message}`, true);
                        processStateManager.updateProgressUI(itemsScanned, totalItemsToScan, successes, failures, itemDisplayPath, "列出内容失败");
                    }
                }
                await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS));
            }

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

            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 (!allFileEntries.length && failures > 0) summary = `<div class="fastlink-result"><h3>😢 生成失败</h3><p>未能提取有效文件信息 (${successes} 成功, ${failures} 失败)</p><p>⏱️ 耗时: ${totalTime} 秒</p></div>`;
            else if (!allFileEntries.length) summary = `<div class="fastlink-result"><h3>🤔 无文件</h3><p>未选中文件或文件夹为空</p></div>`;
            else {
                let link = allFileEntries.join('$'); if (processedAnyFolder) link = FOLDER_LINK_PREFIX + link;
                summary = `<div class="fastlink-result"><h3>🎉 生成成功</h3><p>✅ 文件数量: ${successes} 个 (失败 ${failures})</p><p>⏱️ 耗时: ${totalTime} 秒</p><textarea class="fastlink-link-text" readonly>${link}</textarea></div>`;
                uiManager.showModal("秒传链接已生成", summary, 'showLink', true, link); return link;
            }
            uiManager.updateModalContent(summary); uiManager.enableModalCloseButton(true); return "";
        },

        parseShareLink: (shareLink) => { /* ... (no change) ... */ if (shareLink.startsWith(FOLDER_LINK_PREFIX)) shareLink = shareLink.substring(FOLDER_LINK_PREFIX.length); return shareLink.split('$').map(sLink => { const parts = sLink.split('#'); return parts.length >= 3 ? { etag: parts[0], size: parts[1], fileName: parts.slice(2).join('#') } : null; }).filter(i => i); },

        transferFromShareLink: async function(shareLink) {
            if (!shareLink?.trim()) { uiManager.showAlert("链接为空"); return; }
            processStateManager.reset();
            this.currentOperationRateLimitStatus.consecutiveRateLimitFailures = 0; // Reset for this operation
            const isFolderStructure = shareLink.startsWith(FOLDER_LINK_PREFIX);
            const files = this.parseShareLink(shareLink);
            if (!files.length) { uiManager.showAlert("无法解析链接或链接中无有效文件信息"); return; }
            let rootDirId = this.getCurrentDirectoryId();
            if (rootDirId === null || isNaN(parseInt(rootDirId))) { uiManager.showAlert("无法确定当前目标目录ID。"); return; }
            rootDirId = parseInt(rootDirId);

            uiManager.showModal("转存状态", `
                <div class="fastlink-progress-container"><div class="fastlink-progress-bar" style="width: 0%"></div></div>
                <div class="fastlink-status">
                    <p>🚀 准备转存 ${files.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>`, 'progress_stoppable', false);

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

            for (let i = 0; i < files.length; i++) {
                if (processStateManager.isStopRequested()) break;
                const file = files[i];
                processStateManager.updateProgressUI(i, files.length, successes, failures, file.fileName, "");

                let effectiveParentId = rootDirId; let actualFileName = file.fileName;
                try {
                    if (isFolderStructure && file.fileName.includes('/')) {
                        const pathParts = file.fileName.split('/'); actualFileName = pathParts.pop();
                        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, files.length, successes, failures, file.fileName, `检查/创建路径: ${currentCumulativePath}`);
                            if (folderCache[currentCumulativePath]) parentIdForPathSegment = folderCache[currentCumulativePath];
                            else {
                                let existingFolderId = null;
                                const dirContents = await apiHelper.listDirectoryContents(parentIdForPathSegment, 500); // Uses _executeApiWithRetries
                                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, files.length, successes, failures, file.fileName, `创建文件夹: ${currentCumulativePath}`);
                                    const createdFolder = await apiHelper.createFolder(parentIdForPathSegment, part); // Uses _executeApiWithRetries
                                    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})`);
                    processStateManager.updateProgressUI(i, files.length, successes, failures, actualFileName, `秒传到ID: ${effectiveParentId}`);
                    await apiHelper.rapidUpload(file.etag, file.size, actualFileName, effectiveParentId); // Uses _executeApiWithRetries
                    if (processStateManager.isStopRequested()) throw new Error("UserStopped");
                    successes++; processStateManager.appendLogMessage(`✔️ 文件: ${file.fileName}`);
                } catch (e) {
                    if (processStateManager.isStopRequested()) break;
                    failures++;
                    processStateManager.appendLogMessage(`❌ 文件 "${actualFileName}" 失败: ${e.message}`, true);
                    processStateManager.updateProgressUI(i, files.length, successes, failures, actualFileName, "操作失败");
                }
                await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS));
            }

            const totalTime = Math.round((Date.now() - startTime) / 1000);
            let resultEmoji = successes > 0 && failures === 0 ? '🎉' : (successes > 0 ? '🎯' : '😢');
            if (processStateManager.isStopRequested()) resultEmoji = '🔴';
            const finalMessage = processStateManager.isStopRequested() ? "操作已由用户停止" : "转存完成";
            const summary = `<div class="fastlink-result"><h3>${resultEmoji} ${finalMessage}</h3><p>✅ 成功: ${successes} 个文件</p><p>❌ 失败: ${failures} 个文件</p><p>⏱️ 耗时: ${totalTime} 秒</p>${!processStateManager.isStopRequested() ? '<p>📢 请手动刷新页面查看结果</p>' : ''}</div>`;
            uiManager.updateModalContent(summary); uiManager.enableModalCloseButton(true);
        }
    };

    // --- UI Manager (simplified for brevity, assuming mostly unchanged from previous version) ---
    const uiManager = {
        modalElement: null, dropdownMenuElement: null, STYLE_ID: 'fastlink-dynamic-styles', MODAL_CONTENT_ID: 'fastlink-modal-content-area',
        applyStyles: function() { /* ... (exact same as before) ... */ 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-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}`); },
        createDropdownButton: function() { /* ... (exact same as before) ... */ 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'; 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-receiveDirect" class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" style="padding: 5px 12px;">链接转存</li><li id="fastlink-closeMenu" 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-closeMenu').addEventListener('click', (e) => { e.stopPropagation(); dropdownMenu.style.display = 'none'; }); dropdownMenu.querySelector('#fastlink-generateShare').addEventListener('click', async (e) => { e.stopPropagation(); dropdownMenu.style.display = 'none'; await coreLogic.generateShareLink(); }); dropdownMenu.querySelector('#fastlink-receiveDirect').addEventListener('click', (e) => { e.stopPropagation(); dropdownMenu.style.display = 'none'; this.showModal("粘贴秒传链接转存", "", 'inputLink'); }); 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) { /* ... (Mostly same, check button IDs) ... */ 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 += `<textarea class="fastlink-modal-input" placeholder="粘贴秒传链接...">${content|| ''}</textarea>`; 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 === 'showLink') { htmlContent += `<button id="fl-m-copy" class="copy-btn">复制</button><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 { 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()}`); if(confirmBtn){ confirmBtn.onclick = async () => { const linkInput = this.modalElement.querySelector(`.fastlink-modal-input`); const link = linkInput ? linkInput.value : null; if (link) { confirmBtn.disabled = true; if(cancelBtn) cancelBtn.disabled = true; await coreLogic.transferFromShareLink(link); if(this.modalElement){ confirmBtn.disabled = false; if(cancelBtn) cancelBtn.disabled = false;}} else this.showAlert("请输入链接"); };} 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(cancelBtn && (closable || type === 'progress_stoppable')){ cancelBtn.onclick = () => this.hideModal(); } if(stopBtn){ stopBtn.onclick = () => processStateManager.requestStop(); } if(!closable && cancelBtn && type !== 'progress_stoppable') cancelBtn.disabled = true; },
        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; } },
        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() { /* ... (exact same as before) ... */ 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; if (uiManager.createDropdownButton()) return; } if (loadAttempts < maxAttempts) { const delay = loadAttempts < 3 ? 1500 : 3000; setTimeout(tryAddButton, delay); } else console.warn(`[${SCRIPT_NAME}] 达到最大尝试次数,未能添加按钮。`); } const observer = new MutationObserver((mutations, obs) => { const targetAreaExists = !!document.querySelector(DOM_SELECTORS.TARGET_BUTTON_AREA); const ourButtonExists = !!document.querySelector('.fastlink-main-button-container'); if (targetAreaExists && !ourButtonExists) { loadAttempts = 0; setTimeout(tryAddButton, 700); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); setTimeout(tryAddButton, 500); }
    if (document.readyState === 'complete' || document.readyState === 'interactive') setTimeout(initialize, 300); else window.addEventListener('DOMContentLoaded', () => setTimeout(initialize, 300));

})();