您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
相较于原版本,增加了公共资源库和资源共享计划。重要提示:由于作者不会写Tampermonkey脚本,本脚本由AI生成,作者不保证后续维护的及时性。
当前为
// ==UserScript== // @name 123云盘秒传链接(with 123Pan-Unlimited-Share) // @namespace http://tampermonkey.net/ // @version v1.3.1-mod-v3 // @description 相较于原版本,增加了公共资源库和资源共享计划。重要提示:由于作者不会写Tampermonkey脚本,本脚本由AI生成,作者不保证后续维护的及时性。 // @author Gemini // @match *://*.123pan.com/* // @match *://*.123pan.cn/* // @match *://*.123865.com/* // @match *://*.123684.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=123pan.com // @license MIT // @grant GM_setClipboard // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // ==/UserScript== (function() { 'use strict'; // --- Constants and Configuration --- const SCRIPT_NAME = "123FastLink(with 123Pan-Unlimited-Share)"; const SCRIPT_VERSION = "v1.3.1-mod-v3"; // 修复指定文件夹转存问题 const LEGACY_FOLDER_LINK_PREFIX_V1 = "123FSLinkV1$"; const COMMON_PATH_LINK_PREFIX_V1 = "123FLCPV1$"; const LEGACY_FOLDER_LINK_PREFIX_V2 = "123FSLinkV2$"; const COMMON_PATH_LINK_PREFIX_V2 = "123FLCPV2$"; const COMMON_PATH_DELIMITER = "%"; const BASE62_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const API_PATHS = { UPLOAD_REQUEST: "/b/api/file/upload_request", LIST_NEW: "/b/api/file/list/new", FILE_INFO: "/b/api/file/info", SHARE_LIST: "/b/api/share/get" }; // 公共资源库 API 端点 (相对路径) const PUBLIC_REPO_API_PATHS = { LIST_PUBLIC_SHARES: "/api/list_public_shares", SEARCH_DATABASE: "/api/search_database", GET_CONTENT_TREE: "/api/get_content_tree", GET_SHARE_CODE: "/api/get_sharecode", SUBMIT_DATABASE: "/api/submit_database", TRANSFORM_TO_123FL: "/api/transformShareCodeTo123FastLinkJson", TRANSFORM_FROM_123FL: "/api/transform123FastLinkJsonToShareCode" }; const PUBLIC_REPO_BASE_URL_KEY = "fastlink_public_repo_base_url"; const DEFAULT_PUBLIC_REPO_BASE_URL = "https://pan.whose.icu"; // 注意:不保证稳定性,已设置不对中国大陆IP用户提供服务 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: 200 }; const FILTER_CONFIG = { STORAGE_KEY: 'fastlink_filter_settings', DEFAULT_FILTERS: [ { ext: 'nfo', name: '电影信息文件', emoji: '📝', enabled: true }, { ext: 'jpg', name: '图片文件', emoji: '🖼️', enabled: true }, { ext: 'jpeg', name: '图片文件', emoji: '🖼️', enabled: false }, { ext: 'png', name: '图片文件', emoji: '🖼️', enabled: true }, { ext: 'gif', name: '动图文件', emoji: '🎞️', enabled: false }, { ext: 'bmp', name: '图片文件', emoji: '🖼️', enabled: false }, { ext: 'webp', name: '图片文件', emoji: '🖼️', enabled: false }, { ext: 'tif', name: '图片文件', emoji: '🖼️', enabled: false }, { ext: 'tiff', name: '图片文件', emoji: '🖼️', enabled: false }, { ext: 'txt', name: '文本文件', emoji: '📄', enabled: false }, { ext: 'srt', name: '字幕文件', emoji: '💬', enabled: false }, { ext: 'ass', name: '字幕文件', emoji: '💬', enabled: false }, { ext: 'ssa', name: '字幕文件', emoji: '💬', enabled: false }, { ext: 'vtt', name: '字幕文件', emoji: '💬', enabled: false }, { ext: 'sub', name: '字幕文件', emoji: '💬', enabled: false }, { ext: 'idx', name: '字幕索引', emoji: '🔍', enabled: false }, { ext: 'xml', name: 'XML文件', emoji: '🔧', enabled: false }, { ext: 'html', name: '网页文件', emoji: '🌐', enabled: false }, { ext: 'htm', name: '网页文件', emoji: '🌐', enabled: false }, { ext: 'url', name: '网址链接', emoji: '🔗', enabled: false }, { ext: 'lnk', name: '快捷方式', emoji: '🔗', enabled: false }, { ext: 'pdf', name: 'PDF文档', emoji: '📑', enabled: false }, { ext: 'doc', name: 'Word文档', emoji: '📘', enabled: false }, { ext: 'docx', name: 'Word文档', emoji: '📘', enabled: false }, { ext: 'xls', name: 'Excel表格', emoji: '📊', enabled: false }, { ext: 'xlsx', name: 'Excel表格', emoji: '📊', enabled: false }, { ext: 'ppt', name: 'PPT演示', emoji: '📽️', enabled: false }, { ext: 'pptx', name: 'PPT演示', emoji: '📽️', enabled: false }, { ext: 'md', name: 'Markdown文件', emoji: '📝', enabled: false }, { ext: 'torrent', name: '种子文件', emoji: '🧲', enabled: false }, ], DEFAULT_FILTER_OPTIONS: { filterOnShareEnabled: true, filterOnTransferEnabled: true, } }; // --- Helper: GM_xmlhttpRequest Wrapper --- function gmXmlHttpRequestPromise(details) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ ...details, onload: (response) => { try { const responseData = JSON.parse(response.responseText); resolve(responseData); } catch (e) { console.error(`[${SCRIPT_NAME}] API响应JSON解析失败:`, response.responseText, e); reject(new Error(`API响应JSON解析失败: ${e.message}. 响应文本: ${response.responseText.substring(0,100)}`)); } }, onerror: (error) => { console.error(`[${SCRIPT_NAME}] API请求错误:`, error); reject(new Error(`API请求错误: ${error.statusText || '网络错误'}`)); }, ontimeout: () => { console.error(`[${SCRIPT_NAME}] API请求超时`); reject(new Error('API请求超时')); } }); }); } // --- 公共资源库 API 调用封装 --- const publicRepoApiHelper = { getBaseUrl: () => GM_getValue(PUBLIC_REPO_BASE_URL_KEY, DEFAULT_PUBLIC_REPO_BASE_URL), setBaseUrl: (url) => GM_setValue(PUBLIC_REPO_BASE_URL_KEY, url), _call: async function(method, endpointPath, data = null, timeoutMs=15000) { const baseUrl = this.getBaseUrl(); if (!baseUrl.startsWith("http") || !baseUrl.endsWith("/")) { throw new Error("公共资源库服务器URL格式不正确,请在设置中修改。"); } const url = new URL(endpointPath.startsWith('/') ? endpointPath.substring(1) : endpointPath, baseUrl).toString(); const details = { method: method, url: url, headers: { "Content-Type": "application/json" }, timeout: timeoutMs, }; if (data) { details.data = JSON.stringify(data); } try { return await gmXmlHttpRequestPromise(details); } catch (e) { console.error(`[${SCRIPT_NAME}] 公共资源库API调用失败 (${method} ${url}):`, e); // 尝试从GM_xmlhttpRequest的错误中提取更具体的信息 let errorMessage = e.message; if (e.message && e.message.includes("API响应JSON解析失败") && e.message.includes("响应文本:")) { errorMessage = `请求失败:服务器返回了无法解析的内容。`; } else if (e.message && e.message.includes("网络错误")) { errorMessage = `请求失败:网络连接问题或服务器无响应。`; } else if (e.message && e.message.includes("超时")) { errorMessage = `请求失败:连接超时。`; } throw new Error(errorMessage); // 重新抛出错误,以便上层处理 } }, listPublicShares: async function(page = 1) { return this._call("GET", `${PUBLIC_REPO_API_PATHS.LIST_PUBLIC_SHARES}?page=${page}`); }, searchDatabase: async function(rootFolderName, page = 1) { return this._call("POST", PUBLIC_REPO_API_PATHS.SEARCH_DATABASE, { rootFolderName, page }); }, getContentTree: async function(params) { // params: { codeHash } or { shareCode } return this._call("POST", PUBLIC_REPO_API_PATHS.GET_CONTENT_TREE, params); }, getShareCode: async function(codeHash) { return this._call("POST", PUBLIC_REPO_API_PATHS.GET_SHARE_CODE, { codeHash }); }, transformShareCodeTo123FastLinkJson: async function(shareCode, rootFolderName) { return this._call("POST", PUBLIC_REPO_API_PATHS.TRANSFORM_TO_123FL, { shareCode, rootFolderName }); }, transform123FastLinkJsonToShareCode: async function(fastLinkJsonString, generateShortCode, shareProject) { return this._call("POST", PUBLIC_REPO_API_PATHS.TRANSFORM_FROM_123FL, { "123FastLinkJson": fastLinkJsonString, generateShortCode, shareProject }); } }; const filterManager = { filters: [], filterOnShareEnabled: true, filterOnTransferEnabled: true, init: function() { this.loadSettings(); }, loadSettings: function() { try { const savedSettings = GM_getValue(FILTER_CONFIG.STORAGE_KEY); if (savedSettings) { const parsedSettings = JSON.parse(savedSettings); if (Array.isArray(parsedSettings.filters)) this.filters = parsedSettings.filters; else { this.filters = parsedSettings; this.filterOnShareEnabled = FILTER_CONFIG.DEFAULT_FILTER_OPTIONS.filterOnShareEnabled; this.filterOnTransferEnabled = FILTER_CONFIG.DEFAULT_FILTER_OPTIONS.filterOnTransferEnabled; } if (typeof parsedSettings.filterOnShareEnabled === 'boolean') this.filterOnShareEnabled = parsedSettings.filterOnShareEnabled; if (typeof parsedSettings.filterOnTransferEnabled === 'boolean') this.filterOnTransferEnabled = parsedSettings.filterOnTransferEnabled; console.log(`[${SCRIPT_NAME}] 已加载过滤器设置`); } else this.resetToDefaults(); } catch (e) { console.error(`[${SCRIPT_NAME}] 加载过滤器设置失败:`, e); this.resetToDefaults(); } }, saveSettings: function() { try { GM_setValue(FILTER_CONFIG.STORAGE_KEY, JSON.stringify({ filters: this.filters, filterOnShareEnabled: this.filterOnShareEnabled, filterOnTransferEnabled: this.filterOnTransferEnabled })); return true; } catch (e) { console.error(`[${SCRIPT_NAME}] 保存过滤器设置失败:`, e); return false; } }, resetToDefaults: function() { this.filters = JSON.parse(JSON.stringify(FILTER_CONFIG.DEFAULT_FILTERS)); this.filterOnShareEnabled = FILTER_CONFIG.DEFAULT_FILTER_OPTIONS.filterOnShareEnabled; this.filterOnTransferEnabled = FILTER_CONFIG.DEFAULT_FILTER_OPTIONS.filterOnTransferEnabled; console.log(`[${SCRIPT_NAME}] 已重置为默认过滤器设置`); }, shouldFilterFile: function(fileName, isShareOperation = true) { if ((isShareOperation && !this.filterOnShareEnabled) || (!isShareOperation && !this.filterOnTransferEnabled)) return false; if (!fileName) return false; const lastDotIndex = fileName.lastIndexOf('.'); if (lastDotIndex === -1) return false; const extension = fileName.substring(lastDotIndex + 1).toLowerCase(); const filter = this.filters.find(f => f.ext.toLowerCase() === extension); return filter && filter.enabled; }, getFilteredCount: function() { return this.filters.filter(f => f.enabled).length; }, setAllFilters: function(enabled) { this.filters.forEach(filter => filter.enabled = enabled); }, buildFilterModalContent: function() { let html = ` <div class="filter-global-switches"> <div class="filter-switch-item"> <input type="checkbox" id="fl-filter-share-toggle" class="filter-toggle-checkbox" ${this.filterOnShareEnabled ? 'checked' : ''}> <label for="fl-filter-share-toggle"><span class="filter-emoji">🔗</span><span class="filter-name">生成分享链接时启用过滤</span></label> </div> <div class="filter-switch-item"> <input type="checkbox" id="fl-filter-transfer-toggle" class="filter-toggle-checkbox" ${this.filterOnTransferEnabled ? 'checked' : ''}> <label for="fl-filter-transfer-toggle"><span class="filter-emoji">📥</span><span class="filter-name">转存链接/文件时启用过滤</span></label> </div> </div> <hr class="filter-divider"> <div class="filter-description"> <p>管理要过滤的文件类型。启用过滤后,相应类型的文件将不会包含在生成的链接或转存操作中。</p> </div> <div class="filter-select-style-container"> <div id="fl-selected-filter-tags" class="filter-selected-tags"></div> <input type="text" id="fl-filter-search-input" class="filter-search-input" placeholder="输入扩展名 (如: jpg) 或名称添加/搜索..."> <div id="fl-filter-dropdown" class="filter-dropdown"></div> </div> <div class="filter-controls" style="margin-top: 15px;"> <button id="fl-filter-select-all" class="filter-btn">✅ 全选</button> <button id="fl-filter-select-none" class="filter-btn">❌ 全不选</button> <button id="fl-filter-reset" class="filter-btn">🔄 恢复默认</button> </div>`; return html; }, renderFilterItems: function() { try { const modal = uiManager.getModalElement(); if (!modal) { console.warn(`[${SCRIPT_NAME}] Filter settings: renderFilterItems called but no modal element found.`); return; } const selectedTagsContainer = modal.querySelector('#fl-selected-filter-tags'); const dropdown = modal.querySelector('#fl-filter-dropdown'); const searchInput = modal.querySelector('#fl-filter-search-input'); if (!selectedTagsContainer || !dropdown) { console.warn(`[${SCRIPT_NAME}] Filter settings: renderFilterItems missing critical elements (tagsContainer or dropdown).`); return; } selectedTagsContainer.innerHTML = ''; dropdown.innerHTML = ''; const searchTerm = searchInput ? searchInput.value.trim().toLowerCase() : ""; this.filters.forEach((filter, index) => { if (filter.enabled) { const tag = document.createElement('div'); tag.className = 'filter-tag'; tag.dataset.index = index; tag.innerHTML = `<span class="filter-emoji">${filter.emoji}</span><span class="filter-tag-text">.${filter.ext}</span><span class="filter-tag-name">(${filter.name})</span><span class="filter-tag-remove">×</span>`; tag.querySelector('.filter-tag-remove')?.addEventListener('click', () => { this.filters[index].enabled = false; this.renderFilterItems(); }); selectedTagsContainer.appendChild(tag); } else { const filterText = `.${filter.ext} ${filter.name}`.toLowerCase(); if (searchTerm && !filter.ext.toLowerCase().includes(searchTerm) && !filterText.includes(searchTerm)) return; const item = document.createElement('div'); item.className = 'filter-dropdown-item'; item.dataset.index = index; item.innerHTML = `<span class="filter-emoji">${filter.emoji}</span><span class="filter-ext">.${filter.ext}</span><span class="filter-name">${filter.name}</span>`; item.addEventListener('click', () => { this.filters[index].enabled = true; if (searchInput) searchInput.value = ''; this.renderFilterItems(); }); dropdown.appendChild(item); } }); dropdown.style.display = dropdown.children.length > 0 && (document.activeElement === searchInput || dropdown.matches(':hover')) ? 'block' : 'none'; } catch (e) { console.error(`[${SCRIPT_NAME}] CRITICAL ERROR in renderFilterItems:`, e); // Optionally re-throw or handle by showing a specific error to the user via uiManager } }, attachFilterEvents: function() { try { const modal = uiManager.getModalElement(); if (!modal) { console.warn(`[${SCRIPT_NAME}] Filter settings: attachFilterEvents called but no modal element found.`); return; } console.log(`[${SCRIPT_NAME}] Filter settings: Attaching events to modal.`); this.renderFilterItems(); const searchInput = modal.querySelector('#fl-filter-search-input'); const dropdown = modal.querySelector('#fl-filter-dropdown'); if (searchInput && dropdown) { searchInput.addEventListener('input', () => this.renderFilterItems()); searchInput.addEventListener('focus', () => { if (dropdown.children.length > 0) dropdown.style.display = 'block'; }); searchInput.addEventListener('blur', () => setTimeout(() => { if (!dropdown.matches(':hover')) dropdown.style.display = 'none'; }, 200)); searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && searchInput.value.trim() !== '') { e.preventDefault(); const term = searchInput.value.trim().toLowerCase().replace(/^\\./, ''); // Remove leading dot if (!term || !/^[a-z0-9_]+$/.test(term)) { // Basic validation for new extension uiManager.showAlert("无效的扩展名格式。请只使用字母、数字和下划线。", 1500); return; } const matchedIndex = this.filters.findIndex(f => f.ext.toLowerCase() === term); if (matchedIndex !== -1) { // Existing filter if (!this.filters[matchedIndex].enabled) { this.filters[matchedIndex].enabled = true; searchInput.value = ''; this.renderFilterItems(); } else { uiManager.showAlert(`扩展名 ".${term}" 已经启用。`, 1500); } } else { // New filter this.filters.push({ ext: term, name: '自定义类型', emoji: '✨', enabled: true }); this.filters.sort((a, b) => a.ext.localeCompare(b.ext)); searchInput.value = ''; this.renderFilterItems(); uiManager.showAlert(`已添加并启用自定义过滤器 ".${term}"。`, 1500); } } }); dropdown.addEventListener('mouseenter', () => dropdown.dataset.hover = "true"); dropdown.addEventListener('mouseleave', () => { delete dropdown.dataset.hover; if (document.activeElement !== searchInput) dropdown.style.display = 'none'; }); } const shareToggle = modal.querySelector('#fl-filter-share-toggle'); if (shareToggle) shareToggle.addEventListener('change', () => { this.filterOnShareEnabled = shareToggle.checked; }); const transferToggle = modal.querySelector('#fl-filter-transfer-toggle'); if (transferToggle) transferToggle.addEventListener('change', () => { this.filterOnTransferEnabled = transferToggle.checked; }); modal.querySelector('#fl-filter-select-all')?.addEventListener('click', () => { this.setAllFilters(true); this.renderFilterItems(); }); modal.querySelector('#fl-filter-select-none')?.addEventListener('click', () => { this.setAllFilters(false); this.renderFilterItems(); }); modal.querySelector('#fl-filter-reset')?.addEventListener('click', () => { this.resetToDefaults(); if (shareToggle) shareToggle.checked = this.filterOnShareEnabled; if (transferToggle) transferToggle.checked = this.filterOnTransferEnabled; this.renderFilterItems(); }); console.log(`[${SCRIPT_NAME}] Filter settings: Events attached successfully.`); } catch (e) { console.error(`[${SCRIPT_NAME}] CRITICAL ERROR in attachFilterEvents:`, e); uiManager.showError("加载过滤器设置时发生严重错误,可能部分功能无法使用。请尝试刷新页面。", 5000); } } }; const apiHelper = { buildURL: (host, path, queryParams = {}) => { const queryString = new URLSearchParams(queryParams).toString(); return `${host}${path}${queryString ? '?' + queryString : ''}`; }, sendRequest: async function(method, path, queryParams = {}, body = null, isPublicCall = false) { const config = { host: 'https://' + window.location.host, authToken: localStorage['authorToken'], loginUuid: localStorage['LoginUuid'], appVersion: '3', referer: document.location.href, }; const headers = { 'Content-Type': 'application/json;charset=UTF-8', 'platform': 'web', 'App-Version': config.appVersion, 'Origin': config.host, 'Referer': config.referer, }; if (!isPublicCall) { if (config.authToken) headers['Authorization'] = 'Bearer ' + config.authToken; if (config.loginUuid) headers['LoginUuid'] = config.loginUuid; } try { const urlToFetch = this.buildURL(config.host, path, queryParams); const response = await fetch(urlToFetch, { method, headers, body: body ? JSON.stringify(body) : null, credentials: 'include' }); const responseText = await response.text(); let responseData; try { responseData = JSON.parse(responseText); } catch (e) { if (!response.ok) throw new Error(`❗ HTTP ${response.status}: ${responseText || response.statusText}`); throw new Error(`❗ 响应解析JSON失败: ${e.message}`); } if (responseData.code !== 0) { const message = responseData.message || 'API业务逻辑错误'; const apiError = new Error(`❗ ${message}`); if (typeof message === 'string' && (message.includes("频繁") || message.includes("操作过快") || message.includes("rate limit") || message.includes("too many requests"))) apiError.isRateLimit = true; throw apiError; } return responseData; } catch (error) { if (!error.isRateLimit && !error.message?.startsWith("UserStopped")) { /* Log non-rate-limit, non-user-stopped errors */ } throw error; } }, createFolder: async function(parentId, folderName) { return coreLogic._executeApiWithRetries(() => this._createFolderInternal(parentId, folderName), `创建文件夹: ${folderName}`, coreLogic.currentOperationRateLimitStatus); }, _createFolderInternal: async function(parentId, folderName) { if (parentId === undefined || parentId === null || isNaN(parseInt(parentId))) { throw new Error(`创建文件夹 "${folderName}" 失败:父文件夹ID无效 (${parentId})。`); } const requestBody = { driveId: 0, etag: "", fileName: folderName, parentFileId: parseInt(parentId, 10), size: 0, type: 1, NotReuse: true, RequestSource: null, duplicate: 1, event: "newCreateFolder", operateType: 1 }; const responseData = await this.sendRequest("POST", API_PATHS.UPLOAD_REQUEST, {}, requestBody); if (responseData?.data?.Info?.FileId !== undefined) return responseData.data.Info; throw new Error('创建文件夹失败或API响应缺少FileId'); }, listDirectoryContents: async function(parentId, limit = 100) { return coreLogic._executeApiWithRetries(() => this._listDirectoryContentsInternal(parentId, limit), `列出目录ID: ${parentId}`, coreLogic.currentOperationRateLimitStatus); }, _listDirectoryContentsInternal: async function(parentId, limit = 100) { if (parentId === undefined || parentId === null || isNaN(parseInt(parentId))) { throw new Error(`无效的文件夹ID: ${parentId},无法列出内容。`); } let allItems = []; let nextMarker = "0"; let currentPage = 1; 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响应异常'); }, listSharedDirectoryContents: async function(parentId, shareKey, sharePwd, limit = 100) { return coreLogic._executeApiWithRetries( () => this._listSharedDirectoryContentsInternal(parentId, shareKey, sharePwd, limit), `列出分享目录ID: ${parentId} (ShareKey: ${shareKey.substring(0,4)}...)`, coreLogic.currentOperationRateLimitStatus, true ); }, _listSharedDirectoryContentsInternal: async function(parentId, shareKey, sharePwd, limit = 100) { if (parentId === undefined || parentId === null || isNaN(parseInt(parentId))) throw new Error(`无效的分享文件夹ID: ${parentId},无法列出内容。`); if (!shareKey) throw new Error("ShareKey 不能为空。"); let allItems = []; let nextMarker = "0"; let currentPage = 1; do { const queryParams = { limit: limit, next: nextMarker, orderBy: "file_name", orderDirection: "asc", parentFileId: parseInt(parentId, 10), Page: currentPage, shareKey: shareKey, }; if (sharePwd) queryParams.SharePwd = sharePwd; const responseData = await this.sendRequest("GET", API_PATHS.SHARE_LIST, queryParams, null, true); if (responseData?.data?.InfoList) { const newItems = responseData.data.InfoList.map(item => ({ FileID: parseInt(item.FileId, 10) || NaN, FileName: item.FileName || "Unknown", Type: parseInt(item.Type, 10) || 0, Size: parseInt(item.Size, 10) || 0, Etag: item.Etag || "", ParentFileID: parseInt(item.ParentFileId, 10) })); allItems = allItems.concat(newItems); nextMarker = responseData.data.Next; currentPage++; } else { if (currentPage === 1 && !responseData?.data?.InfoList && responseData.message && responseData.code !== 0) throw new Error(`API错误: ${responseData.message}`); nextMarker = "-1"; } } while (nextMarker !== "-1" && nextMarker !== null && nextMarker !== undefined && String(nextMarker).trim() !== ""); return allItems; }, }; const processStateManager = { _userRequestedStop: false, _modalStopButtonId: 'fl-modal-stop-btn', // Keep track of last known progress to update mini bar instantly if needed _lastProgressData: { processed: 0, total: 0, successes: 0, failures: 0, currentFileName: "", extraStatus: "" }, reset: function() { this._userRequestedStop = false; const btn = document.getElementById(this._modalStopButtonId); if(btn){btn.textContent = "🛑 停止"; btn.disabled = false;} // Reset mini progress title too if (uiManager.miniProgressElement) { const miniTitle = uiManager.miniProgressElement.querySelector('.fastlink-mini-progress-title span'); if (miniTitle) miniTitle.textContent = "⚙️ 处理中..."; } }, requestStop: function() { this._userRequestedStop = true; const btn = document.getElementById(this._modalStopButtonId); if(btn){btn.textContent = "正在停止..."; btn.disabled = true;} const minimizeBtn = document.getElementById('fl-m-minimize'); if(minimizeBtn) minimizeBtn.disabled = true; console.log(`[${SCRIPT_NAME}] User requested stop.`); // Update mini progress title if active if (uiManager.isMiniProgressActive && uiManager.miniProgressElement) { const miniTitle = uiManager.miniProgressElement.querySelector('.fastlink-mini-progress-title span'); if (miniTitle) miniTitle.textContent = "🛑 正在停止..."; } }, isStopRequested: function() { return this._userRequestedStop; }, getStopButtonId: function() { return this._modalStopButtonId; }, updateProgressUINow: function() { // Added to directly call update with last known data this.updateProgressUI( this._lastProgressData.processed, this._lastProgressData.total, this._lastProgressData.successes, this._lastProgressData.failures, this._lastProgressData.currentFileName, this._lastProgressData.extraStatus ); }, updateProgressUI: function(processed, total, successes, failures, currentFileName, extraStatus = "") { // Store last data this._lastProgressData = { processed, total, successes, failures, currentFileName, extraStatus }; const bar = document.querySelector('.fastlink-progress-bar'); if (bar) bar.style.width = `${total > 0 ? Math.round((processed / total) * 100) : 0}%`; const statTxt = document.querySelector('.fastlink-status p:first-child'); if (statTxt) statTxt.textContent = `处理中: ${processed} / ${total} 项 (预估)`; const sucCnt = document.querySelector('.fastlink-stats .success-count'); if (sucCnt) sucCnt.textContent = `✅ 成功:${successes}`; const failCnt = document.querySelector('.fastlink-stats .failed-count'); if (failCnt) failCnt.textContent = `❌ 失败:${failures}`; const curFile = document.querySelector('.fastlink-current-file .file-name'); if (curFile) curFile.textContent = currentFileName ? `📄 ${currentFileName}` : "准备中..."; const extraEl = document.querySelector('.fastlink-status .extra-status-message'); if (extraEl) { extraEl.textContent = extraStatus; extraEl.style.display = extraStatus ? 'block' : 'none';} // Update mini progress bar if active if (uiManager.isMiniProgressActive && uiManager.miniProgressElement) { const miniBar = uiManager.miniProgressElement.querySelector('.fastlink-mini-progress-bar'); if (miniBar) miniBar.style.width = `${total > 0 ? Math.round((processed / total) * 100) : 0}%`; const miniFile = uiManager.miniProgressElement.querySelector('.fastlink-mini-progress-file'); if (miniFile) miniFile.textContent = currentFileName ? (currentFileName.length > 30 ? currentFileName.substring(0, 27) + "..." : currentFileName) : "准备中..."; const miniStatus = uiManager.miniProgressElement.querySelector('.fastlink-mini-progress-status'); if (miniStatus) miniStatus.textContent = `${processed}/${total} (✅${successes} ❌${failures})`; const miniTitle = uiManager.miniProgressElement.querySelector('.fastlink-mini-progress-title span'); if (miniTitle) { if (this._userRequestedStop) { miniTitle.textContent = (processed < total) ? "🛑 正在停止..." : "🛑 已停止"; } else if (processed >= total && total > 0) { miniTitle.textContent = "✅ 处理完成"; } else { miniTitle.textContent = "⚙️ 处理中..."; } } } }, appendLogMessage: function(message, isError = false) { const logArea = document.querySelector('.fastlink-status'); console.log(`[${SCRIPT_NAME}] appendLogMessage: 尝试记录: "${message}"`, "错误?", isError, "日志区域存在?", !!logArea); if (logArea) { const p = document.createElement('p'); p.className = isError ? 'error-message' : 'info-message'; p.innerHTML = message; // 使用 innerHTML 以支持可能的 HTML 标签(如加粗) const extraStatusSibling = logArea.querySelector('.extra-status-message'); if (extraStatusSibling) logArea.insertBefore(p, extraStatusSibling.nextSibling); else logArea.appendChild(p); logArea.scrollTop = logArea.scrollHeight; } else { console.error(`[${SCRIPT_NAME}] appendLogMessage: 日志区域 '.fastlink-status' 未找到! 无法记录: "${message}"`); } } }; const coreLogic = { currentOperationRateLimitStatus: { consecutiveRateLimitFailures: 0 }, _executeApiWithRetries: async function(apiFunctionExecutor, itemNameForLog, rateLimitStatusRef, isPublicCallForSendRequest = false) { let generalErrorRetries = 0; while (generalErrorRetries <= RETRY_AND_DELAY_CONFIG.GENERAL_API_MAX_RETRIES) { if (processStateManager.isStopRequested()) throw new Error("UserStopped"); let rateLimitRetriesForCurrentGeneralAttempt = 0; while (rateLimitRetriesForCurrentGeneralAttempt <= RETRY_AND_DELAY_CONFIG.RATE_LIMIT_MAX_ITEM_RETRIES) { if (processStateManager.isStopRequested()) throw new Error("UserStopped"); try { const result = await apiFunctionExecutor(); rateLimitStatusRef.consecutiveRateLimitFailures = 0; 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 || itemNameForLog}: 操作频繁 (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: () => Array.from(document.querySelectorAll(DOM_SELECTORS.FILE_ROW_SELECTOR)).filter(row => (row.querySelector(DOM_SELECTORS.FILE_CHECKBOX_SELECTOR) || {}).checked).map(row => String(row.getAttribute('data-row-key'))).filter(id => id != null), getCurrentDirectoryId: () => { const url = window.location.href; const homeFilePathMatch = url.match(/[?&]homeFilePath=([^&]*)/); if (homeFilePathMatch) { let filePathIds = homeFilePathMatch[1]; if (filePathIds && filePathIds !== "") { if (filePathIds.includes(',')) { const idsArray = filePathIds.split(','); return idsArray[idsArray.length - 1]; } else { return filePathIds; } } else { return "0"; } } const regexes = [ /fid=(\d+)/, /#\/list\/folder\/(\d+)/, /\/drive\/(?:folder\/)?(\d+)/, /\/s\/[a-zA-Z0-9_-]+\/(\d+)/, /(?:\/|^)(\d+)(?=[\/?#]|$)/ ]; for (const regex of regexes) { const match = url.match(regex); if (match && match[1]) { if (match[1] === "0") { if (regex.source === String(/\/drive\/(?:folder\/)?(\d+)/) && url.includes("/drive/0")) return "0"; } return match[1]; } } const lowerUrl = url.toLowerCase(); if (lowerUrl.includes("/drive/0") || lowerUrl.endsWith("/drive") || lowerUrl.endsWith("/drive/") || lowerUrl.match(/^https?:\/\/[^\/]+\/?([#?].*)?$/) || lowerUrl.endsWith(".123pan.com") || lowerUrl.endsWith(".123pan.cn") || lowerUrl.endsWith(".123pan.com/") || lowerUrl.endsWith(".123pan.cn/")) return "0"; try { const pathname = new URL(url).pathname; if (pathname === '/' || pathname.toLowerCase() === '/drive/' || pathname.toLowerCase() === '/index.html') return "0"; } catch(e) { /*ignore*/ } return "0"; }, _findLongestCommonPrefix: function(paths) { if (!paths || paths.length === 0) return ""; if (paths.length === 1 && paths[0].includes('/')) { const lastSlash = paths[0].lastIndexOf('/'); if (lastSlash > -1) return paths[0].substring(0, lastSlash + 1); return ""; } if (paths.length === 1 && !paths[0].includes('/')) return ""; const sortedPaths = [...paths].sort(); const firstPath = sortedPaths[0]; const lastPath = sortedPaths[sortedPaths.length - 1]; let i = 0; while (i < firstPath.length && firstPath.charAt(i) === lastPath.charAt(i)) i++; let prefix = firstPath.substring(0, i); if (prefix.includes('/')) prefix = prefix.substring(0, prefix.lastIndexOf('/') + 1); else { if (!paths.every(p => p === prefix || p.startsWith(prefix + "/"))) return "";} return (prefix.length > 1 && prefix.endsWith('/')) ? prefix : ""; }, _generateLinkProcess: async function(itemFetcherAsyncFn, operationTitleForUI) { processStateManager.reset(); this.currentOperationRateLimitStatus.consecutiveRateLimitFailures = 0; let allFileEntriesData = []; let processedAnyFolder = false; let totalDiscoveredItemsForProgress = 0; let itemsProcessedForProgress = 0; let successes = 0, failures = 0; let jsonDataForExport = null; const startTime = Date.now(); let permanentlyFailedItemsFromFetcher = []; uiManager.showModal(operationTitleForUI, ` <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); processStateManager.appendLogMessage("🚀 [LOG_TEST] _generateLinkProcess: 日志系统准备就绪。模态框已显示。"); // Initial log test try { const result = await itemFetcherAsyncFn( (itemData) => { allFileEntriesData.push(itemData); }, (isFolder) => { if(isFolder) processedAnyFolder = true; }, (progressUpdate) => { if (progressUpdate.total !== undefined) totalDiscoveredItemsForProgress = progressUpdate.total; if (progressUpdate.processed !== undefined) itemsProcessedForProgress = progressUpdate.processed; if (progressUpdate.successCount !== undefined) successes = progressUpdate.successCount; if (progressUpdate.failureCount !== undefined) failures = progressUpdate.failureCount; processStateManager.updateProgressUI(itemsProcessedForProgress, totalDiscoveredItemsForProgress, successes, failures, progressUpdate.currentFile, progressUpdate.extraStatus); } ); // Ensure final counts are taken from the result of the fetcher totalDiscoveredItemsForProgress = result.totalDiscoveredItemsForProgress; itemsProcessedForProgress = result.itemsProcessedForProgress; successes = result.successes; failures = result.failures; if (result.permanentlyFailedItems) permanentlyFailedItemsFromFetcher = result.permanentlyFailedItems; // Get failed items } 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 (allFileEntriesData.length > 0 || permanentlyFailedItemsFromFetcher.length > 0) { // Consider failed items for showing results modal 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 && !link.startsWith(LEGACY_FOLDER_LINK_PREFIX_V2) && !link.startsWith(COMMON_PATH_LINK_PREFIX_V2)) link = LEGACY_FOLDER_LINK_PREFIX_V2 + link; } const commonPathForExport = (commonPrefix && (processedAnyFolder || allPaths.some(p => p.includes('/')))) ? commonPrefix : ""; jsonDataForExport = { scriptVersion: SCRIPT_VERSION, exportVersion: "1.0", usesBase62EtagsInExport: useV2Format, commonPath: commonPathForExport, files: allFileEntriesData.map(entry => ({ path: commonPathForExport ? entry.fullPath.substring(commonPathForExport.length) : entry.fullPath, size: String(entry.size), etag: useV2Format ? hexToOptimizedEtag(entry.etag).optimized : entry.etag })) }; if (processStateManager.isStopRequested()) processStateManager.appendLogMessage(`⚠️ 操作已停止。以下是已处理 ${allFileEntriesData.length} 项的部分链接/数据。`); 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); let titleMessage = failures > 0 && successes > 0 ? "🎯 部分成功" : (successes > 0 ? "🎉 生成成功" : "🤔 无有效数据"); if (processStateManager.isStopRequested()) titleMessage = "🔴 操作已停止 (部分数据)"; else if (successes === 0 && permanentlyFailedItemsFromFetcher.length > 0 && allFileEntriesData.length === 0) titleMessage = "😢 全部失败"; else if (successes > 0 && permanentlyFailedItemsFromFetcher.length > 0) titleMessage = "🎯 部分成功 (含失败项)"; summary = `<div class="fastlink-result"><p>📄 已处理项目 (用于链接/JSON): ${allFileEntriesData.length} 个</p><p>✅ 成功获取链接信息: ${successes} 个</p><p>❌ 失败/跳过项目 (元数据提取阶段): ${failures} 个</p><p>📋 永久失败项目 (无法处理): ${permanentlyFailedItemsFromFetcher.length} 个</p><p>💾 已处理项目总大小: ${formattedTotalSize}</p><p>⏱️ 耗时: ${totalTime} 秒</p><textarea class="fastlink-link-text" readonly>${link}</textarea></div>`; // Add failed items log if any if (permanentlyFailedItemsFromFetcher.length > 0) { summary += `<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>永久失败项目 (${permanentlyFailedItemsFromFetcher.length}):</h4><div id="fastlink-failures-list">`; permanentlyFailedItemsFromFetcher.forEach(pf => { summary += `<p style="margin:2px 0;">📄 <span style="font-weight:bold;">${pf.fileName || '未知文件'}</span> (ID: ${pf.id || 'N/A'}): <span style="color:red;">${pf.error || '未知错误'}</span></p>`; }); summary += `</div></div>`; } uiManager.showModal( titleMessage, // Use the already determined titleMessage summary, 'showLink', true, // Ensure closable is true for showLink type link, jsonDataForExport, permanentlyFailedItemsFromFetcher // Pass failed items to modal for potential button actions ); return link; } else { if (processStateManager.isStopRequested()) summary = `<div class="fastlink-result"><h3>🔴 操作已停止</h3><p>未收集到有效文件信息。</p><p>⏱️ 耗时: ${totalTime} 秒</p></div>`; else if (failures > 0 && successes === 0) summary = `<div class="fastlink-result"><h3>😢 生成失败</h3><p>未能提取有效文件信息 (${successes} 成功, ${failures} 失败)</p><p>⏱️ 耗时: ${totalTime} 秒</p></div>`; else summary = `<div class="fastlink-result"><h3>🤔 无有效文件</h3><p>未选中任何符合条件的文件,或文件夹为空,或所有可选文件均被过滤器排除。</p><p>⏱️ 耗时: ${totalTime} 秒</p></div>`; uiManager.updateModalContent(summary); uiManager.enableModalCloseButton(true); return ""; } }, generateShareLink: async function() { const selectedItemIds = this.getSelectedFileIds(); if (!selectedItemIds.length) { uiManager.showAlert("请先勾选要分享的文件或文件夹。"); return ""; } let permanentlyFailedItems = []; // Log selected items at the very beginning of the share link generation process console.log(`[${SCRIPT_NAME}] generateShareLink: 开始处理选中的ID:`, selectedItemIds); // Use a timeout to ensure modal is ready for appendLogMessage setTimeout(() => { processStateManager.appendLogMessage(`[generateShareLink] 检测到 ${selectedItemIds.length} 个选中的项目。`); }, 100); return this._generateLinkProcess(async (addDataCb, markFolderCb, progressCb) => { let totalDiscovered = selectedItemIds.length; let processedCount = 0; let successCount = 0; let failureCount = 0; async function processSingleItem(itemId, currentRelativePath, preFetchedDetails = null) { if (processStateManager.isStopRequested()) throw new Error("UserStopped"); processStateManager.appendLogMessage(`⚙️ [PSI_START] ID: ${itemId}, Path: '${currentRelativePath || 'ROOT'}', HasPrefetched: ${!!preFetchedDetails}`); if (preFetchedDetails) { processStateManager.appendLogMessage(`📄 [PSI_PREFETCHED_DETAILS] ID: ${itemId}, FID: ${preFetchedDetails.FileID}, Name: '${preFetchedDetails.FileName}', Type: ${preFetchedDetails.Type}, Size: ${preFetchedDetails.Size}, Etag: ${preFetchedDetails.Etag ? preFetchedDetails.Etag.substring(0,10)+'...' : 'N/A'}`); } let itemDetails = preFetchedDetails; const baseItemNameForLog = `${currentRelativePath || 'ROOT'}/${preFetchedDetails ? preFetchedDetails.FileName : itemId}`; if (!itemDetails) { progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: baseItemNameForLog, extraStatus: "获取信息..." }); 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]; processStateManager.appendLogMessage(`📄 [PSI_FETCHED_DETAILS] ID: ${itemId}, FID: ${itemDetails.FileID}, Name: '${itemDetails.FileName}', Type: ${itemDetails.Type}, Size: ${itemDetails.Size}, Etag: ${itemDetails.Etag ? itemDetails.Etag.substring(0,10)+'...' : 'N/A'}`); } catch (e) { if (processStateManager.isStopRequested()) throw e; failureCount++; processedCount++; // Count as processed because we attempted const errorMsg = `获取项目详情 '${baseItemNameForLog}' (ID: ${itemId}) 失败: ${e.message}`; processStateManager.appendLogMessage(`❌ [PSI_FETCH_FAIL] ${errorMsg}`); permanentlyFailedItems.push({ fileName: baseItemNameForLog, id: itemId, error: errorMsg }); progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: baseItemNameForLog, extraStatus: "获取信息失败" }); return; } } else { progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: baseItemNameForLog, extraStatus: "处理预取信息..." }); // Log details if pre-fetched, as they weren't logged by the block above processStateManager.appendLogMessage(`📄 [PSI_USING_PREFETCHED_DETAILS] ID: ${itemId}, FID: ${itemDetails.FileID}, Name: '${itemDetails.FileName}', Type: ${itemDetails.Type}, Size: ${itemDetails.Size}, Etag: ${itemDetails.Etag ? itemDetails.Etag.substring(0,10)+'...' : 'N/A'}`); } processStateManager.appendLogMessage(`[PSI_PRE_TYPE_CHECK] ID: ${itemId}, FID: ${itemDetails.FileID}, Name: '${itemDetails.FileName}', Type: ${itemDetails.Type}, Size: ${itemDetails.Size}, Etag: ${itemDetails.Etag ? itemDetails.Etag.substring(0,10)+'...' : 'N/A'}`); if (isNaN(itemDetails.FileID) && itemDetails.FileID !== 0) { failureCount++; processedCount++; // Count as processed const errorMsg = `项目 '${itemDetails.FileName || itemId}' (ID: ${itemId}) FileID无效 (${itemDetails.FileID})`; processStateManager.appendLogMessage(`❌ [PSI_INVALID_FID] ${errorMsg}`); permanentlyFailedItems.push({ fileName: itemDetails.FileName || String(itemId), id: String(itemId), error: errorMsg }); progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: baseItemNameForLog }); return; } const cleanName = (itemDetails.FileName || "Unknown").replace(/[#$%\/]/g, "_").replace(new RegExp(COMMON_PATH_DELIMITER.replace(/[.*+?^${}()|[\\\]\\\\]/g, '\\$&'), 'g'), '_'); const itemDisplayPath = `${currentRelativePath ? currentRelativePath + '/' : ''}${cleanName}`; const formattedSize = formatBytes(Number(itemDetails.Size) || 0); // Increment processedCount if it hasn't been due to an early fetch/FID failure let alreadyCountedInError = permanentlyFailedItems.some(f => f.id === String(itemId)); if (!alreadyCountedInError) { processedCount++; processStateManager.appendLogMessage(`[PSI_PROCESSED_COUNT_INC] ID: ${itemId}. Processed count is now ${processedCount}.`); } progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: `${itemDisplayPath} (${formattedSize})` }); if (itemDetails.Type === 0) { // File processStateManager.appendLogMessage(`[PSI_FILE_CHECK] '${itemDetails.FileName}' (ID: ${itemId}) is Type 0.`); if (itemDetails.Etag && String(itemDetails.Etag).length > 0 && itemDetails.Size !== undefined) { processStateManager.appendLogMessage(`[PSI_FILE_META_OK] File: '${itemDetails.FileName}', Etag: '${itemDetails.Etag.substring(0,10)}...', Size: ${itemDetails.Size}`); processStateManager.appendLogMessage(`[PSI_PRE_FILTER_CHECK] cleanName: '${cleanName}', filterOnShareEnabled: ${filterManager.filterOnShareEnabled}`); if (filterManager.shouldFilterFile(cleanName, true)) { processStateManager.appendLogMessage(`⏭️ [PSI_FILTERED] File '${itemDisplayPath}' (${formattedSize}) was excluded by filter.`); // Not added to permanentlyFailedItems as this is expected behavior if filtered. } else { addDataCb({ etag: itemDetails.Etag, size: itemDetails.Size, fullPath: itemDisplayPath }); successCount++; processStateManager.appendLogMessage(`✔️ [PSI_FILE_SUCCESS] Added file '${itemDisplayPath}' (${formattedSize}) to link.`); } } else { failureCount++; // This is a failure for link generation let ed = (!itemDetails.Etag || String(itemDetails.Etag).length === 0) ? "缺少或空Etag" : "缺少大小"; const errorMsg = `File '${itemDisplayPath}' (${formattedSize}) (ID: ${itemId})元数据不完整: ${ed}. Etag: '${itemDetails.Etag}', Size: ${itemDetails.Size}`; processStateManager.appendLogMessage(`❌ [PSI_FILE_META_FAIL] ${errorMsg}`); permanentlyFailedItems.push({ fileName: itemDisplayPath, id: String(itemDetails.FileID), error: errorMsg, etag: itemDetails.Etag, size: itemDetails.Size }); } } else if (itemDetails.Type === 1) { // Folder processStateManager.appendLogMessage(`[PSI_FOLDER_CHECK] '${itemDetails.FileName}' (ID: ${itemId}) is Type 1.`); markFolderCb(true); processStateManager.appendLogMessage(`📁 [PSI_SCAN_FOLDER] Scanning folder: ${itemDisplayPath}`); progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: itemDisplayPath, extraStatus: "列出内容..." }); let contents; try { contents = await apiHelper.listDirectoryContents(itemDetails.FileID); if (processStateManager.isStopRequested()) throw new Error("UserStopped"); } catch (e) { if (processStateManager.isStopRequested()) throw e; // This folder itself has an issue if listing fails. failureCount++; const errorMsg = `处理文件夹 '${itemDisplayPath}' (ID: ${itemId}) 内容列出失败: ${e.message}`; processStateManager.appendLogMessage(`❌ [PSI_LIST_DIR_FAIL] ${errorMsg}`); permanentlyFailedItems.push({ fileName: itemDisplayPath, id: String(itemDetails.FileID), error: `列出内容失败: ${e.message}` }); progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: itemDisplayPath, extraStatus: "列出内容失败" }); return; } totalDiscovered += contents.length; for (const contentItem of contents) { if (processStateManager.isStopRequested()) throw new Error("UserStopped"); if (isNaN(contentItem.FileID) && contentItem.FileID !==0) { failureCount++; const errorMsg = `文件夹 '${itemDisplayPath}' 内发现无效项目ID (${contentItem.FileID}), 文件名: '${contentItem.FileName}'`; processStateManager.appendLogMessage(`❌ [PSI_INVALID_SUB_ID] ${errorMsg}`); permanentlyFailedItems.push({ fileName: `${itemDisplayPath}/${contentItem.FileName || '未知'}`, id: String(contentItem.FileID), error: errorMsg }); continue; } await processSingleItem(contentItem.FileID, itemDisplayPath, contentItem); await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS / 2)); } } else { // Unknown type failureCount++; // Count unknown types as failures for link generation const unknownTypeMsg = `项目 '${itemDisplayPath}' (${formattedSize}) (ID: ${itemId}) 是未知类型 (${itemDetails.Type}),已跳过。`; processStateManager.appendLogMessage(`⚠️ [PSI_UNKNOWN_TYPE] ${unknownTypeMsg}`); permanentlyFailedItems.push({ fileName: itemDisplayPath, id: String(itemDetails.FileID), error: unknownTypeMsg, type: itemDetails.Type }); } await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS)); } progressCb({ processed: 0, total: totalDiscovered, successCount: 0, failureCount: 0, currentFile: "准备开始..." }); for (let i = 0; i < selectedItemIds.length; i++) { if (processStateManager.isStopRequested()) break; // Log before processing each top-level item processStateManager.appendLogMessage(`[generateShareLink] 开始处理顶层项目 ${i + 1}/${selectedItemIds.length}, ID: ${selectedItemIds[i]}`); await processSingleItem(selectedItemIds[i], ""); } return { totalDiscoveredItemsForProgress: Math.max(totalDiscovered, processedCount), itemsProcessedForProgress: processedCount, successes: successCount, failures: failureCount, permanentlyFailedItems: permanentlyFailedItems // Return failed items }; }, "生成秒传链接"); }, generateLinkFromPublicShare: async function(shareKey, sharePwd, startParentFileId = "0") { if (!shareKey?.trim()) { uiManager.showAlert("分享Key不能为空。"); return "";} if (isNaN(parseInt(startParentFileId))) { uiManager.showAlert("起始文件夹ID必须是数字。"); return ""; } return this._generateLinkProcess(async (addDataCb, markFolderCb, progressCb) => { let totalDiscovered = 1; let processedCount = 0; let successCount = 0; let failureCount = 0; async function _fetchSharedItemsRecursive(currentSharedParentId, currentRelativePath) { if (processStateManager.isStopRequested()) throw new Error("UserStopped"); const baseItemNameForUI = `${currentRelativePath || '分享根目录'}/ID:${currentSharedParentId}`; progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: baseItemNameForUI, extraStatus: "获取分享内容..." }); let contents; try { contents = await apiHelper.listSharedDirectoryContents(currentSharedParentId, shareKey, sharePwd); if (processStateManager.isStopRequested()) throw new Error("UserStopped"); } catch (e) { if (processStateManager.isStopRequested()) throw e; failureCount++; processedCount++; processStateManager.appendLogMessage(`❌ 获取分享目录 "${baseItemNameForUI}" 内容失败: ${e.message}`, true); progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: baseItemNameForUI, extraStatus: "获取分享内容失败" }); return; } if (processedCount === 0 && currentSharedParentId === startParentFileId) totalDiscovered = contents.length > 0 ? contents.length : 1; else totalDiscovered += contents.length; processedCount++; for (const item of contents) { if (processStateManager.isStopRequested()) throw new Error("UserStopped"); if (isNaN(item.FileID)) { failureCount++; totalDiscovered = Math.max(1, totalDiscovered-1); processStateManager.appendLogMessage(`❌ 分享内发现无效项目ID: ${item.FileName}`, true); continue; } const cleanName = (item.FileName || "Unknown").replace(/[#$%\/]/g, "_").replace(new RegExp(COMMON_PATH_DELIMITER.replace(/[.*+?^${}()|[\\\]\\\\]/g, '\\\\$&'), 'g'), '_'); const itemDisplayPath = `${currentRelativePath ? currentRelativePath + '/' : ''}${cleanName}`; const formattedSize = formatBytes(Number(item.Size) || 0); let itemProcessedThisLoop = false; // Flag to ensure processedCount is incremented correctly for files if (item.Type === 0) { // File progressCb({ processed: processedCount + (itemProcessedThisLoop ? 0 : 1), total: totalDiscovered, successCount, failureCount, currentFile: `${itemDisplayPath} (${formattedSize})` }); if (item.Etag && item.Size !== undefined) { if (filterManager.shouldFilterFile(cleanName, true)) { processStateManager.appendLogMessage(`⏭️ 已过滤: ${itemDisplayPath} (${formattedSize})`); } else { addDataCb({ etag: item.Etag, size: item.Size, fullPath: itemDisplayPath }); successCount++; processStateManager.appendLogMessage(`✔️ 文件 (分享): ${itemDisplayPath} (${formattedSize})`);} } else { failureCount++; let ed = !item.Etag ? "缺少Etag" : "缺少大小"; processStateManager.appendLogMessage(`❌ 分享文件 "${itemDisplayPath}" (${formattedSize}) ${ed}`, true); } if(!itemProcessedThisLoop) { processedCount++; itemProcessedThisLoop = true;} } else if (item.Type === 1) { // Folder progressCb({ processed: processedCount, total: totalDiscovered, successCount, failureCount, currentFile: itemDisplayPath }); // Update UI for folder before recursive call markFolderCb(true); processStateManager.appendLogMessage(`📁 扫描分享文件夹: ${itemDisplayPath}`); await _fetchSharedItemsRecursive(item.FileID, itemDisplayPath); } await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS / 2)); } } progressCb({ processed: 0, total: totalDiscovered, successCount: 0, failureCount: 0, currentFile: "准备开始从分享链接生成..." }); await _fetchSharedItemsRecursive(startParentFileId, ""); return { totalDiscoveredItemsForProgress: Math.max(totalDiscovered, processedCount), itemsProcessedForProgress: processedCount, successes: successCount, failures: failureCount }; }, `从分享链接生成 (Key: ${shareKey.substring(0,8)}...)`); }, parseShareLink: (shareLink) => { let commonBasePath = ""; let isCommonPathFormat = false; let isV2EtagFormat = false; if (shareLink.startsWith(COMMON_PATH_LINK_PREFIX_V2)) { isCommonPathFormat = true; isV2EtagFormat = true; shareLink = shareLink.substring(COMMON_PATH_LINK_PREFIX_V2.length); } else if (shareLink.startsWith(COMMON_PATH_LINK_PREFIX_V1)) { isCommonPathFormat = true; shareLink = shareLink.substring(COMMON_PATH_LINK_PREFIX_V1.length); } if (isCommonPathFormat) { const delimiterPos = shareLink.indexOf(COMMON_PATH_DELIMITER); if (delimiterPos > -1) { commonBasePath = shareLink.substring(0, delimiterPos); shareLink = shareLink.substring(delimiterPos + 1); } else { console.error("Malformed common path link: delimiter not found."); isCommonPathFormat = false; } } else { if (shareLink.startsWith(LEGACY_FOLDER_LINK_PREFIX_V2)) { isV2EtagFormat = true; shareLink = shareLink.substring(LEGACY_FOLDER_LINK_PREFIX_V2.length); } else if (shareLink.startsWith(LEGACY_FOLDER_LINK_PREFIX_V1)) { shareLink = shareLink.substring(LEGACY_FOLDER_LINK_PREFIX_V1.length); } } return shareLink.split('$').map(sLink => { const parts = sLink.split('#'); if (parts.length >= 3) { let etag = parts[0]; try { etag = optimizedEtagToHex(parts[0], isV2EtagFormat); } catch (e) { console.error(`[${SCRIPT_NAME}] Error decoding ETag: ${parts[0]}, ${e.message}`); return null; } let filePath = parts.slice(2).join('#'); if (isCommonPathFormat && commonBasePath) filePath = commonBasePath + filePath; return { etag: etag, size: parts[1], fileName: filePath }; } return null; }).filter(i => i); }, transferFromShareLink: async function(shareLink, targetFolderPath = "") { if (!shareLink?.trim()) { uiManager.showAlert("链接为空"); return; } const filesToProcess = this.parseShareLink(shareLink); if (!filesToProcess.length) { uiManager.showAlert("无法解析链接或链接中无有效文件信息"); return; } const isFolderStructureHint = shareLink.startsWith(LEGACY_FOLDER_LINK_PREFIX_V1) || shareLink.startsWith(COMMON_PATH_LINK_PREFIX_V1) || shareLink.startsWith(LEGACY_FOLDER_LINK_PREFIX_V2) || shareLink.startsWith(COMMON_PATH_LINK_PREFIX_V2) || filesToProcess.some(f => f.fileName.includes('/')); await this._executeActualFileTransfer(filesToProcess, isFolderStructureHint, "链接转存", [], targetFolderPath); }, transferImportedJsonData: async function(jsonData, targetFolderPath = "") { if (!jsonData || typeof jsonData !== 'object') { uiManager.showAlert("JSON数据无效"); return; } const { scriptVersion, exportVersion, usesBase62EtagsInExport, commonPath, files } = jsonData; if (!files || !Array.isArray(files) || files.length === 0) { uiManager.showAlert("JSON文件中没有有效的文件条目。"); return; } processStateManager.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)"; preprocessingFailedItems.push({ fileName: (fileFromJson||{}).path || "未知文件(数据缺失)", error: errorMsg, originalEntry: fileFromJson||{} }); return null; } let finalEtag; try { let attemptDecode = usesBase62EtagsInExport; if (usesBase62EtagsInExport === undefined) { const isLikelyHex = /^[0-9a-fA-F]+$/.test(fileFromJson.etag); if (isLikelyHex && fileFromJson.etag.length === 32) attemptDecode = false; else if (!isLikelyHex || fileFromJson.etag.length < 32) attemptDecode = true; else attemptDecode = false; processStateManager.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) { uiManager.showModal("⚠️ JSON导入预处理失败",`所有 ${preprocessingFailedItems.length} 个文件条目在导入预处理阶段即发生错误,无法继续转存。<br><div id="fastlink-permanent-failures-log" style="display: block; margin-top: 10px; text-align: left; max-height: 150px; overflow-y: auto; border: 1px solid #ddd; padding: 5px; font-size: 0.85em;"><h4>预处理失败项目:</h4><div id="fastlink-failures-list">${preprocessingFailedItems.map(pf => `<p style="margin:2px 0;">📄 <span style="font-weight:bold;">${pf.fileName}</span>: <span style="color:red;">${pf.error}</span></p>`).join('')}</div></div>`, 'info_with_buttons', true, null, null, preprocessingFailedItems); return; } else if (!filesToProcess.length) { uiManager.showAlert("JSON文件中解析后无有效文件可转存(所有条目均无效或解码失败)。"); return; } const isFolderStructureHint = !!commonPath || filesToProcess.some(f => f.fileName.includes('/')); await this._executeActualFileTransfer(filesToProcess, isFolderStructureHint, "文件导入", preprocessingFailedItems, targetFolderPath); }, _executeActualFileTransfer: async function(filesToProcess, isFolderStructureHint, operationTitle = "转存", initialPreprocessingFailures = [], targetFolderPath = "") { processStateManager.reset(); this.currentOperationRateLimitStatus.consecutiveRateLimitFailures = 0; let permanentlyFailedItems = [...initialPreprocessingFailures]; let totalSuccessfullyTransferredSize = 0; let rootDirId = this.getCurrentDirectoryId(); if (rootDirId === null || isNaN(parseInt(rootDirId))) { uiManager.showAlert("无法确定当前目标目录ID。将尝试转存到根目录。"); rootDirId = "0"; } rootDirId = parseInt(rootDirId); let userSpecifiedFolderPath = targetFolderPath ? targetFolderPath.trim() : ""; let finalRootDirId = rootDirId; const initialModalTitle = `⚙️ ${operationTitle}状态 (${filesToProcess.length} 项)`; let modalContent = ` <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}${userSpecifiedFolderPath ? " 的 " + userSpecifiedFolderPath + " 文件夹中" : ""}</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>`; uiManager.showModal(initialModalTitle, modalContent, 'progress_stoppable', false); let successes = 0, failures = 0; const folderCache = {}; const startTime = Date.now(); // 优化1: 文件夹名特殊字符替换函数 function sanitizeFolderName(folderName) { if (typeof folderName !== 'string') return folderName; return folderName .replace(/:/g, ":") .replace(/\//g, "/") .replace(/\\/g, "\") .replace(/\*/g, "*") .replace(/\?/g, "?") .replace(/"/g, """) .replace(/</g, "<") .replace(/>/g, ">") .replace(/\|/g, "|"); } if (userSpecifiedFolderPath) { try { processStateManager.updateProgressUI(0, filesToProcess.length, successes, failures, `创建目标文件夹: ${userSpecifiedFolderPath}`, ""); const dirContents = await apiHelper.listDirectoryContents(rootDirId, 500); if (processStateManager.isStopRequested()) { uiManager.showAlert("操作已取消"); return; } const pathParts = userSpecifiedFolderPath.split('/'); let parentIdForUserPath = rootDirId; let currentPathForUser = ""; for (let i = 0; i < pathParts.length; i++) { let folderName = pathParts[i].trim(); if (!folderName) continue; folderName = sanitizeFolderName(folderName); // 优化1: 应用净化 currentPathForUser = currentPathForUser ? `${currentPathForUser}/${folderName}` : folderName; // 使用净化后的名称构建路径 if (folderCache[currentPathForUser]) { parentIdForUserPath = folderCache[currentPathForUser]; continue; } const existingFolder = dirContents.find(item => item.Type === 1 && item.FileName === folderName && item.ParentFileID == parentIdForUserPath); if (existingFolder && !isNaN(existingFolder.FileID)) { parentIdForUserPath = existingFolder.FileID; processStateManager.appendLogMessage(`ℹ️ 文件夹已存在: ${folderName} (ID: ${parentIdForUserPath})`); } else { processStateManager.appendLogMessage(`📁 创建文件夹: ${folderName} (在ID: ${parentIdForUserPath})`); const newFolder = await apiHelper.createFolder(parentIdForUserPath, folderName); if (processStateManager.isStopRequested()) { uiManager.showAlert("操作已取消"); return; } if (newFolder && !isNaN(parseInt(newFolder.FileId))) { parentIdForUserPath = parseInt(newFolder.FileId); processStateManager.appendLogMessage(`✅ 文件夹创建成功: ${folderName} (ID: ${parentIdForUserPath})`); } else { throw new Error(`创建文件夹返回的ID无效: ${JSON.stringify(newFolder)}`); } } folderCache[currentPathForUser] = parentIdForUserPath; } finalRootDirId = parentIdForUserPath; processStateManager.appendLogMessage(`✅ 目标文件夹就绪: ${userSpecifiedFolderPath} (净化后ID: ${finalRootDirId})`); } catch (error) { processStateManager.appendLogMessage(`❌ 创建目标文件夹 "${userSpecifiedFolderPath}" 失败: ${error.message}`, true); console.error(`[${SCRIPT_NAME}] 创建目标文件夹错误:`, error); uiManager.showAlert(`创建目标文件夹失败: ${error.message},将尝试转存到当前目录 (ID: ${rootDirId})`); finalRootDirId = rootDirId; } } for (let i = 0; i < filesToProcess.length; i++) { if (processStateManager.isStopRequested()) break; const file = filesToProcess[i]; const originalFileNameForLog = file.fileName || "未知文件"; const formattedFileSize = file.size ? formatBytes(Number(file.size)) : "未知大小"; if (!file || !file.fileName || !file.etag || !file.size) { failures++; processStateManager.appendLogMessage(`❌ 跳过无效文件数据 (索引 ${i}): ${originalFileNameForLog}`, true); permanentlyFailedItems.push({ ...file, fileName: originalFileNameForLog, error: "无效文件数据" }); processStateManager.updateProgressUI(i + 1, filesToProcess.length, successes, failures, `无效数据 (${formattedFileSize})`); continue; } if (filterManager.shouldFilterFile(file.fileName, false)) { processStateManager.appendLogMessage(`⏭️ 已过滤: ${file.fileName} (${formattedFileSize})`); processStateManager.updateProgressUI(i + 1, filesToProcess.length, successes, failures, `已过滤: ${file.fileName} (${formattedFileSize})`); continue; } processStateManager.updateProgressUI(i, filesToProcess.length, successes, failures, `${file.fileName} (${formattedFileSize})`, ""); let effectiveParentId = finalRootDirId; let actualFileName = file.fileName; try { if (file.fileName.includes('/')) { const pathParts = file.fileName.split('/'); actualFileName = pathParts.pop(); actualFileName = sanitizeFolderName(actualFileName); // 优化1: 文件名也可能包含这些字符,虽然通常是文件夹 if (!actualFileName && pathParts.length > 0 && file.fileName.endsWith('/')) { processStateManager.appendLogMessage(`⚠️ 文件路径 "${file.fileName}" (${formattedFileSize}) 可能表示目录,跳过。`, true); failures++; permanentlyFailedItems.push({ ...file, error: "路径表示目录" }); continue; } let parentIdForLinkPath = finalRootDirId; let currentCumulativeLinkPath = ""; for (let j = 0; j < pathParts.length; j++) { if (processStateManager.isStopRequested()) throw new Error("UserStopped"); let part = pathParts[j]; if (!part) continue; part = sanitizeFolderName(part); // 优化1: 对路径中的每个部分应用净化 currentCumulativeLinkPath = j === 0 ? part : `${currentCumulativeLinkPath}/${part}`; processStateManager.updateProgressUI(i, filesToProcess.length, successes, failures, `${file.fileName} (${formattedFileSize})`, `检查/创建路径: ${currentCumulativeLinkPath}`); const cacheKeyForLinkPath = `link:${currentCumulativeLinkPath}`; if (folderCache[cacheKeyForLinkPath]) { parentIdForLinkPath = folderCache[cacheKeyForLinkPath]; } else { const dirContents = await apiHelper.listDirectoryContents(parentIdForLinkPath, 500); if (processStateManager.isStopRequested()) throw new Error("UserStopped"); const foundFolder = dirContents.find(it => it.Type === 1 && it.FileName === part && it.ParentFileID == parentIdForLinkPath); if (foundFolder && !isNaN(foundFolder.FileID)) { parentIdForLinkPath = foundFolder.FileID; } else { processStateManager.updateProgressUI(i, filesToProcess.length, successes, failures, `${file.fileName} (${formattedFileSize})`, `创建文件夹: ${currentCumulativeLinkPath}`); const createdFolder = await apiHelper.createFolder(parentIdForLinkPath, part); if (processStateManager.isStopRequested()) throw new Error("UserStopped"); parentIdForLinkPath = parseInt(createdFolder.FileId); } folderCache[cacheKeyForLinkPath] = parentIdForLinkPath; await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS)); } } effectiveParentId = parentIdForLinkPath; } else { actualFileName = sanitizeFolderName(actualFileName); // 优化1: 根目录下的文件名也净化 } if (isNaN(effectiveParentId) || effectiveParentId < 0) throw new Error(`路径创建失败或父ID无效 (${effectiveParentId}) for ${file.fileName} (${formattedFileSize})`); if (!actualFileName) throw new Error(`文件名无效 for ${file.fileName} (${formattedFileSize})`); processStateManager.updateProgressUI(i, filesToProcess.length, successes, failures, `${actualFileName} (${formattedFileSize})`, `秒传到ID: ${effectiveParentId}`); await apiHelper.rapidUpload(file.etag, file.size, actualFileName, effectiveParentId); if (processStateManager.isStopRequested()) throw new Error("UserStopped"); successes++; totalSuccessfullyTransferredSize += Number(file.size); processStateManager.appendLogMessage(`✔️ 文件: ${file.fileName} (${formattedFileSize})`); } catch (e) { if (processStateManager.isStopRequested()) break; failures++; processStateManager.appendLogMessage(`❌ 文件 "${actualFileName}" (${formattedFileSize}) (原始: ${originalFileNameForLog}) 失败: ${e.message}`, true); permanentlyFailedItems.push({ ...file, fileName: originalFileNameForLog, error: e.message }); processStateManager.updateProgressUI(i + 1, filesToProcess.length, successes, failures, `${actualFileName} (${formattedFileSize})`, "操作失败"); } await new Promise(r => setTimeout(r, RETRY_AND_DELAY_CONFIG.PROACTIVE_DELAY_MS)); } processStateManager.updateProgressUI(filesToProcess.length, filesToProcess.length, successes, failures, "处理完成", ""); const totalTime = Math.round((Date.now() - startTime) / 1000); let resultEmoji = successes > 0 && permanentlyFailedItems.length === 0 ? '🎉' : (successes > 0 ? '🎯' : '😢'); if (processStateManager.isStopRequested()) resultEmoji = '🔴'; let finalUserMessage = processStateManager.isStopRequested() ? "操作已由用户停止" : `${operationTitle}完成`; if (!processStateManager.isStopRequested() && permanentlyFailedItems.length > 0) finalUserMessage = `${operationTitle}部分完成或预处理失败,共 ${permanentlyFailedItems.length} 个文件有问题。`; const formattedTotalSuccessfullyTransferredSize = formatBytes(totalSuccessfullyTransferredSize); let summary = `<div class="fastlink-result"><h3>${resultEmoji} ${finalUserMessage}</h3><p>✅ 成功转存: ${successes} 个文件</p><p>💾 成功转存总大小: ${formattedTotalSuccessfullyTransferredSize}</p><p>❌ 转存尝试失败: ${failures} 个文件</p><p>📋 总计问题文件 (含预处理): ${permanentlyFailedItems.length} 个</p><p>⏱️ 耗时: ${totalTime} 秒</p>${!processStateManager.isStopRequested() && successes > 0 ? '<p>📢 请手动刷新页面查看已成功转存的结果</p>' : ''}</div>`; uiManager.updateModalContent(summary); if (permanentlyFailedItems.length > 0 && !processStateManager.isStopRequested()) { const failuresLogDiv = document.getElementById('fastlink-failures-list'); const permanentFailuresDiv = document.getElementById('fastlink-permanent-failures-log'); if (failuresLogDiv && permanentFailuresDiv) { failuresLogDiv.innerHTML = ''; permanentlyFailedItems.forEach(pf => { const p = document.createElement('p'); p.style.margin = '2px 0'; p.innerHTML = `📄 <span style="font-weight:bold;">${pf.fileName}</span>: <span style="color:red;">${pf.error || '未知错误'}</span>`; failuresLogDiv.appendChild(p); }); permanentFailuresDiv.style.display = 'block'; } const modalInstance = uiManager.getModalElement(); if (modalInstance) { let buttonsDiv = modalInstance.querySelector('.fastlink-modal-buttons'); if(!buttonsDiv) { buttonsDiv = document.createElement('div'); buttonsDiv.className = 'fastlink-modal-buttons'; modalInstance.querySelector(`#${uiManager.MODAL_CONTENT_ID}`)?.appendChild(buttonsDiv); } buttonsDiv.innerHTML = ''; const retryBtn = document.createElement('button'); retryBtn.id = 'fl-m-retry-failed'; retryBtn.className = 'confirm-btn'; retryBtn.textContent = `🔁 重试失败项 (${permanentlyFailedItems.length})`; retryBtn.onclick = () => { this._executeActualFileTransfer(permanentlyFailedItems, isFolderStructureHint, operationTitle + " - 重试", [], targetFolderPath); }; buttonsDiv.appendChild(retryBtn); const copyLogBtn = document.createElement('button'); copyLogBtn.id = 'fl-m-copy-failed-log'; copyLogBtn.className = 'copy-btn'; copyLogBtn.style.marginLeft = '10px'; copyLogBtn.textContent = '复制问题日志'; copyLogBtn.onclick = () => { const logText = permanentlyFailedItems.map(pf => `文件: ${pf.fileName || (pf.originalEntry&&pf.originalEntry.path)||'未知路径'}\n${(pf.originalEntry&&pf.originalEntry.etag)?('原始ETag: '+pf.originalEntry.etag+'\n'):(pf.etag?'处理后ETag: '+pf.etag+'\n':'')}${(pf.originalEntry&&pf.originalEntry.size)?('大小: '+pf.originalEntry.size+'\n'):(pf.size?'大小: '+pf.size+'\n':'')}错误: ${pf.error||'未知错误'}`).join('\n\n'); GM_setClipboard(logText); uiManager.showAlert("问题文件日志已复制到剪贴板!", 1500); }; buttonsDiv.appendChild(copyLogBtn); const closeBtnModal = document.createElement('button'); closeBtnModal.id = 'fl-m-final-close'; closeBtnModal.className = 'cancel-btn'; closeBtnModal.textContent = '关闭'; closeBtnModal.style.marginLeft = '10px'; closeBtnModal.onclick = () => uiManager.hideModal(); buttonsDiv.appendChild(closeBtnModal); } uiManager.enableModalCloseButton(false); } else { uiManager.enableModalCloseButton(true); } } }; const uiManager = { modalElement: null, dropdownMenuElement: null, STYLE_ID: 'fastlink-dynamic-styles', MODAL_CONTENT_ID: 'fastlink-modal-content-area', activeModalOperationType: null, modalHideCallback: null, miniProgressElement: null, isMiniProgressActive: false, // Added for mini progress // 公共资源库浏览器的状态 publicRepoState: { currentPage: 1, isLoading: false, isEndOfList: false, currentSearchTerm: '', currentSearchPage: 1, isSearching: false, isSearchEndOfList: false, selectedShareForImport: null, // { codeHash, name } }, _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; let css = ` .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;max-height:90vh;display:flex;flex-direction:column;text-align:center} .fastlink-modal-title{font-size:18px;font-weight:700;margin-bottom:15px} .fastlink-modal-content{flex:1;overflow-y:auto;max-height:calc(90vh - 140px);} .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{margin-top:15px;} .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-modal-buttons .minimize-btn{background-color:#ffc107;color:#212529;margin-left:5px;} .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-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:10002 !important;max-height:calc(100vh - 80px);overflow-y:auto;top:100%;left:0;} .fastlink-result{text-align:center} .fastlink-result h3{font-size:18px;margin:5px 0 15px} .fastlink-result p{margin:8px 0} .fastlink-result .submit-to-public-repo-container { margin-top: 15px; padding-top: 10px; border-top: 1px solid #eee; text-align: left; } .fastlink-result .submit-to-public-repo-container label { display: block; margin-bottom: 5px; font-weight: bold; } .fastlink-result .submit-to-public-repo-container input[type="text"] { width: calc(100% - 20px); padding: 8px; margin-bottom: 5px; border: 1px solid #ccc; border-radius: 4px; } .fastlink-result .submit-to-public-repo-container p.hint { font-size: 0.85em; color: #666; margin-bottom: 10px; } .fastlink-result .submit-to-public-repo-container button { display: block; width: 100%; background-color: #5cb85c; color: white; } .fastlink-drag-drop-area{border:2px dashed #ccc;padding:10px;transition: border-color .3s ease;} .fastlink-drag-drop-area.drag-over-active{border-color:#007bff; background-color: #f8f9fa;} .filter-controls{display:flex;justify-content:space-between;margin-bottom:15px;} .filter-btn{padding:5px 10px;border:1px solid #ddd;border-radius:4px;background:#f8f9f8;cursor:pointer;font-size:0.9em;} .filter-btn:hover{background:#e9ecef;} .filter-description{margin-bottom:15px;text-align:left;font-size:0.9em;} .filter-list{max-height:250px;overflow-y:auto;border:1px solid #eee;padding:5px;text-align:left;margin-bottom:15px;} .filter-item{display:flex;align-items:center;padding:5px 0;border-bottom:1px solid #f5f5f5;} .filter-item:last-child{border-bottom:none;} .filter-checkbox{margin-right:10px;} .filter-emoji{margin-right:5px;} .filter-ext{font-weight:bold;margin-right:8px;} .filter-name{color:#666;font-size:0.9em;} .fastlink-modal.filter-dialog{max-height:90vh;display:flex;flex-direction:column;} .fastlink-modal.filter-dialog .fastlink-modal-content{flex:1;overflow-y:auto;max-height:calc(90vh - 120px);} .filter-global-switches{margin-bottom:15px;text-align:left;} .filter-switch-item{display:flex;align-items:center;margin-bottom:8px;} .filter-toggle-checkbox{margin-right:10px;} .filter-divider{margin:15px 0;border:0;border-top:1px solid #eee;} .filter-select-style-container { position: relative; margin-bottom: 15px; } .filter-selected-tags { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px; min-height: 38px; margin-bottom: -1px; } .filter-tag { display: inline-flex; align-items: center; background-color: #e6f7ff; border: 1px solid #91d5ff; border-radius: 4px; padding: 3px 8px; font-size: 0.9em; cursor: default; } .filter-tag .filter-emoji { margin-right: 4px; } .filter-tag .filter-tag-text { font-weight: bold; } .filter-tag .filter-tag-name { color: #555; margin-left: 4px; font-size: 0.9em; } .filter-tag-remove { margin-left: 8px; cursor: pointer; font-weight: bold; color: #555; } .filter-tag-remove:hover { color: #000; } .filter-search-input { width: 100%; padding: 8px 10px; border: 1px solid #d9d9d9; border-radius: 0 0 4px 4px; box-sizing: border-box; font-size: 0.95em; } .filter-selected-tags + .filter-search-input { border-top-left-radius: 0; border-top-right-radius: 0; } .filter-search-input:focus { outline: none; border-color: #40a9ff; box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); } .filter-dropdown { position: absolute; top: 100%; left: 0; right: 0; background-color: #fff; border: 1px solid #d9d9d9; border-top: none; max-height: 200px; overflow-y: auto; z-index: 1001; display: none; border-radius: 0 0 4px 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); } .filter-dropdown-item { display: flex; align-items: center; padding: 8px 12px; cursor: pointer; font-size: 0.9em; } .filter-dropdown-item:hover { background-color: #f5f5f5; } .filter-dropdown-item .filter-emoji { margin-right: 6px; } .filter-dropdown-item .filter-ext { font-weight: bold; margin-right: 6px; } .filter-dropdown-item .filter-name { color: #555; } .fastlink-modal.filter-dialog .fastlink-modal-content { max-height: calc(90vh - 160px); } .folder-selector-container{margin-top:10px;text-align:left;}.folder-selector-label{display:block;margin-bottom:5px;font-size:0.9em;}.folder-selector-input-container{position:relative;}.folder-selector-input{width:100%;padding:8px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box;}.folder-selector-dropdown{position:absolute;width:100%;max-height:200px;overflow-y:auto;background:#fff;border:1px solid #ccc;border-top:none;border-radius:0 0 4px 4px;z-index:1000;display:none;}.folder-selector-dropdown.active{display:block;}.folder-item{display:flex;align-items:center;padding:8px 10px;cursor:pointer;}.folder-item:hover{background:#f5f5f5;}.folder-item-checkbox{margin-right:10px;}.folder-item-icon{margin-right:8px;color:#1890ff;}.folder-item-name{flex-grow:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.folder-tag-container{display:flex;flex-wrap:wrap;gap:5px;margin-top:5px;min-height:30px;border:1px solid #eee;padding:5px;border-radius:4px;}.folder-tag{display:flex;align-items:center;background:#e6f7ff;border-radius:2px;padding:2px 8px;border:1px solid #91d5ff;}.folder-tag-text{margin-right:5px;}.folder-tag-remove{cursor:pointer;color:#999;font-weight:bold;font-size:14px;}.folder-tag-remove:hover{color:#666;} .fastlink-mini-progress{position:fixed;bottom:15px;right:15px;width:280px;background-color:#fff;border:1px solid #ccc;border-radius:6px;box-shadow:0 2px 10px rgba(0,0,0,.2);z-index:10005;padding:10px;font-size:0.85em;display:none;flex-direction:column;} .fastlink-mini-progress-title{font-weight:bold;margin-bottom:5px;display:flex;justify-content:space-between;align-items:center;} .fastlink-mini-progress-bar-container{width:100%;height:8px;background-color:#e9ecef;border-radius:4px;overflow:hidden;margin-bottom:5px;} .fastlink-mini-progress-bar{height:100%;background-color:#007bff;transition:width .2s ease;} .fastlink-mini-progress-file{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:3px;color:#555;} .fastlink-mini-progress-status{font-size:0.9em;color:#333;} .fastlink-mini-progress-restore-btn{font-size:0.8em;padding:3px 8px;background-color:#6c757d;color:white;border:none;border-radius:3px;cursor:pointer;align-self:flex-start;margin-top:5px;} .fastlink-mini-progress-restore-btn:hover{background-color:#5a6268;} /* 公共资源库浏览器样式 */ .public-repo-search-bar { display: flex; margin-bottom: 10px; } .public-repo-search-input { flex-grow: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px 0 0 4px; } .public-repo-search-button { padding: 8px 12px; background-color: #007bff; color: white; border: 1px solid #007bff; border-radius: 0 4px 4px 0; cursor: pointer; } .public-repo-content-area { max-height: 300px; overflow-y: auto; overflow-x: hidden; /* 修复1: 防止内容区自身横向滚动 */ border: 1px solid #eee; padding: 5px; margin-bottom: 10px; } .public-repo-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 5px; border-bottom: 1px solid #f0f0f0; cursor: pointer; } .public-repo-item:last-child { border-bottom: none; } .public-repo-item:hover, .public-repo-item.selected { background-color: #e9f5ff; } .public-repo-item-info { flex-grow: 1; overflow: hidden; /* 修复1: 确保信息区不会被过长内容撑开 */ min-width: 0; /* 修复1: 配合flex-grow和overflow:hidden */ margin-right: 5px; /* 给右侧按钮留点空间 */ } .public-repo-item-name { font-weight: bold; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .public-repo-item-timestamp { font-size: 0.8em; color: #666; display: block; } .public-repo-item-actions button { background: none; border: none; font-size: 1.2em; cursor: pointer; padding: 5px; color: #007bff; flex-shrink: 0; /* 防止按钮被挤压 */ } .public-repo-loading, .public-repo-empty { text-align: center; color: #888; padding: 15px; } .public-repo-import-status { font-size: 0.9em; color: #666; margin-top: 5px; min-height: 1.2em; } .public-repo-tree-modal .fastlink-modal-content { max-height: 70vh; } /* 确保目录树模态框内容区高度 */ .public-repo-tree-modal .fastlink-modal-title { text-align: left; } /* 目录树标题靠左 */ .public-repo-tree-item { padding: 2px 0; font-family: monospace; white-space: pre; font-size: 0.9em; } `; GM_addStyle(css); }, initMiniProgress: function() { // Added for mini progress if (this.miniProgressElement) return; this.miniProgressElement = document.createElement('div'); this.miniProgressElement.className = 'fastlink-mini-progress'; this.miniProgressElement.innerHTML = ` <div class="fastlink-mini-progress-title"> <span>⚙️ 处理中...</span> <button class="fastlink-mini-progress-restore-btn">恢复</button> </div> <div class="fastlink-mini-progress-bar-container"><div class="fastlink-mini-progress-bar" style="width: 0%;"></div></div> <div class="fastlink-mini-progress-file">准备中...</div> <div class="fastlink-mini-progress-status">0/0</div> `; document.body.appendChild(this.miniProgressElement); this.miniProgressElement.querySelector('.fastlink-mini-progress-restore-btn').addEventListener('click', () => { this.hideMiniProgress(); if (this.modalElement && this.activeModalOperationType === 'progress_stoppable') { this.modalElement.style.display = 'flex'; } }); }, showMiniProgress: function() { // Added for mini progress if (this.miniProgressElement) { this.miniProgressElement.style.display = 'flex'; this.isMiniProgressActive = true; } }, hideMiniProgress: function() { // Added for mini progress if (this.miniProgressElement) { this.miniProgressElement.style.display = 'none'; this.isMiniProgressActive = false; } }, createDropdownButton: function() { const existingButtons = document.querySelectorAll('.fastlink-main-button-container'); existingButtons.forEach(btn => btn.remove()); const targetElement = document.querySelector(DOM_SELECTORS.TARGET_BUTTON_AREA); if (targetElement && targetElement.parentNode) { const buttonContainer = document.createElement('div'); buttonContainer.className = 'fastlink-main-button-container 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-public-repo-browser" class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" style="padding: 5px 12px;">📦 公共资源库</li> <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 class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="separator" style="border-top: 1px solid #eee; margin: 3px 0; padding: 0;"></li> <li id="fastlink-receiveDirect" class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" style="padding: 5px 12px;">📥 链接/文件转存</li> <li id="fastlink-generateFromPublicShare" class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" style="padding: 5px 12px;">🌐 从分享链接生成</li> <li class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="separator" style="border-top: 1px solid #eee; margin: 3px 0; padding: 0;"></li> <li id="fastlink-filterSettings" class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" style="padding: 5px 12px;">🔍 元数据过滤设置</li> <li id="fastlink-public-repo-settings" class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" style="padding: 5px 12px;">⚙️ 公共资源库服务器设置</li> </ul>`; this.dropdownMenuElement = dropdownMenu; 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-public-repo-browser').addEventListener('click', (e) => { e.stopPropagation(); dropdownMenu.style.display = 'none'; this.showPublicRepoBrowserModal(); }); dropdownMenu.querySelector('#fastlink-generateShare').addEventListener('click', async (e) => { e.stopPropagation(); dropdownMenu.style.display = 'none'; await coreLogic.generateShareLink(); }); dropdownMenu.querySelector('#fastlink-generateFromPublicShare').addEventListener('click', (e) => { e.stopPropagation(); dropdownMenu.style.display = 'none'; this.showModal("🌐 从分享链接中生成链接", "", 'inputPublicShare'); }); dropdownMenu.querySelector('#fastlink-receiveDirect').addEventListener('click', (e) => { e.stopPropagation(); dropdownMenu.style.display = 'none'; this.showModal("📥 文件转存/粘贴链接", "", 'inputLink'); }); dropdownMenu.querySelector('#fastlink-filterSettings').addEventListener('click', (e) => { e.stopPropagation(); dropdownMenu.style.display = 'none'; this.showModal("🔍 元数据过滤设置", "", 'filterSettings'); }); dropdownMenu.querySelector('#fastlink-public-repo-settings').addEventListener('click', (e) => { e.stopPropagation(); dropdownMenu.style.display = 'none'; this.showPublicRepoSettingsModal(); }); 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; } }, showPublicRepoSettingsModal: function() { const currentUrl = publicRepoApiHelper.getBaseUrl(); const content = ` <p style="text-align:left; font-size:0.9em; margin-bottom:8px;">请输入公共资源库服务器的 Base URL (链接必须以 http/https 开头,并以 / 结尾)。</p> <p style="text-align:left; font-size:0.9em; margin-bottom:8px; color: red;">如何搭建服务器?请看部署教程:<br>https://github.com/realcwj/123Pan-Unlimited-Share?tab=readme-ov-file#如何使用</p> <input type="url" id="fl-m-public-repo-url" class="fastlink-modal-input" value="${currentUrl}" placeholder="例如: http://your-server.com/"> <p id="fl-m-public-repo-url-status" style="font-size:0.8em; color:red; min-height:1em;"></p> `; this.showModal("⚙️ 公共资源库服务器设置", content, 'publicRepoSettings'); }, showPublicRepoBrowserModal: async function() { this.publicRepoState.currentPage = 1; this.publicRepoState.isLoading = false; this.publicRepoState.isEndOfList = false; this.publicRepoState.currentSearchTerm = ''; this.publicRepoState.currentSearchPage = 1; this.publicRepoState.isSearching = false; this.publicRepoState.isSearchEndOfList = false; this.publicRepoState.selectedShareForImport = null; const content = ` <div class="public-repo-search-bar"> <input type="text" id="fl-m-public-repo-search-input" class="public-repo-search-input" placeholder="搜索分享的根目录名..."> </div> <div id="fl-m-public-repo-content-area" class="public-repo-content-area"> <p class="public-repo-loading">正在加载...</p> </div> <p id="fl-m-public-repo-import-status" class="public-repo-import-status"></p> `; this.showModal("📦 公共资源库", content, 'publicRepoBrowser'); const contentArea = this.modalElement.querySelector('#fl-m-public-repo-content-area'); const searchInput = this.modalElement.querySelector('#fl-m-public-repo-search-input'); const loadShares = async (page, searchTerm = '') => { if (this.publicRepoState.isLoading) return; this.publicRepoState.isLoading = true; const isSearchMode = !!searchTerm; const loadingEl = contentArea.querySelector('.public-repo-loading'); if (!loadingEl && page > 1) { // Add loading more indicator if not first page and no global loading const moreLoading = document.createElement('p'); moreLoading.className = 'public-repo-loading'; moreLoading.textContent = '正在加载更多...'; contentArea.appendChild(moreLoading); } try { let data; if (isSearchMode) { data = await publicRepoApiHelper.searchDatabase(searchTerm, page); } else { data = await publicRepoApiHelper.listPublicShares(page); } const existingLoading = contentArea.querySelector('.public-repo-loading'); if (existingLoading) existingLoading.remove(); if (data && data.success) { if (page === 1) contentArea.innerHTML = ''; // Clear on first page of new list/search if (data.files && data.files.length > 0) { data.files.forEach(share => { const itemDiv = document.createElement('div'); itemDiv.className = 'public-repo-item'; itemDiv.dataset.codehash = share.codeHash; itemDiv.dataset.name = share.name; const infoDiv = document.createElement('div'); infoDiv.className = 'public-repo-item-info'; infoDiv.innerHTML = ` <span class="public-repo-item-name">${share.name}</span> <span class="public-repo-item-timestamp">更新时间: ${new Date(share.timestamp).toLocaleString()}</span> `; const actionsDiv = document.createElement('div'); actionsDiv.className = 'public-repo-item-actions'; const viewTreeBtn = document.createElement('button'); viewTreeBtn.innerHTML = '🔍'; viewTreeBtn.title = '查看目录结构'; viewTreeBtn.onclick = (e) => { e.stopPropagation(); this.showPublicShareContentTreeModal(share.codeHash); }; actionsDiv.appendChild(viewTreeBtn); itemDiv.appendChild(infoDiv); itemDiv.appendChild(actionsDiv); itemDiv.onclick = () => { document.querySelectorAll('.public-repo-item.selected').forEach(sel => sel.classList.remove('selected')); itemDiv.classList.add('selected'); this.publicRepoState.selectedShareForImport = { codeHash: share.codeHash, name: share.name }; const importStatusEl = this.modalElement.querySelector('#fl-m-public-repo-import-status'); if (importStatusEl) importStatusEl.textContent = `已选择 "${share.name}"`; }; contentArea.appendChild(itemDiv); }); if (isSearchMode) { this.publicRepoState.currentSearchPage = page; this.publicRepoState.isSearchEndOfList = data.end; } else { this.publicRepoState.currentPage = page; this.publicRepoState.isEndOfList = data.end; } } else if (page === 1) { contentArea.innerHTML = `<p class="public-repo-empty">${isSearchMode ? '没有匹配的搜索结果。' : '暂无公共资源。'}</p>`; } if ((isSearchMode && data.end) || (!isSearchMode && data.end)) { const endMsg = document.createElement('p'); endMsg.className = 'public-repo-empty'; endMsg.textContent = '已到达列表末尾。'; contentArea.appendChild(endMsg); } } else { if (page === 1) contentArea.innerHTML = ''; // Clear on first page this.showAlert(`加载公共分享失败: ${data.message || '未知错误'}`); if (isSearchMode) this.publicRepoState.isSearchEndOfList = true; else this.publicRepoState.isEndOfList = true; } } catch (error) { console.error("加载公共分享出错:", error); if (page === 1) contentArea.innerHTML = ''; this.showAlert(`加载公共分享出错: ${error.message}`); if (isSearchMode) this.publicRepoState.isSearchEndOfList = true; else this.publicRepoState.isEndOfList = true; } finally { this.publicRepoState.isLoading = false; } }; // Initial load loadShares(1); // Search functionality let searchTimeout; searchInput.addEventListener('input', () => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { const term = searchInput.value.trim(); this.publicRepoState.currentSearchTerm = term; this.publicRepoState.selectedShareForImport = null; // Clear selection on new search const importStatusEl = this.modalElement.querySelector('#fl-m-public-repo-import-status'); if (importStatusEl) importStatusEl.textContent = ''; if (term) { this.publicRepoState.isSearching = true; this.publicRepoState.currentSearchPage = 1; this.publicRepoState.isSearchEndOfList = false; contentArea.innerHTML = '<p class="public-repo-loading">正在搜索...</p>'; loadShares(1, term); } else { this.publicRepoState.isSearching = false; this.publicRepoState.currentPage = 1; this.publicRepoState.isEndOfList = false; contentArea.innerHTML = '<p class="public-repo-loading">正在加载...</p>'; loadShares(1); } }, 500); }); // Infinite scroll contentArea.addEventListener('scroll', () => { if (this.publicRepoState.isLoading) return; const isEnd = this.publicRepoState.isSearching ? this.publicRepoState.isSearchEndOfList : this.publicRepoState.isEndOfList; if (isEnd) return; if (contentArea.scrollTop + contentArea.clientHeight >= contentArea.scrollHeight - 50) { // 50px threshold if (this.publicRepoState.isSearching) { loadShares(this.publicRepoState.currentSearchPage + 1, this.publicRepoState.currentSearchTerm); } else { loadShares(this.publicRepoState.currentPage + 1); } } }); }, showPublicShareContentTreeModal: async function(codeHash) { const modalId = `public-repo-tree-modal-${Date.now()}`; const tempModalElement = document.createElement('div'); tempModalElement.id = modalId; const title = "🌲 目录结构"; let treeContent = `<div id="fl-m-tree-display" style="text-align:left; max-height: 60vh; overflow-y: auto;"><p class="public-repo-loading">正在加载目录树...</p></div>`; this.showModal(title, treeContent, 'publicRepoTree', true, null, null, null, tempModalElement); // Use a new type if needed for styling const treeDisplayArea = tempModalElement.querySelector('#fl-m-tree-display'); try { const result = await publicRepoApiHelper.getContentTree({ codeHash }); if (result.isFinish === true) { if (Array.isArray(result.message) && result.message.length > 0) { treeDisplayArea.innerHTML = result.message.map(line => `<div class="public-repo-tree-item">${line[0].replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")}</div>`).join(''); } else { treeDisplayArea.innerHTML = '<p class="public-repo-empty">此分享内容为空或目录结构无法解析。</p>'; } } else { treeDisplayArea.innerHTML = `<p class="error-message">获取目录树失败: ${result.message}</p>`; } } catch (error) { console.error('获取目录树失败:', error); treeDisplayArea.innerHTML = `<p class="error-message">请求目录树失败: ${error.message}</p>`; } }, async handlePublicRepoImport() { if (!this.publicRepoState.selectedShareForImport) { this.showAlert("请先从列表中选择一个分享进行导入。"); return; } const { codeHash, name } = this.publicRepoState.selectedShareForImport; const importBtn = this.modalElement.querySelector('#fl-m-public-repo-import-btn'); const importStatusEl = this.modalElement.querySelector('#fl-m-public-repo-import-status'); if (importBtn) importBtn.disabled = true; if (importStatusEl) importStatusEl.textContent = `正在处理 "${name}"...`; try { // 1. 获取完整分享码 if (importStatusEl) importStatusEl.textContent = `(1/3) 正在获取 "${name}" 的完整分享码...`; const shareCodeData = await publicRepoApiHelper.getShareCode(codeHash); if (!shareCodeData.isFinish || !shareCodeData.message) { throw new Error(`获取完整分享码失败: ${shareCodeData.message || '未知错误'}`); } const longShareCode = shareCodeData.message; // 2. 转换为123FastLink JSON if (importStatusEl) importStatusEl.textContent = `(2/3) 正在为 "${name}" 转换格式...`; // 使用选择的分享名作为 rootFolderName const fastLinkJsonData = await publicRepoApiHelper.transformShareCodeTo123FastLinkJson(longShareCode, name); if (!fastLinkJsonData.isFinish || typeof fastLinkJsonData.message !== 'object') { throw new Error(`分享码转换为JSON失败: ${fastLinkJsonData.message || '未知错误'}`); } const jsonDataToImport = fastLinkJsonData.message; // 3. 使用脚本自带的JSON导入功能 if (importStatusEl) importStatusEl.textContent = `(3/3) 正在导入 "${name}" 到您的网盘...`; await coreLogic.transferImportedJsonData(jsonDataToImport, ""); // "" 表示导入到当前目录 if (importStatusEl) importStatusEl.textContent = `✅ "${name}" 导入成功!请刷新页面查看。`; } catch (error) { console.error("公共资源库导入失败:", error); if (importStatusEl) importStatusEl.textContent = `❌ 导入 "${name}" 失败: ${error.message}`; this.showError(`导入 "${name}" 失败: ${error.message}`, 5000); } finally { if (importBtn) importBtn.disabled = false; } }, showModal: function(title, content, type = 'info', closable = true, pureLinkForClipboard = null, jsonDataForExport = null, preprocessingFailuresForLog = null, customModalElement = null) { const isOperationalModal = (t) => ['progress_stoppable', 'inputLink', 'inputPublicShare', 'filterSettings', 'showLink', 'publicRepoSettings', 'publicRepoBrowser', 'publicRepoTree'].includes(t); if (this.modalElement && this.activeModalOperationType && this.activeModalOperationType !== type && isOperationalModal(this.activeModalOperationType) && isOperationalModal(type) ) { console.log(`[${SCRIPT_NAME}] Hiding active modal ('${this.activeModalOperationType}') for new modal ('${type}').`); if (this.modalHideCallback) { this.modalHideCallback(); this.modalHideCallback = null; } if (this.modalElement !== customModalElement) { this.modalElement.style.display = 'none'; } } else if (this.modalElement && type !== 'info' && type !== 'error' && this.activeModalOperationType !== type) { if (this.modalElement !== customModalElement) this.hideModal(); } if (customModalElement) { this.modalElement = customModalElement; if (!customModalElement.parentNode) { document.body.appendChild(customModalElement); } if (!this.modalElement.classList.contains('fastlink-modal')) { this.modalElement.classList.add('fastlink-modal'); } this.modalElement.style.display = 'flex'; } else if (this.modalElement && this.modalElement.style.display === 'none' && this.activeModalOperationType === type && isOperationalModal(type)) { this.modalElement.style.display = 'flex'; const titleEl = this.modalElement.querySelector('.fastlink-modal-title'); if (titleEl) titleEl.textContent = title; if (type === 'progress_stoppable') { const stopBtnInstance = this.modalElement.querySelector(`#${processStateManager.getStopButtonId()}`); const cancelBtnInstance = this.modalElement.querySelector('#fl-m-cancel.close-btn'); if (stopBtnInstance) { stopBtnInstance.textContent = processStateManager.isStopRequested() ? "正在停止..." : "🛑 停止"; stopBtnInstance.disabled = processStateManager.isStopRequested(); } if (cancelBtnInstance) { cancelBtnInstance.textContent = processStateManager.isStopRequested() ? "关闭" : "隐藏"; cancelBtnInstance.disabled = stopBtnInstance && !stopBtnInstance.disabled && !processStateManager.isStopRequested(); } } this.activeModalOperationType = type; return; } else { if (this.modalElement) this.modalElement.remove(); this.modalElement = document.createElement('div'); this.modalElement.className = 'fastlink-modal'; document.body.appendChild(this.modalElement); } this.activeModalOperationType = type; this.modalElement.classList.remove('filter-dialog', 'public-repo-tree-modal'); this.modalElement.style.width = '420px'; if (type === 'filterSettings') { this.modalElement.classList.add('filter-dialog'); } else if (type === 'publicRepoTree') { this.modalElement.classList.add('public-repo-tree-modal'); } else if (type === 'publicRepoBrowser'){ this.modalElement.style.width = '500px'; } let htmlContent = `<div class="fastlink-modal-title">${title}</div><div id="${this.MODAL_CONTENT_ID}" class="fastlink-modal-content">`; if (type === 'inputLink') { htmlContent += `<div id="fl-m-drop-area" class="fastlink-drag-drop-area"><textarea id="fl-m-link-input" class="fastlink-modal-input" placeholder="🔗 粘贴秒传链接 或 📂 将文件拖放到此处..." style="min-height: 60px;">${content|| ''}</textarea><div id="fl-m-file-drop-status" style="font-size:0.9em; color:#28a745; margin-top:5px; margin-bottom:5px; min-height:1.2em;"></div><div class="fastlink-file-input-container"><label for="fl-m-file-input">或通过选择文件导入:</label><input type="file" id="fl-m-file-input" accept=".json,.123fastlink,.txt" class="fastlink-modal-file-input"></div></div><div class="folder-selector-container"><label for="fl-folder-selector" class="folder-selector-label">目标文件夹路径 (可选):</label><div class="folder-selector-input-container"><input type="text" id="fl-folder-selector" class="folder-selector-input" placeholder="输入目标文件夹路径,如: 电影/漫威"><div id="fl-folder-dropdown" class="folder-selector-dropdown"></div></div><div id="fl-selected-folders" class="folder-tag-container"></div></div>`; } else if (type === 'inputPublicShare') { htmlContent += `<input type="text" id="fl-m-public-share-key" class="fastlink-modal-input" placeholder="🔑 分享Key 或 完整分享链接"><input type="text" id="fl-m-public-share-pwd" class="fastlink-modal-input" placeholder="🔒 提取码 (如有)"><input type="text" id="fl-m-public-share-fid" class="fastlink-modal-input" value="0" placeholder="📁 起始文件夹ID (默认0为根目录)">`; } else if (type === 'filterSettings') { htmlContent += filterManager.buildFilterModalContent(); } else if (type === 'publicRepoSettings') { htmlContent += content; } else if (type === 'publicRepoBrowser') { htmlContent += content; } else if (type === 'publicRepoTree') { htmlContent += content; } else htmlContent += content; htmlContent += `</div><div class="fastlink-modal-buttons">`; if (type === 'inputLink') { htmlContent += `<button id="fl-m-confirm" class="confirm-btn">➡️ 转存</button><button id="fl-m-cancel" class="cancel-btn">取消</button>`; } else if (type === 'inputPublicShare') { htmlContent += `<button id="fl-m-generate-public" class="confirm-btn">✨ 生成</button><button id="fl-m-cancel" class="cancel-btn">取消</button>`; } else if (type === 'filterSettings') { htmlContent += `<button id="fl-m-save-filters" class="confirm-btn">💾 保存设置</button><button id="fl-m-cancel" class="cancel-btn">取消</button>`; } else if (type === 'publicRepoSettings') { htmlContent += `<button id="fl-m-save-repo-url" class="confirm-btn">💾 保存URL</button><button id="fl-m-cancel" class="cancel-btn">取消</button>`;} else if (type === 'publicRepoBrowser') { htmlContent += `<button id="fl-m-public-repo-import-btn" class="confirm-btn">📥 导入选中项</button><button id="fl-m-cancel" class="cancel-btn">关闭</button>`;} else if (type === 'showLink') { htmlContent += `<div class="submit-to-public-repo-container"> <label for="fl-m-public-repo-sharename">分享名:</label> <input type="text" id="fl-m-public-repo-sharename" class="fastlink-modal-input" value="${jsonDataForExport && jsonDataForExport.commonPath ? jsonDataForExport.commonPath.replace(/\/$/, '') : ''}"> <p class="hint"><b>若您勾选了多个独立的文件/文件夹,导致该输入框内容为空,请手动填写一个总的分享名,否则会将每个勾选项都视为一个独立的分享。</b></p> <button id="fl-m-submit-to-public-repo" class="submit-btn confirm-btn" style="margin-bottom:10px;">⏫ 提交到公共资源库</button> </div>`; if (pureLinkForClipboard || jsonDataForExport) { htmlContent += `<button id="fl-m-copy" class="copy-btn">📋 复制链接</button>`; if (jsonDataForExport) htmlContent += `<button id="fl-m-export-json" class="export-btn">📄 导出为 JSON</button>`; } if (preprocessingFailuresForLog && preprocessingFailuresForLog.length > 0) { htmlContent += `<button id="fl-m-copy-generation-failed-log" class="copy-btn" style="margin-left:10px; background-color: #ff7f50;">📋 复制失败日志 (${preprocessingFailuresForLog.length})</button>`; } htmlContent += `<button id="fl-m-cancel" class="close-btn cancel-btn" style="margin-left:10px;">关闭</button>`; } else if (type === 'progress_stoppable') { htmlContent += `<button id="${processStateManager.getStopButtonId()}" class="stop-btn">🛑 停止</button><button id="fl-m-minimize" class="minimize-btn">最小化</button><button id="fl-m-cancel" class="close-btn cancel-btn" ${processStateManager.isStopRequested() ? '' : 'disabled'}>关闭</button>`; } else if (type === 'info_with_buttons' && preprocessingFailuresForLog && preprocessingFailuresForLog.length > 0) { htmlContent += `<button id="fl-m-copy-preprocessing-log" class="copy-btn">📋 复制日志</button><button id="fl-m-cancel" class="close-btn cancel-btn" style="margin-left:10px;">关闭</button>`; } else { htmlContent += `<button id="fl-m-cancel" class="close-btn cancel-btn">关闭</button>`; } htmlContent += `</div>`; this.modalElement.innerHTML = htmlContent; const confirmBtn = this.modalElement.querySelector('#fl-m-confirm'); if(confirmBtn){ confirmBtn.onclick = async () => { const linkInputEl = this.modalElement.querySelector(`#fl-m-link-input`); const fileInputEl = this.modalElement.querySelector(`#fl-m-file-input`); const folderSelectorEl = this.modalElement.querySelector(`#fl-folder-selector`); let link = linkInputEl ? linkInputEl.value.trim() : null; let file = fileInputEl && fileInputEl.files && fileInputEl.files.length > 0 ? fileInputEl.files[0] : null; let targetFolderPath = folderSelectorEl ? folderSelectorEl.value.trim() : ""; confirmBtn.disabled = true; this.modalElement.querySelector('#fl-m-cancel.cancel-btn')?.setAttribute('disabled', 'true'); if (file) { processStateManager.appendLogMessage(`ℹ️ 从文件 "${file.name}" 导入...`); try { const fileContent = await file.text(); const jsonData = JSON.parse(fileContent); await coreLogic.transferImportedJsonData(jsonData, targetFolderPath); } catch (e) { console.error(`[${SCRIPT_NAME}] 文件导入失败:`, e); processStateManager.appendLogMessage(`❌ 文件导入失败: ${e.message}`, true); uiManager.showError(`文件读取或解析失败: ${e.message}`); } } else if (link) { await coreLogic.transferFromShareLink(link, targetFolderPath); } else { this.showAlert("请输入链接或选择/拖放文件"); } if(this.modalElement && confirmBtn){ confirmBtn.disabled = false; this.modalElement.querySelector('#fl-m-cancel.cancel-btn')?.removeAttribute('disabled'); } }; } const saveFiltersBtn = this.modalElement.querySelector('#fl-m-save-filters'); if(saveFiltersBtn){ saveFiltersBtn.onclick = () => { if (filterManager.saveSettings()){ this.hideModal(); this.showAlert("✅ 过滤器设置已保存!", 1500); } else { this.showError("❌ 保存过滤器设置失败!"); } }; } if (type === 'publicRepoSettings') { const saveRepoUrlBtn = this.modalElement.querySelector('#fl-m-save-repo-url'); const urlInput = this.modalElement.querySelector('#fl-m-public-repo-url'); const statusEl = this.modalElement.querySelector('#fl-m-public-repo-url-status'); if (saveRepoUrlBtn && urlInput && statusEl) { saveRepoUrlBtn.onclick = () => { const newUrl = urlInput.value.trim(); if (!newUrl.startsWith("http://") && !newUrl.startsWith("https://")) { statusEl.textContent = "错误: URL必须以 http:// 或 https:// 开头。"; return; } if (!newUrl.endsWith("/")) { statusEl.textContent = "错误: URL必须以 / 结尾。"; return; } statusEl.textContent = ""; publicRepoApiHelper.setBaseUrl(newUrl); this.hideModal(); this.showAlert("✅ 公共资源库服务器URL已保存!", 1500); }; } } if (type === 'publicRepoBrowser') { const importRepoBtn = this.modalElement.querySelector('#fl-m-public-repo-import-btn'); if (importRepoBtn) { importRepoBtn.onclick = () => this.handlePublicRepoImport(); } } if (type === 'showLink') { const submitToPublicBtn = this.modalElement.querySelector('#fl-m-submit-to-public-repo'); const shareNameInput = this.modalElement.querySelector('#fl-m-public-repo-sharename'); if (submitToPublicBtn && shareNameInput && jsonDataForExport) { submitToPublicBtn.onclick = async () => { let currentJsonData = JSON.parse(JSON.stringify(jsonDataForExport)); let rootFolderNameFromInput = shareNameInput.value.trim(); submitToPublicBtn.disabled = true; submitToPublicBtn.textContent = '检查同名资源...'; // 优化2: 检查数据库中是否有同名资源 if (rootFolderNameFromInput) { // 只有当用户实际输入了根目录名时才检查 try { const searchResult = await publicRepoApiHelper.searchDatabase(rootFolderNameFromInput, 1); if (searchResult.success && searchResult.files && searchResult.files.length > 0) { // 精确匹配检查,API是模糊搜索,我们需要精确匹配 const exactMatch = searchResult.files.find(f => f.name === rootFolderNameFromInput); if (exactMatch) { this.showAlert(`上传失败:数据库已有同名资源 "${rootFolderNameFromInput}"。请前往公共资源库搜索,确保您没有重复提交已有资源。`, 5000); submitToPublicBtn.textContent = '⏫ 提交到公共资源库'; // 恢复按钮文本 submitToPublicBtn.disabled = false; return; // 阻止提交 } } } catch (searchError) { console.warn(`[${SCRIPT_NAME}] 搜索同名资源时发生错误:`, searchError); // 出错也继续尝试上传,避免因为搜索服务不可用导致无法上传 } } // 更新 commonPath if (rootFolderNameFromInput === "" && currentJsonData.commonPath === "") { /*保持原样*/ } else { currentJsonData.commonPath = rootFolderNameFromInput ? rootFolderNameFromInput + "/" : ""; } submitToPublicBtn.textContent = '处理中...'; try { const jsonString = JSON.stringify(currentJsonData); const result = await publicRepoApiHelper.transform123FastLinkJsonToShareCode(jsonString, true, true); if (result.isFinish) { submitToPublicBtn.textContent = '✅ 提交成功'; let successMsg = "提交成功!" + (result.message && Array.isArray(result.message) && result.message.length > 1 ? ` 本次提交生成了 ${result.message.length} 个独立的分享。` : ""); this.showAlert(successMsg, 3000); } else { submitToPublicBtn.textContent = '❌ 提交失败'; this.showError(`提交失败: ${result.message || '未知错误'}`, 4000); } } catch (error) { submitToPublicBtn.textContent = '❌ 提交失败'; this.showError(`提交请求失败: ${error.message}`, 4000); } // 按钮状态由成功/失败决定,不再重置为可点击,除非明确需要 }; } } if(type === 'filterSettings'){ filterManager.attachFilterEvents(); } if (type === 'inputLink') { /* ... existing inputLink event listeners ... */ const dropArea = this.modalElement.querySelector('#fl-m-drop-area'); const fileInputEl = this.modalElement.querySelector(`#fl-m-file-input`); const linkInputEl = this.modalElement.querySelector('#fl-m-link-input'); const statusDiv = this.modalElement.querySelector('#fl-m-file-drop-status'); if (dropArea && fileInputEl && linkInputEl && statusDiv) { linkInputEl.addEventListener('input', () => { if (linkInputEl.value.trim() !== '') { if (fileInputEl.files && fileInputEl.files.length > 0) fileInputEl.value = ''; statusDiv.textContent = ''; } }); fileInputEl.addEventListener('change', () => { if (fileInputEl.files && fileInputEl.files.length > 0) { statusDiv.textContent = `已选中文件: ${fileInputEl.files[0].name}。请点击下方"转存"按钮。`; if(linkInputEl) linkInputEl.value = ''; } else statusDiv.textContent = ''; }); ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => dropArea.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); }, false)); ['dragenter', 'dragover'].forEach(eventName => dropArea.addEventListener(eventName, () => dropArea.classList.add('drag-over-active'), false)); ['dragleave', 'drop'].forEach(eventName => dropArea.addEventListener(eventName, () => dropArea.classList.remove('drag-over-active'), false)); dropArea.addEventListener('drop', (e) => { const dt = e.dataTransfer; if (dt && dt.files && dt.files.length > 0) { const droppedFile = dt.files[0]; if (droppedFile.name.endsWith('.json') || droppedFile.name.endsWith('.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:", err); if (statusDiv) statusDiv.textContent = "处理拖放文件时发生错误。"; } } else { if (statusDiv) statusDiv.textContent = "文件类型无效。请拖放 .json, .123fastlink, 或 .txt 文件。"; } } }, false); } const folderSelector = this.modalElement.querySelector('#fl-folder-selector'); const folderDropdown = this.modalElement.querySelector('#fl-folder-dropdown'); if (folderSelector && folderDropdown) { folderSelector.addEventListener('click', function() { folderDropdown.classList.toggle('active'); }); folderSelector.addEventListener('blur', function() { setTimeout(() => { folderDropdown.classList.remove('active'); }, 200); }); } } const generatePublicBtn = this.modalElement.querySelector('#fl-m-generate-public'); if(generatePublicBtn){ generatePublicBtn.onclick = async () => { const shareKeyEl = this.modalElement.querySelector('#fl-m-public-share-key'); const sharePwdEl = this.modalElement.querySelector('#fl-m-public-share-pwd'); const shareFidEl = this.modalElement.querySelector('#fl-m-public-share-fid'); /* ... logic for generatePublicBtn ... */ const rawShareKeyInput = shareKeyEl ? shareKeyEl.value.trim() : null; let sharePwd = sharePwdEl ? sharePwdEl.value.trim() : null; const shareFid = shareFidEl ? shareFidEl.value.trim() : "0"; let finalShareKey = rawShareKeyInput; if (rawShareKeyInput) { if (rawShareKeyInput.includes('/s/')) { try { let url; try { url = new URL(rawShareKeyInput); } catch (e) { if (!rawShareKeyInput.startsWith('http')) url = new URL('https://' + rawShareKeyInput); else throw e; } const pathSegments = url.pathname.split('/'); const sIndex = pathSegments.indexOf('s'); if (sIndex !== -1 && pathSegments.length > sIndex + 1) { finalShareKey = pathSegments[sIndex + 1]; const searchParams = new URLSearchParams(url.search); const possiblePwdParams = ['pwd', '提取码', 'password', 'extract', 'code']; for (const paramName of possiblePwdParams) { if (searchParams.has(paramName)) { const urlPwd = searchParams.get(paramName); if (urlPwd && (!sharePwd || sharePwd.length === 0)) { sharePwd = urlPwd; if (sharePwdEl) sharePwdEl.value = sharePwd; } break; } } if ((!sharePwd || sharePwd.length === 0)) { const fullUrl = rawShareKeyInput; const pwdRegexes = [ /[?&]提取码[:=]([A-Za-z0-9]+)/, /提取码[:=]([A-Za-z0-9]+)/, /[?&]pwd[:=]([A-Za-z0-9]+)/, /[?&]password[:=]([A-Za-z0-9]+)/ ]; for (const regex of pwdRegexes) { const match = fullUrl.match(regex); if (match && match[1]) { sharePwd = match[1]; if (sharePwdEl) sharePwdEl.value = sharePwd; break; } } } } else { let pathAfterS = rawShareKeyInput.substring(rawShareKeyInput.lastIndexOf('/s/') + 3); finalShareKey = pathAfterS.split(/[/?#]/)[0]; } } catch (e) { let pathAfterS = rawShareKeyInput.substring(rawShareKeyInput.lastIndexOf('/s/') + 3); finalShareKey = pathAfterS.split(/[/?#]/)[0]; if (!sharePwd || sharePwd.length === 0) { const pwdMatch = rawShareKeyInput.match(/提取码[:=]([A-Za-z0-9]+)/); if (pwdMatch && pwdMatch[1]) { sharePwd = pwdMatch[1]; if (sharePwdEl) sharePwdEl.value = sharePwd; } } console.warn(`[${SCRIPT_NAME}] 分享链接解析失败: ${e.message}`); } } if (finalShareKey && finalShareKey.includes('自定义')) finalShareKey = finalShareKey.split('自定义')[0]; } if (!finalShareKey) { this.showAlert("请输入有效的分享Key或分享链接。"); return; } if (isNaN(parseInt(shareFid))) { this.showAlert("起始文件夹ID必须是数字。"); return; } generatePublicBtn.disabled = true; this.modalElement.querySelector('#fl-m-cancel.cancel-btn')?.setAttribute('disabled', 'true'); await coreLogic.generateLinkFromPublicShare(finalShareKey, sharePwd, shareFid); if(this.modalElement && generatePublicBtn){ generatePublicBtn.disabled = false; this.modalElement.querySelector('#fl-m-cancel.cancel-btn')?.removeAttribute('disabled');} };} const copyBtn = this.modalElement.querySelector('#fl-m-copy'); if(copyBtn){ copyBtn.onclick = () => { const textToCopy = pureLinkForClipboard || this.modalElement.querySelector('.fastlink-link-text')?.value; if (textToCopy) { GM_setClipboard(textToCopy); this.showAlert("已复制到剪贴板!");} else this.showError("无法找到链接文本。"); };} const exportJsonBtn = this.modalElement.querySelector('#fl-m-export-json'); if(exportJsonBtn && jsonDataForExport){ exportJsonBtn.onclick = () => { try { 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}`); }};} const copyGenFailedLogBtn = this.modalElement.querySelector('#fl-m-copy-generation-failed-log'); if (copyGenFailedLogBtn && preprocessingFailuresForLog && preprocessingFailuresForLog.length > 0) { copyGenFailedLogBtn.onclick = () => { const logText = preprocessingFailuresForLog.map(pf => `文件: ${pf.fileName || '未知文件'} (ID: ${pf.id || 'N/A'})\n错误: ${pf.error || '未知错误'}\n${pf.etag ? ('ETag: ' + pf.etag + '\n') : ''}${pf.size !== undefined ? ('Size: ' + pf.size + '\n') : ''}`).join('\n'); GM_setClipboard(logText); this.showAlert("失败项目日志已复制到剪贴板!", 1500); }; } const stopBtn = this.modalElement.querySelector(`#${processStateManager.getStopButtonId()}`); if(stopBtn){ stopBtn.onclick = () => { if (confirm("确定要停止当前操作吗?")) { processStateManager.requestStop(); const closeBtnForStop = this.modalElement.querySelector('#fl-m-cancel.close-btn'); if(closeBtnForStop) closeBtnForStop.disabled = false; const minimizeBtnForStop = this.modalElement.querySelector('#fl-m-minimize'); if(minimizeBtnForStop) minimizeBtnForStop.disabled = true; } }; } const minimizeBtn = this.modalElement.querySelector('#fl-m-minimize'); if (minimizeBtn) { minimizeBtn.onclick = () => { if (this.modalElement) this.modalElement.style.display = 'none'; this.showMiniProgress(); processStateManager.updateProgressUINow(); }; } const cancelBtn = this.modalElement.querySelector('#fl-m-cancel.cancel-btn'); if (cancelBtn) { if (type === 'progress_stoppable') { cancelBtn.textContent = processStateManager.isStopRequested() ? "关闭" : "隐藏"; cancelBtn.disabled = !processStateManager.isStopRequested(); cancelBtn.onclick = () => { if (processStateManager.isStopRequested()) { this.hideModal(); } else { if (this.modalElement) this.modalElement.style.display = 'none'; if (this.modalHideCallback) { this.modalHideCallback(); this.modalHideCallback = null; } } }; } else if (closable) { cancelBtn.disabled = false; cancelBtn.onclick = () => this.hideModal(); } else { cancelBtn.disabled = true; } } const copyPreprocessingLogBtn = this.modalElement.querySelector('#fl-m-copy-preprocessing-log'); if(copyPreprocessingLogBtn && preprocessingFailuresForLog) { copyPreprocessingLogBtn.onclick = () => { const logText = preprocessingFailuresForLog.map(pf => `文件: ${pf.fileName || (pf.originalEntry&&pf.originalEntry.path)||'未知路径'}\n${(pf.originalEntry&&pf.originalEntry.etag)?('原始ETag: '+pf.originalEntry.etag+'\n'):(pf.etag?'处理后ETag: '+pf.etag+'\n':'')}${(pf.originalEntry&&pf.originalEntry.size)?('大小: '+pf.originalEntry.size+'\n'):(pf.size?'大小: '+pf.size+'\n':'')}错误: ${pf.error||'未知错误'}`).join('\n\n'); GM_setClipboard(logText); this.showAlert("预处理失败日志已复制到剪贴板!", 1500); };} if (type === 'progress_stoppable') { this.modalHideCallback = () => { const stopBtnInstance = this.modalElement?.querySelector(`#${processStateManager.getStopButtonId()}`); if (stopBtnInstance && !processStateManager.isStopRequested()) stopBtnInstance.textContent = "🛑 停止 (后台)"; }; } if(type === 'inputLink' || type === 'inputPublicShare' || type === 'publicRepoSettings'){ const firstInput = this.modalElement.querySelector('input[type="text"], input[type="url"], textarea'); if(firstInput) setTimeout(() => firstInput.focus(), 100); } }, enableModalCloseButton: function(enable = true) { if (this.modalElement) { const closeBtn = this.modalElement.querySelector('#fl-m-cancel.close-btn'); if (closeBtn) { closeBtn.disabled = !enable; if(enable && this.activeModalOperationType === 'progress_stoppable') closeBtn.textContent = "关闭"; } const stopBtn = this.modalElement.querySelector(`#${processStateManager.getStopButtonId()}`); if (stopBtn && enable) stopBtn.disabled = true; // If enabling close, typically stop is done } }, updateModalContent: function(newContent) { if (this.modalElement) { const ca = this.modalElement.querySelector(`#${this.MODAL_CONTENT_ID}`); if (ca) { if (ca.tagName === 'TEXTAREA' || ca.hasAttribute('contenteditable')) ca.value = newContent; else ca.innerHTML = newContent; ca.scrollTop = ca.scrollHeight;} } }, hideModal: function() { if (this.modalElement) { this.modalElement.remove(); this.modalElement = null; } this.activeModalOperationType = null; this.modalHideCallback = null; }, 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; }, }; function initialize() { console.log(`[${SCRIPT_NAME}] ${SCRIPT_VERSION} 初始化...`); filterManager.init(); uiManager.applyStyles(); uiManager.initMiniProgress(); 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)); } 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."); if (num === 0n) return BASE62_CHARS[0]; let base62 = ""; let n = num; while (n > 0n) { base62 = BASE62_CHARS[Number(n % 62n)] + base62; n = n / 62n; } return base62; } function base62ToBigInt(str) { if (typeof str !== 'string' || str.length === 0) throw new Error("Input must be non-empty string."); let num = 0n; for (let i = 0; i < str.length; i++) { const char = str[i]; const val = BASE62_CHARS.indexOf(char); if (val === -1) throw new Error(`Invalid Base62 char: ${char}`); num = num * 62n + BigInt(val); } return num; } function hexToOptimizedEtag(hexEtag) { if (!isValidHex(hexEtag) || hexEtag.length === 0) return { original: hexEtag, optimized: null, useV2: false }; try { const bigIntValue = BigInt('0x' + hexEtag); const base62Value = bigIntToBase62(bigIntValue); if (base62Value.length > 0 && base62Value.length < hexEtag.length) return { original: hexEtag, optimized: base62Value, useV2: true }; return { original: hexEtag, optimized: hexEtag, useV2: false }; } catch (e) { console.warn(`[${SCRIPT_NAME}] ETag "${hexEtag}" to Base62 failed: ${e.message}. Using original.`); return { original: hexEtag, optimized: null, useV2: false }; } } function optimizedEtagToHex(optimizedEtag, isV2Etag) { if (!isV2Etag) return optimizedEtag; if (typeof optimizedEtag !== 'string' || optimizedEtag.length === 0) throw new Error("V2 ETag cannot be empty."); try { const bigIntValue = base62ToBigInt(optimizedEtag); let hex = bigIntValue.toString(16).toLowerCase(); if (hex.length < 32 && optimizedEtag.length >= 21 && optimizedEtag.length <= 22) hex = hex.padStart(32, '0'); return hex; } catch (e) { throw new Error(`Base62 ETag "${optimizedEtag}" to Hex failed: ${e.message}`); } } })(); 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]; }