// ==UserScript== // @name 123云盘文件批量转存助手 // @name:en 123FastLinkPlus // @version 1.0.2 // @description 一键将夸克网盘、天翼云盘的个人文件和分享链接转存到123云盘 // @author meguoe // @license Apache-2.0 // @match https://pan.quark.cn/* // @match https://drive.quark.cn/* // @match https://pan.quark.cn/s/* // @match https://drive.quark.cn/s/* // @match https://cloud.189.cn/web/* // @icon https://www.google.com/s2/favicons?sz=64&domain=123pan.com // @grant GM_setClipboard // @grant GM_notification // @grant GM_xmlhttpRequest // @grant GM_cookie // @grant GM_getValue // @grant GM_setValue // @run-at document-end // @connect drive.quark.cn // @connect drive-pc.quark.cn // @connect pc-api.uc.cn // @connect cloud.189.cn // @connect www.123pan.com // ==/UserScript== (function () { "use strict"; // 添加统一的CSS样式 const addStyles = () => { if (document.getElementById("fastlink-styles")) return; const style = document.createElement("style"); style.id = "fastlink-styles"; style.textContent = ` /* CSS变量定义 */ :root { --fastlink-primary-color: #1890ff; --fastlink-primary-light: #40a9ff; --fastlink-primary-dark: #096dd9; --fastlink-success-color: #52c41a; --fastlink-success-light: #73d13d; --fastlink-success-dark: #389e0d; --fastlink-error-color: #ff4d4f; --fastlink-error-light: #ff7875; --fastlink-error-dark: #cf1322; --fastlink-warning-color: #faad14; --fastlink-warning-light: #ffc53d; --fastlink-warning-dark: #d48806; --fastlink-info-color: #13c2c2; --fastlink-info-light: #36cfc9; --fastlink-info-dark: #08979c; --fastlink-text-color: #262626; --fastlink-text-secondary: #595959; --fastlink-text-light: #8c8c8c; --fastlink-text-lighter: #bfbfbf; --fastlink-bg-color: #ffffff; --fastlink-bg-light: #fafafa; --fastlink-bg-lighter: #f5f5f5; --fastlink-border-color: #d9d9d9; --fastlink-border-light: #e8e8e8; --fastlink-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); --fastlink-shadow-light: 0 2px 8px rgba(0, 0, 0, 0.1); --fastlink-radius: 8px; --fastlink-radius-small: 4px; --fastlink-radius-large: 12px; --fastlink-font-size: 14px; --fastlink-font-size-small: 12px; --fastlink-font-size-large: 16px; --fastlink-font-size-xl: 18px; --fastlink-spacing: 16px; --fastlink-spacing-small: 8px; --fastlink-spacing-large: 24px; --fastlink-spacing-xl: 32px; --fastlink-transition: all 0.3s ease; --fastlink-transition-fast: all 0.15s ease; } /* 对话框基础样式 */ .fastlink-dialog-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 10000; display: flex; align-items: center; justify-content: center; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; } .fastlink-dialog { background: var(--fastlink-bg-color); padding: var(--fastlink-spacing-large); border-radius: var(--fastlink-radius); width: 90%; max-width: 600px; max-height: 80vh; display: flex; flex-direction: column; box-shadow: var(--fastlink-shadow); animation: fastlink-dialog-fade-in 0.3s ease; } @keyframes fastlink-dialog-fade-in { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } } .fastlink-breadcrumb-item { max-width: 60px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .fastlink-dialog-title { font-size: var(--fastlink-font-size-large); font-weight: 600; margin-bottom: var(--fastlink-spacing); color: var(--fastlink-text-color); display: flex; align-items: center; justify-content: space-between; } .fastlink-dialog-content { flex: 1; overflow-y: hidden; margin-bottom: var(--fastlink-spacing); } .fastlink-dialog-footer { display: flex; gap: var(--fastlink-spacing-small); justify-content: flex-end; margin-top: var(--fastlink-spacing); } /* 按钮样式 */ .fastlink-btn { padding: 10px 24px; border: none; border-radius: var(--fastlink-radius-small); cursor: pointer; font-size: var(--fastlink-font-size); font-weight: 500; transition: var(--fastlink-transition); min-width: 80px; text-align: center; display: inline-flex; align-items: center; justify-content: center; gap: 8px; } .fastlink-btn:hover { box-shadow: var(--fastlink-shadow-light); } .fastlink-btn:active { transition: var(--fastlink-transition-fast); } .fastlink-btn-primary { background: var(--fastlink-primary-color) !important; color: white !important; box-shadow: 0 2px 0 rgba(0, 0, 0, 0.04); } .fastlink-btn-primary:hover { background: var(--fastlink-primary-light); } .fastlink-btn-success { background: var(--fastlink-success-color); color: white; box-shadow: 0 2px 0 rgba(0, 0, 0, 0.04); } .fastlink-btn-success:hover { background: var(--fastlink-success-light); } .fastlink-btn-default { background: var(--fastlink-bg-color); color: var(--fastlink-text-color); border: 1px solid var(--fastlink-border-color); } .fastlink-btn-default:hover { background: var(--fastlink-bg-light); border-color: var(--fastlink-primary-light); } /* 进度条样式 */ .fastlink-progress-container { margin: var(--fastlink-spacing) 0; } .fastlink-progress-text { font-size: var(--fastlink-font-size-small); color: var(--fastlink-text-secondary); font-weight: 500; margin-bottom: var(--fastlink-spacing-small); } .fastlink-progress-bar-container { width: 100%; height: 10px; background: var(--fastlink-bg-lighter); border-radius: var(--fastlink-radius-small); overflow: hidden; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06); } .fastlink-progress-bar { width: 0%; height: 100%; background: linear-gradient(90deg, var(--fastlink-primary-color), var(--fastlink-primary-light)); transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); border-radius: var(--fastlink-radius-small); position: relative; overflow: hidden; } .fastlink-progress-bar::after { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); animation: fastlink-progress-shine 1.5s infinite; } @keyframes fastlink-progress-shine { 0% { left: -100%; } 100% { left: 100%; } } /* 日志样式 */ .fastlink-log-container { margin-top: var(--fastlink-spacing); text-align: left; display: none; } .fastlink-log { max-height: 200px; overflow-y: auto; font-size: var(--fastlink-font-size-small); font-family: monospace; background: var(--fastlink-bg-light); padding: 10px 16px; border-radius: 4px; border: 1px solid var(--fastlink-border-light); } .fastlink-log-item { display: flex; align-items: flex-start; margin-bottom: 4px; padding-bottom: 4px; border-bottom: 1px solid var(--fastlink-border-light); } .fastlink-log-time { color: var(--fastlink-text-light); margin-right: var(--fastlink-spacing-small); min-width: 60px; } .fastlink-log-file { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .fastlink-log-status { margin-left: var(--fastlink-spacing-small); font-weight: 500; } .fastlink-log-status.success { color: var(--fastlink-success-color); } .fastlink-log-status.error { color: var(--fastlink-error-color); } /* 表单样式 */ .fastlink-form-item { margin-bottom: var(--fastlink-spacing); } .fastlink-form-label { display: block; margin-bottom: var(--fastlink-spacing-small); font-weight: 500; color: var(--fastlink-text-color); } .fastlink-form-input { width: 100%; padding: 8px; border: 1px solid var(--fastlink-border-color); border-radius: 4px; font-size: var(--fastlink-font-size); transition: var(--fastlink-transition); } .fastlink-form-input:focus { outline: none; border-color: var(--fastlink-primary-color); box-shadow: 0 0 0 2px rgba(13, 83, 255, 0.1); } .fastlink-form-textarea { width: 100%; min-height: 200px; padding: var(--fastlink-spacing-small); border: 1px solid var(--fastlink-border-color); border-radius: 4px; font-size: var(--fastlink-font-size-small); font-family: monospace; resize: vertical; transition: var(--fastlink-transition); } .fastlink-form-textarea:focus { outline: none; border-color: var(--fastlink-primary-color); box-shadow: 0 0 0 2px rgba(13, 83, 255, 0.1); } /* 错误对话框样式 */ .fastlink-error-icon { color: var(--fastlink-error-color); margin-bottom: var(--fastlink-spacing); text-align: center; } .fastlink-error-title { font-size: var(--fastlink-font-size-large); font-weight: 600; margin-bottom: var(--fastlink-spacing-small); color: var(--fastlink-text-color); text-align: center; } .fastlink-error-message { font-size: var(--fastlink-font-size); color: var(--fastlink-text-secondary); margin-bottom: var(--fastlink-spacing-large); text-align: center; white-space: pre-line; } `; document.head.appendChild(style); }; const utils = { // 对话框缓存 _dialogCache: { loading: null, auth: null, cookie: null, error: null, currentId: 0, }, // 初始化样式 initStyles() { addStyles(); }, getCachedCookie() { return GM_getValue("quark_cookie", ""); }, saveCookie(cookie) { GM_setValue("quark_cookie", cookie); }, getCookie(name) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop().split(";").shift(); return null; }, // 123云盘认证信息管理 get123PanAuth() { return { authToken: GM_getValue("pan123_authToken", ""), loginUuid: GM_getValue("pan123_loginUuid", ""), }; }, save123PanAuth(authToken, loginUuid) { // 只使用GM_setValue存储认证信息,不使用localStorage,提高安全性 GM_setValue("pan123_authToken", authToken); GM_setValue("pan123_loginUuid", loginUuid); }, show123PanAuthDialog(onSave, currentAuth = null) { // 初始化样式 this.initStyles(); const auth = currentAuth || this.get123PanAuth(); const dialog = document.createElement("div"); dialog.id = "pan123-auth-input-dialog"; dialog.className = "fastlink-dialog-overlay"; dialog.innerHTML = ` `; document.body.appendChild(dialog); document.getElementById("pan123-auth-save-btn").onclick = () => { const authToken = document .getElementById("pan123-auth-token") .value.trim(); const loginUuid = document .getElementById("pan123-login-uuid") .value.trim(); if (!authToken || !loginUuid) { alert("认证信息不能为空"); return; } this.save123PanAuth(authToken, loginUuid); dialog.remove(); GM_notification({ text: "123云盘认证信息已保存", timeout: 2000, }); if (onSave) { onSave({ authToken, loginUuid }); } }; document.getElementById("pan123-auth-cancel-btn").onclick = () => { dialog.remove(); }; }, showCookieInputDialog(onSave, currentCookie = "") { // 初始化样式 this.initStyles(); const dialog = document.createElement("div"); dialog.id = "quark-cookie-input-dialog"; dialog.className = "fastlink-dialog-overlay"; dialog.innerHTML = ` `; document.body.appendChild(dialog); document.getElementById("quark-cookie-save-btn").onclick = () => { const cookie = document .getElementById("quark-cookie-input") .value.trim(); if (!cookie) { alert("Cookie不能为空"); return; } this.saveCookie(cookie); dialog.remove(); GM_notification({ text: "Cookie已保存", timeout: 2000, }); if (onSave) { onSave(cookie); } }; document.getElementById("quark-cookie-cancel-btn").onclick = () => { dialog.remove(); }; }, sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }, findReact(dom, traverseUp = 0) { let key = Object.keys(dom).find((key) => { return ( key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$") ); }); let domFiber = dom[key]; if (domFiber == null) { return null; } if (domFiber._currentElement) { let compFiber = domFiber._currentElement._owner; for (let i = 0; i < traverseUp; i++) { compFiber = compFiber._currentElement._owner; } return compFiber._instance; } const GetCompFiber = (fiber) => { let parentFiber = fiber.return; while (typeof parentFiber.type === "string") { parentFiber = parentFiber.return; } return parentFiber; }; let compFiber = GetCompFiber(domFiber); for (let i = 0; i < traverseUp; i++) { compFiber = GetCompFiber(compFiber); } return compFiber.stateNode || compFiber; }, getCurrentPath() { try { const urlParams = new URLSearchParams(window.location.search); const dirFid = urlParams.get("dir_fid"); if (!dirFid || dirFid === "0") { return ""; } const breadcrumb = document.querySelector(".breadcrumb-list"); if (breadcrumb) { const items = breadcrumb.querySelectorAll(".breadcrumb-item"); const pathParts = []; for (let i = 1; i < items.length; i++) { const text = items[i].textContent.trim(); if (text) { pathParts.push(text); } } return pathParts.join("/"); } return ""; } catch (e) { return ""; } }, getSelectedList() { try { const fileListDom = document.getElementsByClassName("file-list")[0]; if (!fileListDom) { return []; } const reactObj = this.findReact(fileListDom); const props = reactObj?.props; if (props) { const fileList = props.list || []; const selectedKeys = props.selectedRowKeys || []; const selectedList = []; fileList.forEach(function (val) { if (selectedKeys.includes(val.fid)) { selectedList.push(val); } }); return selectedList; } return []; } catch (e) { return []; } }, post(url, data, headers = {}) { return new Promise((resolve, reject) => { try { // 验证URL if (!url || typeof url !== "string" || !url.startsWith("http")) { reject(new Error("无效的请求URL")); return; } // 验证数据 if (data === undefined || data === null) { reject(new Error("请求数据不能为空")); return; } const requestData = JSON.stringify(data); const QUARK_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch"; const defaultHeaders = { "Content-Type": "application/json;charset=utf-8", "User-Agent": QUARK_UA, Origin: location.origin, Referer: `${location.origin}/`, Dnt: "", "Cache-Control": "no-cache", Pragma: "no-cache", Expires: "0", }; GM_xmlhttpRequest({ method: "POST", url: url, headers: { ...defaultHeaders, ...headers }, data: requestData, onload: function (response) { try { // 检查响应状态 if (response.status < 200 || response.status >= 300) { reject(new Error(`请求失败,状态码:${response.status}`)); return; } // 尝试解析响应 let result; try { result = JSON.parse(response.responseText); } catch (e) { reject(new Error("响应解析失败")); return; } resolve(result); } catch (e) { reject(new Error("处理响应时出错")); } }, onerror: function (error) { reject(new Error("网络请求失败")); }, ontimeout: function () { reject(new Error("请求超时")); }, }); } catch (e) { reject(new Error(`请求准备失败:${e.message}`)); } }); }, get(url, headers = {}) { return new Promise((resolve, reject) => { try { // 验证URL if (!url || typeof url !== "string" || !url.startsWith("http")) { reject(new Error("无效的请求URL")); return; } GM_xmlhttpRequest({ method: "GET", url: url, headers: headers, onload: function (response) { try { if (response.status >= 200 && response.status < 300) { resolve(response.responseText); } else { reject(new Error(`请求失败: ${response.status}`)); } } catch (e) { reject(new Error("处理响应时出错")); } }, onerror: function (error) { reject(new Error("网络请求失败")); }, ontimeout: function () { reject(new Error("请求超时")); }, }); } catch (e) { reject(new Error(`请求准备失败:${e.message}`)); } }); }, async getFolderFiles(folderId, folderPath = "", onProgress) { const API_URL = "https://drive-pc.quark.cn/1/clouddrive/file/sort?pr=ucpro&fr=pc"; const allFiles = []; let page = 1; const pageSize = 50; while (true) { const url = `${API_URL}&pdir_fid=${folderId}&_page=${page}&_size=${pageSize}&_fetch_total=1&_fetch_sub_dirs=0&_sort=file_type:asc,updated_at:desc`; const result = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, onload: function (response) { try { resolve(JSON.parse(response.responseText)); } catch (e) { reject(new Error("响应解析失败")); } }, onerror: () => reject(new Error("网络请求失败")), }); }); if (result?.code !== 0 || !result?.data?.list) { break; } const items = result.data.list; for (const item of items) { const itemPath = folderPath ? `${folderPath}/${item.file_name}` : item.file_name; if (item.dir) { const subFiles = await this.getFolderFiles( item.fid, itemPath, onProgress ); allFiles.push(...subFiles); } else if (item.file) { allFiles.push({ ...item, path: itemPath }); if (onProgress) { onProgress(); } } } if (items.length < pageSize) { break; } page++; } return allFiles; }, async getShareToken(shareId, passcode = "", cookie = "") { const API_URL = "https://pc-api.uc.cn/1/clouddrive/share/sharepage/token"; try { const result = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: API_URL, headers: { "Content-Type": "application/json", Cookie: cookie, "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", Referer: "https://pan.quark.cn/", }, data: JSON.stringify({ pwd_id: shareId, passcode: passcode, }), onload: function (response) { try { resolve(JSON.parse(response.responseText)); } catch (e) { reject(new Error("响应解析失败")); } }, onerror: () => reject(new Error("网络请求失败")), }); }); if (result?.code === 31001) { throw new Error("请先登录网盘"); } if (result?.code !== 0) { throw new Error( `获取token失败,代码:${result.code},消息:${result.message}` ); } return { stoken: result.data.stoken, title: result.data.title || "", }; } catch (error) { throw error; } }, async getFilesWithMd5(fileList, onProgress) { const API_URL = "https://drive.quark.cn/1/clouddrive/file/download?pr=ucpro&fr=pc"; const BATCH_SIZE = 15; const MAX_PARALLEL = 3; const validFiles = fileList.filter((item) => item.file === true); const pathMap = this._buildPathMap(validFiles); const data = []; let processed = 0; // 分批处理,每批并行发送多个请求 const batches = []; for (let i = 0; i < validFiles.length; i += BATCH_SIZE) { batches.push(validFiles.slice(i, i + BATCH_SIZE)); } // 并行处理批次 for (let i = 0; i < batches.length; i += MAX_PARALLEL) { const parallelBatches = batches.slice(i, i + MAX_PARALLEL); const batchPromises = parallelBatches.map((batch, batchIndex) => { const batchProcessed = processed + batchIndex * BATCH_SIZE; return this._processFileBatch( batch, pathMap, API_URL, data, batchProcessed, validFiles.length, onProgress ); }); await Promise.all(batchPromises); processed += parallelBatches.reduce( (sum, batch) => sum + batch.length, 0 ); // 批次之间添加较小的延迟,避免请求过于密集 if (i + MAX_PARALLEL < batches.length) { await this.sleep(500); } } return data; }, _buildPathMap(files) { const pathMap = {}; files.forEach((file) => { pathMap[file.fid] = file.path; }); return pathMap; }, async _processFileBatch( batch, pathMap, apiUrl, data, processed, total, onProgress ) { const fids = batch.map((item) => item.fid); try { const result = await this.post(apiUrl, { fids }); this._validateApiResponse(result); if (result?.data) { const filesWithPath = this._processFileData(result.data, pathMap); data.push(...filesWithPath); } if (onProgress) { onProgress(processed + batch.length, total); } } catch (error) { throw error; } }, _validateApiResponse(result) { if (result?.code === 31001) { throw new Error("请先登录网盘"); } if (result?.code !== 0) { throw new Error( `获取链接失败,代码:${result.code},消息:${result.message}` ); } }, _processFileData(fileData, pathMap) { return fileData.map((file) => { const newFile = { ...file, path: pathMap[file.fid] || file.file_name, }; let md5 = newFile.md5 || newFile.hash || newFile.etag || ""; md5 = this.decodeMd5(md5); if (md5) { newFile.md5 = md5; } return newFile; }); }, async scanQuarkShareFiles( shareId, stoken, cookie, parentFileId = 0, path = "", recursive = true ) { const fileItems = []; let page = 1; while (true) { const url = this._buildShareDetailUrl( shareId, stoken, parentFileId, page ); const result = await this._fetchShareDetail(url, cookie); if (!this._isValidShareResponse(result)) break; await this._processShareItems( result.data.list, shareId, stoken, cookie, path, recursive, fileItems ); if (this._shouldStopPaging(result.data.list)) break; page++; } return fileItems; }, _buildShareDetailUrl(shareId, stoken, parentFileId, page) { return `https://pc-api.uc.cn/1/clouddrive/share/sharepage/detail?pwd_id=${shareId}&stoken=${encodeURIComponent( stoken )}&pdir_fid=${parentFileId}&_page=${page}&_size=100&pr=ucpro&fr=pc`; }, async _fetchShareDetail(url, cookie) { return await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, headers: { Cookie: cookie, "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0", Referer: "https://pan.quark.cn/", }, onload: function (response) { try { resolve(JSON.parse(response.responseText)); } catch (e) { reject(new Error("响应解析失败")); } }, onerror: () => reject(new Error("网络请求失败")), }); }); }, _isValidShareResponse(result) { return result.code === 0 && result.data?.list; }, _shouldStopPaging(items) { return items.length < 100; }, async _processShareItems( items, shareId, stoken, cookie, path, recursive, fileItems ) { for (const item of items) { const itemPath = path ? `${path}/${item.file_name}` : item.file_name; if (item.dir) { if (recursive) { const subFiles = await this.scanQuarkShareFiles( shareId, stoken, cookie, item.fid, itemPath, true ); fileItems.push(...subFiles); } } else { fileItems.push({ fid: item.fid, token: item.share_fid_token, name: item.file_name, size: item.size, path: itemPath, }); } } }, async batchGetShareFilesMd5( shareId, stoken, cookie, fileItems, onProgress ) { const md5Map = {}; const batchSize = 10; const MAX_PARALLEL = 3; let totalProcessed = 0; // 分批处理,每批并行发送多个请求 const batches = []; for (let i = 0; i < fileItems.length; i += batchSize) { batches.push(fileItems.slice(i, i + batchSize)); } // 并行处理批次 for (let i = 0; i < batches.length; i += MAX_PARALLEL) { const parallelBatches = batches.slice(i, i + MAX_PARALLEL); const batchPromises = parallelBatches.map((batch) => this._processMd5Batch(batch, shareId, stoken, cookie, md5Map) ); await Promise.all(batchPromises); totalProcessed += parallelBatches.reduce( (sum, batch) => sum + batch.length, 0 ); if (onProgress) { onProgress(totalProcessed, fileItems.length); } // 批次之间添加较小的延迟,避免请求过于密集 if (i + MAX_PARALLEL < batches.length) { await this.sleep(500); } } return md5Map; }, async _processMd5Batch(batch, shareId, stoken, cookie, md5Map) { const fids = batch.map((item) => item.fid); const tokens = batch.map((item) => item.token); try { const requestBody = this._buildMd5RequestBody( fids, tokens, shareId, stoken ); const md5Result = await this._fetchMd5Data(requestBody, cookie); this._processMd5Result(md5Result, fids, md5Map); } catch (e) { fids.forEach((fid) => (md5Map[fid] = "")); } }, _buildMd5RequestBody(fids, tokens, shareId, stoken) { return { fids, pwd_id: shareId, stoken, fids_token: tokens, }; }, async _fetchMd5Data(requestBody, cookie) { const url = `https://pc-api.uc.cn/1/clouddrive/file/download?pr=ucpro&fr=pc&uc_param_str=&__dt=${ Math.floor(Math.random() * 4 + 1) * 60 * 1000 }&__t=${Date.now()}`; return await new Promise((resolve) => { GM_xmlhttpRequest({ method: "POST", url: url, headers: { "Content-Type": "application/json", Cookie: cookie, "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/3.14.2 Chrome/112.0.5615.165 Electron/24.1.3.8 Safari/537.36 Channel/pckk_other_ch", Referer: "https://pan.quark.cn/", Accept: "application/json, text/plain, */*", Origin: "https://pan.quark.cn", }, data: JSON.stringify(requestBody), onload: function (response) { try { const parsed = JSON.parse(response.responseText); resolve(parsed); } catch (e) { resolve({ code: -1, message: "解析失败" }); } }, onerror: (error) => { resolve({ code: -1, message: "网络错误" }); }, }); }); }, _processMd5Result(md5Result, fids, md5Map) { if (md5Result.code === 0 && md5Result.data) { const dataList = Array.isArray(md5Result.data) ? md5Result.data : [md5Result.data]; dataList.forEach((item, idx) => { const fid = fids[idx]; if (!fid) return; let md5 = item.md5 || item.hash || ""; md5 = utils.decodeMd5(md5); md5Map[fid] = md5; }); } else { fids.forEach((fid) => (md5Map[fid] = "")); } }, generateRapidTransferJson(filesData) { const files = filesData.map((file) => ({ path: file.path || file.file_name, etag: (file.etag || file.md5 || "").toLowerCase(), size: file.size, })); const totalSize = files.reduce((sum, f) => sum + f.size, 0); return { scriptVersion: "3.0.3", exportVersion: "1.0", usesBase62EtagsInExport: false, commonPath: "", files: files, totalFilesCount: files.length, totalSize: totalSize, }; }, showLoadingDialog(title = "正在转存文件") { // 初始化样式 this.initStyles(); // 检查缓存中是否已有对话框 if (this._dialogCache.loading) { // 更新标题 const titleEl = this._dialogCache.loading.querySelector( ".fastlink-dialog-title" ); if (titleEl) { titleEl.textContent = title; } // 更新进度为0 this.updateProgress(0, 1); // 清空日志 const logEl = this._dialogCache.loading.querySelector( "#fastlink-loading-log" ); if (logEl) { logEl.innerHTML = ""; } // 显示对话框 this._dialogCache.loading.style.display = "flex"; return this._dialogCache.loading; } const dialog = document.createElement("div"); dialog.id = "fastlink-loading-dialog"; dialog.className = "fastlink-dialog-overlay"; dialog.innerHTML = ` `; document.body.appendChild(dialog); dialog.querySelector("#fastlink-loading-close-btn").onclick = () => { this.closeLoadingDialog(); }; dialog.querySelector("#fastlink-loading-open-123pan-btn").onclick = () => { window.open(`https://www.123pan.com/?homeFilePath=${utils._dialogCache.currentId}`, "_blank"); }; // 缓存对话框 this._dialogCache.loading = dialog; return dialog; }, updateProgress(processed, total, phase = "转存") { const titleEl = document.querySelector( "#fastlink-loading-dialog .fastlink-dialog-title" ); const progressBar = document.getElementById( "fastlink-loading-progress-bar" ); const progressText = document.getElementById( "fastlink-loading-progress-text" ); // 更新标题 if (titleEl) { if (phase.includes("转存") || phase.includes("保存")) { titleEl.textContent = "正在转存文件"; } else if (phase.includes("完成")) { titleEl.textContent = "文件转存完成"; } } // 更新进度条和进度文本 if (progressBar && progressText) { const percent = total > 0 ? ((processed / total) * 100).toFixed(1) : 0; progressBar.style.width = `${percent}%`; progressText.style.display = "block"; progressText.textContent = `正在转存 ${processed} / ${total} (${percent}%)`; } }, addSaveLog(fileName, status, error = "") { const logContainer = document.getElementById( "fastlink-loading-log-container" ); const logEl = document.getElementById("fastlink-loading-log"); if (logEl) { // 显示日志容器 if (logContainer) { logContainer.style.display = "block"; } const logItem = document.createElement("div"); logItem.className = "fastlink-log-item"; const timestamp = new Date().toLocaleTimeString(); let statusText = status === "success" ? "✅ 成功" : "❌ 失败"; let statusClass = status === "success" ? "success" : "error"; if (error) { statusText += `: ${error}`; } logItem.innerHTML = ` [${timestamp}] ${fileName} ${statusText} `; logEl.appendChild(logItem); logEl.scrollTop = logEl.scrollHeight; } }, closeLoadingDialog() { if (this._dialogCache.loading) { // 隐藏对话框而不是删除,以便后续重用 this._dialogCache.loading.style.display = "none"; } }, showError(message, showCookieButton = false) { // 初始化样式 this.initStyles(); // 检查缓存中是否已有对话框 if (this._dialogCache.error) { // 更新消息 const messageEl = this._dialogCache.error.querySelector( ".fastlink-error-message" ); if (messageEl) { messageEl.textContent = message; } // 更新按钮状态 const cookieBtn = this._dialogCache.error.querySelector( "#fastlink-error-cookie-btn" ); if (cookieBtn) { cookieBtn.style.display = showCookieButton ? "block" : "none"; } // 显示对话框 this._dialogCache.error.style.display = "flex"; return this._dialogCache.error; } const dialog = document.createElement("div"); dialog.id = "fastlink-error-dialog"; dialog.className = "fastlink-dialog-overlay"; dialog.innerHTML = ` `; document.body.appendChild(dialog); if (showCookieButton) { dialog.querySelector("#fastlink-error-cookie-btn").onclick = () => { dialog.style.display = "none"; this.showCookieInputDialog(null, this.getCachedCookie()); }; } dialog.querySelector("#fastlink-error-close-btn").onclick = () => { dialog.style.display = "none"; }; // 缓存对话框 this._dialogCache.error = dialog; return dialog; }, showFolderSelectDialog(apiClient, onSelect) { // 初始化样式 this.initStyles(); const dialog = document.createElement("div"); dialog.id = "fastlink-folder-select-dialog"; dialog.className = "fastlink-dialog-overlay"; dialog.innerHTML = ` `; document.body.appendChild(dialog); let currentFolderId = "0"; let folderTree = []; // 加载文件夹结构 async function loadFolders(folderId = "0", showLoading = true) { if (showLoading) { document.getElementById("fastlink-folder-loading").style.display = "block"; document.getElementById("fastlink-folder-content").style.display = "none"; } try { const folders = await apiClient.getFolderList(folderId); folderTree = folders; renderFolders(folders); } catch (error) { console.error("[123Link] [PanApiClient]", "加载文件夹失败:", error); utils.showError("加载文件夹失败"); } finally { document.getElementById("fastlink-folder-loading").style.display = "none"; document.getElementById("fastlink-folder-content").style.display = "block"; } } // 渲染文件夹列表 function renderFolders(folders) { const contentEl = document.getElementById("fastlink-folder-content"); contentEl.innerHTML = ""; if (folders.length === 0) { contentEl.innerHTML = '
当前目录无文件夹
'; return; } folders.forEach(folder => { const folderEl = document.createElement("div"); folderEl.className = "fastlink-folder-item"; folderEl.style = ` padding: 8px 12px; border-radius: var(--fastlink-radius-small); cursor: pointer; transition: var(--fastlink-transition); display: flex; align-items: center; gap: 8px; `; folderEl.dataset.fid = folder.fileId; folderEl.dataset.name = folder.fileName; folderEl.innerHTML = ` ${folder.fileName} `; folderEl.onclick = () => { currentFolderId = folder.fileId; updateBreadcrumb(folder.fileId, folder.fileName); loadFolders(folder.fileId); }; folderEl.onmouseenter = () => { folderEl.style.background = "var(--fastlink-bg-light)"; }; folderEl.onmouseleave = () => { folderEl.style.background = "transparent"; }; contentEl.appendChild(folderEl); }); } // 更新面包屑导航 const breadcrumbs = [{ fid: "0", name: "123云盘" }]; function updateBreadcrumb(folderId, folderName) { utils._dialogCache.currentId = folderId; // 判断当前是否包含folderId const folderIndex = breadcrumbs.findIndex(item => item.fid === folderId); if (folderIndex !== -1) { breadcrumbs.splice(folderIndex + 1); } else { breadcrumbs.push({ fid: folderId, name: folderName }); } // 清空并重新构建面包屑 const breadcrumbEl = document.getElementById("fastlink-folder-breadcrumb"); breadcrumbEl.innerHTML = ""; breadcrumbs.forEach((item, index) => { const isLastItem = index === breadcrumbs.length - 1; const breadcrumbItem = document.createElement("span"); breadcrumbItem.className = "fastlink-breadcrumb-item"; breadcrumbItem.dataset.fid = item.fid; breadcrumbItem.textContent = item.name; breadcrumbItem.style = ` transition: var(--fastlink-transition); border-radius: var(--fastlink-radius-small); cursor: ${isLastItem ? "default" : "pointer"}; max-width: ${isLastItem ? "100px" : "60px"}; color: ${isLastItem ? "var(--fastlink-text)" : "var(--fastlink-primary-color)"}; `; if (!isLastItem) { breadcrumbItem.onclick = () => { loadFolders(item.fid); updateBreadcrumb(item.fid, item.name); }; } breadcrumbEl.appendChild(breadcrumbItem); if (index < breadcrumbs.length - 1) { const separator = document.createElement("span"); separator.textContent = "/"; separator.style.color = "var(--fastlink-primary-color)"; breadcrumbEl.appendChild(separator); } }); } // 初始化加载根目录 loadFolders(); // 新建文件夹按钮 document.getElementById("fastlink-create-folder-btn").onclick = async () => { const folderName = document.getElementById("fastlink-new-folder-name").value.trim(); if (!folderName) { alert("请输入文件夹名称"); return; } try { const result = await apiClient.mkdir(currentFolderId, folderName); if (result.success) { document.getElementById("fastlink-new-folder-name").value = ""; loadFolders(currentFolderId); GM_notification({ text: "文件夹创建成功", timeout: 2000 }); } else { throw new Error("创建文件夹失败"); } } catch (error) { console.error("创建文件夹失败:", error); utils.showError("创建文件夹失败"); } }; // 取消按钮 document.getElementById("fastlink-folder-cancel-btn").onclick = () => { dialog.remove(); }; // 选择按钮 document.getElementById("fastlink-folder-select-btn").onclick = () => { if (onSelect) { onSelect(currentFolderId); } dialog.remove(); }; // 搜索功能 const searchInput = document.getElementById("fastlink-folder-search"); searchInput.oninput = () => { const searchTerm = searchInput.value.toLowerCase(); const folderItems = document.querySelectorAll(".fastlink-folder-item"); folderItems.forEach(item => { const folderName = item.dataset.name.toLowerCase(); if (folderName.includes(searchTerm)) { item.style.display = "flex"; } else { item.style.display = "none"; } }); }; return dialog; }, decodeMd5(md5) { if (!md5 || !md5.includes("==")) { return md5 || ""; } try { const binaryString = atob(md5); if (binaryString.length === 16) { return Array.from(binaryString, (char) => char.charCodeAt(0).toString(16).padStart(2, "0") ).join(""); } return ""; } catch (e) { return ""; } }, // 123云盘相关功能 pan123: { // 123云盘API客户端 apiClient: null, // 初始化API客户端 initApiClient() { const auth = utils.get123PanAuth(); this.apiClient = { host: "https://www.123pan.com", authToken: auth.authToken, loginUuid: auth.loginUuid, appVersion: "3", referer: document.location.href, buildURL(path, queryParams) { const queryString = new URLSearchParams( queryParams || {} ).toString(); return `${this.host}${path}?${queryString}`; }, sendRequest(method, path, queryParams, body) { return new Promise((resolve, reject) => { const headers = { "Content-Type": "application/json;charset=UTF-8", Authorization: "Bearer " + this.authToken, platform: "web", "App-Version": this.appVersion, LoginUuid: this.loginUuid, Origin: this.host, Referer: this.referer, }; try { GM_xmlhttpRequest({ method: method, url: this.buildURL(path, queryParams), headers: headers, data: body, withCredentials: true, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data.code === 401) { utils.show123PanAuthDialog(async (newAuth) => { setTimeout(() => generateAndSaveTo123Pan(), 100); }) return; } if (data.code !== 0) { reject(new Error(data.message)); return; } resolve(data); } catch (e) { reject(new Error("解析响应失败: " + e.message)); } }, onerror: (error) => { console.error( "[123Link] [PanApiClient]", "API请求失败:", error ); reject(new Error("网络请求失败: " + error.message)); }, ontimeout: () => { reject(new Error("请求超时")); }, }); } catch (e) { console.error("[123Link] [PanApiClient]", "API请求失败:", e); reject(e); } }); }, async getParentFileId() { return "0"; }, async getFolderList(parentFileId = "0", page = 1, pageSize = 50) { try { const response = await this.sendRequest( "GET", "/b/api/file/list/new", { driveId: "0", parentFileId: parentFileId.toString(), Page: page.toString(), pageSize: pageSize.toString(), limit: pageSize.toString(), next: "0", orderBy: "file_name", orderDirection: "asc", trashed: "false", SearchData: "", OnlyLookAbnormalFile: "0", event: "homeListFile", operateType: "1", inDirectSpace: "false" } ); if (response.code === 0 && response.data) { const folders = response.data.InfoList || []; return folders.filter(item => item.Type === 1).map(item => ({ fileId: item.FileId, fileName: item.FileName, type: item.Type, parentFileId: item.ParentFileId || parentFileId })); } return []; } catch (error) { console.error("[123Link] [PanApiClient]", "获取文件夹列表失败:", error); return []; } }, async getFolderTree(parentFileId = "0", depth = 2) { const folderTree = []; const visited = new Set(); async function traverse(currentId, currentDepth) { if (currentDepth > depth || visited.has(currentId)) { return; } visited.add(currentId); const folders = await this.getFolderList(currentId); for (const folder of folders) { const folderNode = { ...folder, children: [] }; folderTree.push(folderNode); if (currentDepth < depth) { folderNode.children = await traverse.call(this, folder.fileId, currentDepth + 1); } } return folderTree; } return await traverse.call(this, parentFileId, 1); }, async uploadRequest(fileInfo) { try { const response = await this.sendRequest( "POST", "/b/api/file/upload_request", {}, JSON.stringify({ ...fileInfo, RequestSource: null, }) ); const reuse = response["data"]["Reuse"]; if (response["code"] !== 0) { return [false, response["message"]]; } if (!reuse) { console.error( "[123Link] [PanApiClient]", "保存文件失败:", fileInfo.fileName, "response:", response ); return [false, null]; } else { return [true, null]; } } catch (error) { console.error("[123Link] [PanApiClient]", "上传请求失败:", error); return [false, "请求失败"]; } }, async getFile(fileInfo, parentFileId) { if (!parentFileId) { parentFileId = await this.getParentFileId(); } return await this.uploadRequest({ driveId: 0, etag: fileInfo.etag, fileName: fileInfo.fileName, parentFileId, size: fileInfo.size, type: 0, duplicate: 1, }); }, async mkdir(parentFileId, folderName = "New Folder") { let folderFileId = null; try { const response = await this.sendRequest( "POST", "/b/api/file/upload_request", {}, JSON.stringify({ driveId: 0, etag: "", fileName: folderName, parentFileId, size: 0, type: 1, duplicate: 1, NotReuse: true, event: "newCreateFolder", operateType: 1, RequestSource: null, }) ); folderFileId = response["data"]["Info"]["FileId"]; } catch (error) { console.error( "[123Link] [PanApiClient]", "创建文件夹失败:", error ); return { folderFileId: null, folderName: folderName, success: false, }; } return { folderFileId: folderFileId, folderName: folderName, success: true, }; }, }; return this.apiClient; }, // 保存JSON格式的秒链到123云盘 async saveJsonShareLink(jsonData, onProgress, targetFolderId = "0") { try { // 初始化API客户端 const apiClient = this.initApiClient(); // 检查是否登录 if (!apiClient.authToken) { throw new Error("请先登录123云盘"); } // 解析JSON数据 const shareFileList = this._parseJsonShareLink(jsonData); // 创建文件夹结构 const filesWithParentId = await this._makeDirForFiles( shareFileList, apiClient, onProgress, targetFolderId ); // 保存文件 const saveResult = await this._saveFileList( filesWithParentId, apiClient, onProgress ); return saveResult; } catch (error) { console.error("[123Link] [pan123]", "保存失败:", error); throw error; } }, // 解析JSON格式的秒链 _parseJsonShareLink(jsonData) { const commonPath = jsonData["commonPath"] || ""; const shareFileList = jsonData["files"]; if (jsonData["usesBase62EtagsInExport"]) { shareFileList.forEach((file) => { file.etag = this._base62ToHex(file.etag); }); } shareFileList.forEach((file) => { file.fileName = file.path.split("/").pop(); }); return { files: shareFileList, commonPath: commonPath, }; }, // 创建文件夹结构 async _makeDirForFiles(shareData, apiClient, onProgress, targetFolderId = "0") { const { files, commonPath } = shareData; const total = files.length; const folder = {}; // 使用指定的目标文件夹ID或默认根文件夹ID const rootFolderId = targetFolderId; // 创建commonPath对应的文件夹结构 if (commonPath) { const commonPathParts = commonPath .split("/") .filter((part) => part !== ""); let currentParentId = rootFolderId; for (let i = 0; i < commonPathParts.length; i++) { const currentPath = commonPathParts.slice(0, i + 1).join("/"); const folderName = commonPathParts[i]; if (!folder[currentPath]) { const newFolder = await apiClient.mkdir( currentParentId, folderName ); await new Promise((resolve) => setTimeout(resolve, 100)); folder[currentPath] = newFolder.folderFileId; } currentParentId = folder[currentPath]; } } else { folder[""] = rootFolderId; } // 为每个文件创建对应的文件夹结构并添加parentFolderId for (let i = 0; i < files.length; i++) { const item = files[i]; const itemPath = item.path.split("/").slice(0, -1); // 从commonPath或根文件夹开始 let nowParentFolderId = folder[commonPath.slice(0, -1)] || rootFolderId; // 创建文件路径对应的文件夹结构 for (let j = 0; j < itemPath.length; j++) { const path = itemPath.slice(0, j + 1).join("/"); if (!folder[path]) { const newFolderID = await apiClient.mkdir( nowParentFolderId, itemPath[j] ); await new Promise((resolve) => setTimeout(resolve, 100)); folder[path] = newFolderID.folderFileId; nowParentFolderId = newFolderID.folderFileId; } else { nowParentFolderId = folder[path]; } } // 添加parentFolderId到文件信息 files[i].parentFolderId = nowParentFolderId; // 调用进度回调 if (onProgress) { onProgress(i + 1, total, item.fileName, 0, 0); } } return files; }, // 保存文件列表 async _saveFileList(shareFileList, apiClient, onProgress) { const total = shareFileList.length; const successList = []; const failedList = []; for (let i = 0; i < shareFileList.length; i++) { const fileInfo = shareFileList[i]; if (i > 0) { await new Promise((resolve) => setTimeout(resolve, 100)); } await this._saveSingleFile( fileInfo, apiClient, successList, failedList, i, total, onProgress ); } utils.updateProgress(1, 1, "完成"); return { success: successList, failed: failedList, }; }, async _saveSingleFile( fileInfo, apiClient, successList, failedList, index, total, onProgress ) { try { const reuse = await apiClient.getFile( { etag: fileInfo.etag, size: fileInfo.size, fileName: fileInfo.fileName, }, fileInfo.parentFolderId ); if (reuse[0]) { this._handleSaveSuccess(fileInfo, successList); } else { this._handleSaveFailure(fileInfo, failedList, reuse[1]); } // 调用进度回调 if (onProgress) { const completed = index + 1; const success = successList.length; const failed = failedList.length; utils.updateProgress(completed, total); onProgress(completed, total, fileInfo.fileName, success, failed); } } catch (error) { this._handleSaveFailure(fileInfo, failedList, error.message); console.error( "[123Link] [pan123]", "保存文件异常:", fileInfo.fileName, error ); } }, _handleSaveSuccess(fileInfo, successList) { successList.push(fileInfo); utils.addSaveLog(fileInfo.fileName, "success"); }, _handleSaveFailure(fileInfo, failedList, error) { fileInfo.error = error; failedList.push(fileInfo); utils.addSaveLog(fileInfo.fileName, "failed", error); }, // Base62转换相关方法 _base62chars() { return "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; }, _base62ToHex(base62) { if (!base62) return ""; const chars = this._base62chars(); let num = 0n; for (let i = 0; i < base62.length; i++) { num = num * 62n + BigInt(chars.indexOf(base62[i])); } let hex = num.toString(16); if (hex.length % 2) hex = "0" + hex; while (hex.length < 32) hex = "0" + hex; return hex; }, }, }; const tianyiService = { getSelectedFiles() { try { if (typeof unsafeWindow !== "undefined") { let list; if (/\/web\/share/.test(location.href)) { list = unsafeWindow.shareUser?.getSelectedFileList(); } else { list = unsafeWindow.file?.getSelectedFileList(); } if (list && list.length > 0) { return list; } } } catch (e) { // ignore } const selectedItems = []; let selectedElements = document.querySelectorAll("li.c-file-item-select"); if (selectedElements.length === 0) { const checkedBoxes = document.querySelectorAll(".ant-checkbox-checked"); if (checkedBoxes.length > 0) { selectedElements = Array.from(checkedBoxes) .map((box) => box.closest("li.c-file-item")) .filter((el) => el); } } if (selectedElements.length === 0) { return []; } selectedElements.forEach((itemEl) => { if (itemEl.__vue__) { const vueInstance = itemEl.__vue__; const fileData = vueInstance.fileItem || vueInstance.fileInfo || vueInstance.item || vueInstance.file; if (fileData) { if ( !selectedItems.some( (item) => item.fileId === (fileData.id || fileData.fileId) ) ) { const normalizedItem = { fileId: fileData.id || fileData.fileId, fileName: fileData.name || fileData.fileName, isFolder: fileData.isFolder || fileData.fileCata === 2, md5: fileData.md5, size: fileData.size, }; selectedItems.push(normalizedItem); } } } }); return selectedItems; }, async getPersonalFolderFiles(folderId, path = "", onProgress = null) { const files = []; let pageNum = 1; const pageSize = 100; while (true) { const appKey = "600100422"; const timestamp = Date.now().toString(); const urlParams = { folderId: folderId, pageNum: pageNum, pageSize: pageSize, orderBy: "lastOpTime", descending: "true", }; const signParams = { ...urlParams, Timestamp: timestamp, AppKey: appKey, }; const signature = this.get189Signature(signParams); const url = `https://cloud.189.cn/api/open/file/listFiles.action?${new URLSearchParams( urlParams )}`; const text = await utils.get(url, { Accept: "application/json;charset=UTF-8", "Sign-Type": "1", Signature: signature, Timestamp: timestamp, AppKey: appKey, }); const data = JSON.parse(text); if (data.res_code !== 0) break; const fileList = data.fileListAO?.fileList || []; const folderList = data.fileListAO?.folderList || []; if (fileList.length === 0 && folderList.length === 0) break; for (const file of fileList) { const filePath = path ? `${path}/${file.name}` : file.name; files.push({ path: filePath, etag: (file.md5 || "").toLowerCase(), size: file.size, fileId: file.id, }); if (onProgress) onProgress(); } for (const folder of folderList) { const folderPath = path ? `${path}/${folder.name}` : folder.name; const subFiles = await this.getPersonalFolderFiles( folder.id, folderPath, onProgress ); files.push(...subFiles); } if (fileList.length + folderList.length < pageSize) break; pageNum++; } return files; }, async getBaseShareInfo(shareUrl, sharePwd) { let match = shareUrl.match(/\/t\/([a-zA-Z0-9]+)/) || shareUrl.match(/[?&]code=([a-zA-Z0-9]+)/); if (!match) throw new Error("无效的189网盘分享链接"); const shareCode = match[1]; let accessCode = sharePwd || ""; if (!accessCode) { const cookieName = `share_${shareCode}`; const cookiePwd = utils.getCookie(cookieName); if (cookiePwd) { accessCode = cookiePwd; } else { try { const decodedUrl = decodeURIComponent(shareUrl); const pwdMatch = decodedUrl.match( /[((]访问码[::]\s*([a-zA-Z0-9]+)/ ); if (pwdMatch && pwdMatch[1]) { accessCode = pwdMatch[1]; } } catch (e) { /* ignore decoding errors */ } } } let shareId = shareCode; if (accessCode) { const checkUrl = `https://cloud.189.cn/api/open/share/checkAccessCode.action?shareCode=${shareCode}&accessCode=${accessCode}`; try { const checkText = await utils.get(checkUrl, { Accept: "application/json;charset=UTF-8", Referer: "https://cloud.189.cn/web/main/", }); const checkData = JSON.parse(checkText); if (checkData.shareId) shareId = checkData.shareId; } catch (e) { /* ignore */ } } const params = { shareCode, accessCode: accessCode }; const timestamp = Date.now().toString(); const appKey = "600100422"; const signData = { ...params, Timestamp: timestamp, AppKey: appKey }; const signature = this.get189Signature(signData); const apiUrl = `https://cloud.189.cn/api/open/share/getShareInfoByCodeV2.action?${new URLSearchParams( params )}`; const text = await utils.get(apiUrl, { Accept: "application/json;charset=UTF-8", "Sign-Type": "1", Signature: signature, Timestamp: timestamp, AppKey: appKey, Referer: "https://cloud.189.cn/web/main/", }); let data; try { data = JSON.parse( text.replace( /"(id|fileId|parentId|shareId)":"?(\d{15,})"?/g, '"$1":"$2"' ) ); } catch (e) { throw new Error("解析分享信息失败"); } if (data.res_code !== 0) { if (data.res_code === 40401 && !accessCode) throw new Error("该分享需要提取码,请输入提取码"); throw new Error(`获取分享信息失败: ${data.res_message || "未知错误"}`); } return { shareId: data.shareId || shareId, shareMode: data.shareMode || "0", accessCode: accessCode, shareCode: shareCode, title: data.fileName || "", }; }, async get189ShareFiles( shareId, shareDirFileId, fileId, path = "", shareMode = "0", accessCode = "", shareCode = "", onProgress = null ) { const files = []; let page = 1; while (true) { const params = { pageNum: page.toString(), pageSize: "100", fileId: fileId.toString(), shareDirFileId: shareDirFileId.toString(), isFolder: "true", shareId: shareId.toString(), shareMode: shareMode, iconOption: "5", orderBy: "lastOpTime", descending: "true", accessCode: accessCode || "", }; const queryString = new URLSearchParams(params).toString(); const url = `https://cloud.189.cn/api/open/share/listShareDir.action?${queryString}`; const headers = { Accept: "application/json;charset=UTF-8", Referer: "https://cloud.189.cn/web/main/", }; if (shareCode && accessCode) { headers["Cookie"] = `share_${shareCode}=${accessCode}`; } const text = await utils.get(url, headers); let data; try { const fixedText = text.replace( /"(id|fileId|parentId|shareId)":(\d{15,})/g, '"$1":"$2"' ); data = JSON.parse(fixedText); } catch (e) { break; } if (data.res_code !== 0) { break; } const fileList = data.fileListAO?.fileList || []; const folderList = data.fileListAO?.folderList || []; for (const file of fileList) { const filePath = path ? `${path}/${file.name}` : file.name; files.push({ path: filePath, etag: (file.md5 || "").toLowerCase(), size: file.size, }); if (onProgress) onProgress(); } for (const folder of folderList) { const folderPath = path ? `${path}/${folder.name}` : folder.name; const subFiles = await this.get189ShareFiles( shareId, folder.id, folder.id, folderPath, shareMode, accessCode, shareCode, onProgress ); files.push(...subFiles); } if (fileList.length + folderList.length < 100) { break; } page++; } return files; }, get189Signature(params) { const sortedKeys = Object.keys(params).sort(); const sortedParams = sortedKeys .map((key) => `${key}=${params[key]}`) .join("&"); return this.simpleMD5(sortedParams); }, simpleMD5(str) { function rotateLeft(value, shift) { return (value << shift) | (value >>> (32 - shift)); } function addUnsigned(x, y) { const lsw = (x & 0xffff) + (y & 0xffff); const msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xffff); } function F(x, y, z) { return (x & y) | (~x & z); } function G(x, y, z) { return (x & z) | (y & ~z); } function H(x, y, z) { return x ^ y ^ z; } function I(x, y, z) { return y ^ (x | ~z); } function FF(a, b, c, d, x, s, ac) { a = addUnsigned(a, addUnsigned(addUnsigned(F(b, c, d), x), ac)); return addUnsigned(rotateLeft(a, s), b); } function GG(a, b, c, d, x, s, ac) { a = addUnsigned(a, addUnsigned(addUnsigned(G(b, c, d), x), ac)); return addUnsigned(rotateLeft(a, s), b); } function HH(a, b, c, d, x, s, ac) { a = addUnsigned(a, addUnsigned(addUnsigned(H(b, c, d), x), ac)); return addUnsigned(rotateLeft(a, s), b); } function II(a, b, c, d, x, s, ac) { a = addUnsigned(a, addUnsigned(addUnsigned(I(b, c, d), x), ac)); return addUnsigned(rotateLeft(a, s), b); } function convertToWordArray(str) { const lWordCount = ((str.length + 8) >>> 6) + 1; const lMessageLength = lWordCount * 16; const lWordArray = new Array(lMessageLength - 1); let lBytePosition = 0; let lByteCount = 0; while (lByteCount < str.length) { const lWordIndex = (lByteCount - (lByteCount % 4)) / 4; lBytePosition = (lByteCount % 4) * 8; lWordArray[lWordIndex] = lWordArray[lWordIndex] | (str.charCodeAt(lByteCount) << lBytePosition); lByteCount++; } const lWordIndex = (lByteCount - (lByteCount % 4)) / 4; lBytePosition = (lByteCount % 4) * 8; lWordArray[lWordIndex] = lWordArray[lWordIndex] | (0x80 << lBytePosition); lWordArray[lMessageLength - 2] = str.length << 3; lWordArray[lMessageLength - 1] = str.length >>> 29; return lWordArray; } function wordToHex(value) { let result = ""; for (let i = 0; i <= 3; i++) { const byte = (value >>> (i * 8)) & 255; result += ("0" + byte.toString(16)).slice(-2); } return result; } const x = convertToWordArray(str); let a = 0x67452301, b = 0xefcdab89, c = 0x98badcfe, d = 0x10325476; const S11 = 7, S12 = 12, S13 = 17, S14 = 22; const S21 = 5, S22 = 9, S23 = 14, S24 = 20; const S31 = 4, S32 = 11, S33 = 16, S34 = 23; const S41 = 6, S42 = 10, S43 = 15, S44 = 21; for (let k = 0; k < x.length; k += 16) { const AA = a, BB = b, CC = c, DD = d; a = FF(a, b, c, d, x[k + 0], S11, 0xd76aa478); d = FF(d, a, b, c, x[k + 1], S12, 0xe8c7b756); c = FF(c, d, a, b, x[k + 2], S13, 0x242070db); b = FF(b, c, d, a, x[k + 3], S14, 0xc1bdceee); a = FF(a, b, c, d, x[k + 4], S11, 0xf57c0faf); d = FF(d, a, b, c, x[k + 5], S12, 0x4787c62a); c = FF(c, d, a, b, x[k + 6], S13, 0xa8304613); b = FF(b, c, d, a, x[k + 7], S14, 0xfd469501); a = FF(a, b, c, d, x[k + 8], S11, 0x698098d8); d = FF(d, a, b, c, x[k + 9], S12, 0x8b44f7af); c = FF(c, d, a, b, x[k + 10], S13, 0xffff5bb1); b = FF(b, c, d, a, x[k + 11], S14, 0x895cd7be); a = FF(a, b, c, d, x[k + 12], S11, 0x6b901122); d = FF(d, a, b, c, x[k + 13], S12, 0xfd987193); c = FF(c, d, a, b, x[k + 14], S13, 0xa679438e); b = FF(b, c, d, a, x[k + 15], S14, 0x49b40821); a = GG(a, b, c, d, x[k + 1], S21, 0xf61e2562); d = GG(d, a, b, c, x[k + 6], S22, 0xc040b340); c = GG(c, d, a, b, x[k + 11], S23, 0x265e5a51); b = GG(b, c, d, a, x[k + 0], S24, 0xe9b6c7aa); a = GG(a, b, c, d, x[k + 5], S21, 0xd62f105d); d = GG(d, a, b, c, x[k + 10], S22, 0x2441453); c = GG(c, d, a, b, x[k + 15], S23, 0xd8a1e681); b = GG(b, c, d, a, x[k + 4], S24, 0xe7d3fbc8); a = GG(a, b, c, d, x[k + 9], S21, 0x21e1cde6); d = GG(d, a, b, c, x[k + 14], S22, 0xc33707d6); c = GG(c, d, a, b, x[k + 3], S23, 0xf4d50d87); b = GG(b, c, d, a, x[k + 8], S24, 0x455a14ed); a = GG(a, b, c, d, x[k + 13], S21, 0xa9e3e905); d = GG(d, a, b, c, x[k + 2], S22, 0xfcefa3f8); c = GG(c, d, a, b, x[k + 7], S23, 0x676f02d9); b = GG(b, c, d, a, x[k + 12], S24, 0x8d2a4c8a); a = HH(a, b, c, d, x[k + 5], S31, 0xfffa3942); d = HH(d, a, b, c, x[k + 8], S32, 0x8771f681); c = HH(c, d, a, b, x[k + 11], S33, 0x6d9d6122); b = HH(b, c, d, a, x[k + 14], S34, 0xfde5380c); a = HH(a, b, c, d, x[k + 1], S31, 0xa4beea44); d = HH(d, a, b, c, x[k + 4], S32, 0x4bdecfa9); c = HH(c, d, a, b, x[k + 7], S33, 0xf6bb4b60); b = HH(b, c, d, a, x[k + 10], S34, 0xbebfbc70); a = HH(a, b, c, d, x[k + 13], S31, 0x289b7ec6); d = HH(d, a, b, c, x[k + 0], S32, 0xeaa127fa); c = HH(c, d, a, b, x[k + 3], S33, 0xd4ef3085); b = HH(b, c, d, a, x[k + 6], S34, 0x4881d05); a = HH(a, b, c, d, x[k + 9], S31, 0xd9d4d039); d = HH(d, a, b, c, x[k + 12], S32, 0xe6db99e5); c = HH(c, d, a, b, x[k + 15], S33, 0x1fa27cf8); b = HH(b, c, d, a, x[k + 2], S34, 0xc4ac5665); a = II(a, b, c, d, x[k + 0], S41, 0xf4292244); d = II(d, a, b, c, x[k + 7], S42, 0x432aff97); c = II(c, d, a, b, x[k + 14], S43, 0xab9423a7); b = II(b, c, d, a, x[k + 5], S44, 0xfc93a039); a = II(a, b, c, d, x[k + 12], S41, 0x655b59c3); d = II(d, a, b, c, x[k + 3], S42, 0x8f0ccc92); c = II(c, d, a, b, x[k + 10], S43, 0xffeff47d); b = II(b, c, d, a, x[k + 1], S44, 0x85845dd1); a = II(a, b, c, d, x[k + 8], S41, 0x6fa87e4f); d = II(d, a, b, c, x[k + 15], S42, 0xfe2ce6e0); c = II(c, d, a, b, x[k + 6], S43, 0xa3014314); b = II(b, c, d, a, x[k + 13], S44, 0x4e0811a1); a = II(a, b, c, d, x[k + 4], S41, 0xf7537e82); d = II(d, a, b, c, x[k + 11], S42, 0xbd3af235); c = II(c, d, a, b, x[k + 2], S43, 0x2ad7d2bb); b = II(b, c, d, a, x[k + 9], S44, 0xeb86d391); a = addUnsigned(a, AA); b = addUnsigned(b, BB); c = addUnsigned(c, CC); d = addUnsigned(d, DD); } return ( wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d) ).toLowerCase(); }, }; async function generateAndSaveTo123Pan() { try { // 先检查123云盘认证信息 const auth = utils.get123PanAuth(); if (!auth.authToken || !auth.loginUuid) { utils.closeLoadingDialog(); if ( confirm( "未检测到123云盘认证信息,请先设置认证信息后再转存。\n\n点击确定进入设置,点击取消取消转存操作。" ) ) { utils.show123PanAuthDialog(async (newAuth) => { setTimeout(() => generateAndSaveTo123Pan(), 100); }); } return; } // 先检查是否有已选择的文件或文件夹 const hostname = location.hostname; let hasSelectedFiles = false; if (hostname.includes("cloud.189.cn")) { const selectedFiles = tianyiService.getSelectedFiles(); hasSelectedFiles = selectedFiles.length > 0; } else if (hostname.includes("quark.cn")) { const selectedItems = utils.getSelectedList(); hasSelectedFiles = selectedItems.length > 0; } if (!hasSelectedFiles) { utils.showError("请选择要转存的文件或文件夹"); return; } // 初始化API客户端以获取文件夹结构 const apiClient = utils.pan123.initApiClient(); // 先显示文件夹选择对话框 utils.showFolderSelectDialog(apiClient, async (targetFolderId) => { try { const path = location.pathname; let json = null; let shareTitle = ""; if (hostname.includes("cloud.189.cn")) { if (path.startsWith("/web/main")) { json = await generateTianyiHomeJsonInternal(); } else { const result = await generateTianyiShareJsonInternal(); json = result.json; shareTitle = result.title; } } else if (hostname.includes("quark.cn")) { const isSharePage = /^\/(s|share)\//.test(path); if (isSharePage) { const match = location.pathname.match(/\/(s|share)\/([a-zA-Z0-9]+)/); if (!match) { throw new Error("无法获取分享ID"); } const shareId = match[2]; let cookie = utils.getCachedCookie(); if (!cookie || cookie.length < 10) { utils.showCookieInputDialog((newCookie) => { setTimeout(() => generateAndSaveTo123Pan(), 100); }); return; } const result = await generateShareJsonInternal(shareId, cookie); json = result.json; shareTitle = result.title; } else { json = await generateHomeJsonInternal(); } } if (json) { // 修改saveTo123PanInternal,使其接受targetFolderId参数 await saveTo123PanInternal(json, shareTitle, targetFolderId); } } catch (error) { utils.closeLoadingDialog(); utils.showError(error.message || "转存到123云盘失败"); } }); } catch (error) { utils.closeLoadingDialog(); utils.showError(error.message || "转存到123云盘失败"); } } async function saveTo123PanInternal(json, shareTitle = "", targetFolderId = "0") { try { const auth = utils.get123PanAuth(); if (!auth.authToken || !auth.loginUuid) { utils.closeLoadingDialog(); if ( confirm( "未检测到123云盘认证信息,请先设置认证信息后再转存。\n\n点击确定进入设置,点击取消取消转存操作。" ) ) { utils.show123PanAuthDialog(async (newAuth) => { await saveTo123PanInternal(json, shareTitle, targetFolderId); }); } return; } try { let percent = 0; const saveResult = await utils.pan123.saveJsonShareLink( json, (completed, total) => { percent = total > 0 ? Math.round((completed / total) * 100) : 100; utils.updateProgress(completed, total); }, targetFolderId ); const successCount = saveResult.success.length; const failedCount = saveResult.failed.length; if (percent === 100) { const progressText = document.getElementById( "fastlink-loading-progress-text" ); progressText.textContent = `✅ 成功: ${successCount}, ❌ 失败: ${failedCount}`; } } catch (error) { console.error("转存到123云盘失败:", error); utils.showError(`转存失败: ${error.message}`); } } catch (error) { console.error("转存到123云盘失败:", error); utils.showError(`转存失败: ${error.message}`); } } async function generateTianyiShareJsonInternal() { utils.showLoadingDialog("正在转存文件", "准备中..."); try { const selectedFiles = tianyiService.getSelectedFiles(); if (selectedFiles.length === 0) { utils.closeLoadingDialog(); throw new Error("请选择要转存的文件或文件夹"); } const shareUrl = window.location.href; let sharePwd = ""; const allFiles = []; let filesFound = 0; const onProgress = () => { filesFound++; }; const { shareId, shareMode, accessCode, shareCode, title } = await tianyiService.getBaseShareInfo(shareUrl, sharePwd); for (const item of selectedFiles) { if (item.isFolder) { const folderPath = item.fileName; const subFiles = await tianyiService.get189ShareFiles( shareId, item.fileId, item.fileId, folderPath, shareMode, accessCode, shareCode, onProgress ); allFiles.push(...subFiles); } else { allFiles.push({ path: item.fileName, etag: (item.md5 || "").toLowerCase(), size: item.size, }); onProgress(); } } await utils.sleep(300); const finalJson = utils.generateRapidTransferJson(allFiles); return { json: finalJson, title }; } catch (error) { utils.closeLoadingDialog(); throw error; } } async function generateTianyiHomeJsonInternal() { utils.showLoadingDialog("正在转存文件", "准备中..."); try { const selectedFiles = tianyiService.getSelectedFiles(); if (selectedFiles.length === 0) { utils.closeLoadingDialog(); throw new Error("请先勾选要生成JSON的文件或文件夹"); } const allFiles = []; let filesFound = 0; const onProgress = () => { filesFound++; }; for (const item of selectedFiles) { if (item.isFolder) { const subFiles = await tianyiService.getPersonalFolderFiles( item.fileId, item.fileName, onProgress ); allFiles.push(...subFiles); } else { allFiles.push({ path: item.fileName, size: item.size, fileId: item.fileId, etag: (item.md5 || "").toLowerCase(), }); onProgress(); } } await utils.sleep(300); const finalJson = utils.generateRapidTransferJson(allFiles); return finalJson; } catch (error) { utils.closeLoadingDialog(); throw error; } } async function generateHomeJsonInternal() { utils.showLoadingDialog("正在转存文件", "准备中..."); try { const selectedItems = utils.getSelectedList(); if (selectedItems.length === 0) { throw new Error("请选择要转存的文件或文件夹"); } const currentPath = utils.getCurrentPath(); const allFiles = []; let totalFilesFound = 0; for (const item of selectedItems) { if (item.file) { const filePath = currentPath ? `${currentPath}/${item.file_name}` : item.file_name; allFiles.push({ ...item, path: filePath }); totalFilesFound++; } else if (item.dir) { const folderPath = currentPath ? `${currentPath}/${item.file_name}` : item.file_name; const folderFiles = await utils.getFolderFiles( item.fid, folderPath, () => { totalFilesFound++; } ); allFiles.push(...folderFiles); } } if (allFiles.length === 0) { throw new Error("没有找到任何文件"); } const filesData = await utils.getFilesWithMd5(allFiles); const json = utils.generateRapidTransferJson(filesData); return json; } catch (error) { utils.closeLoadingDialog(); throw error; } } async function generateShareJsonInternal(shareId, cookie) { utils.showLoadingDialog("正在转存文件", "准备中..."); try { const { stoken, title } = await utils.getShareToken(shareId, "", cookie); const selectedItems = utils.getSelectedList(); if (selectedItems.length === 0) { throw new Error("请选择要转存的文件或文件夹"); } const allFileItems = []; let totalFilesFound = 0; for (const item of selectedItems) { if (item.file) { const parentFid = item.pdir_fid; const filesInParent = await utils.scanQuarkShareFiles( shareId, stoken, cookie, parentFid, "", false ); const fileInfo = filesInParent.find((f) => f.fid === item.fid); if (fileInfo) { const fileItem = { fid: item.fid, token: fileInfo.token, name: item.file_name, size: item.size, path: item.file_name, }; allFileItems.push(fileItem); } else { const fileItem = { fid: item.fid, token: item.share_fid_token, name: item.file_name, size: item.size, path: item.file_name, }; allFileItems.push(fileItem); } totalFilesFound++; } else if (item.dir) { const folderFiles = await utils.scanQuarkShareFiles( shareId, stoken, cookie, item.fid, item.file_name ); allFileItems.push(...folderFiles); totalFilesFound += folderFiles.length; } } if (allFileItems.length === 0) { throw new Error("没有找到任何文件"); } await utils.sleep(300); const md5Map = await utils.batchGetShareFilesMd5( shareId, stoken, cookie, allFileItems ); const files = allFileItems.map((item) => ({ path: item.path, etag: (md5Map[item.fid] || "").toLowerCase(), size: item.size, })); const json = { scriptVersion: "3.0.3", exportVersion: "1.0", usesBase62EtagsInExport: false, commonPath: "", files, totalFilesCount: files.length, totalSize: files.reduce((sum, f) => sum + f.size, 0), }; return { json, title }; } catch (error) { utils.closeLoadingDialog(); throw error; } } function addButton() { const hostname = location.hostname; let container; if (document.getElementById("quark-json-generator-btn")) { return; } if (hostname.includes("cloud.189.cn")) { const isMainPage = location.pathname.startsWith("/web/main"); if (isMainPage) { container = document.querySelector( '[class*="FileHead_file-head-left"]' ); } else { container = document.querySelector(".file-operate"); } if (!container) return; const button = document.createElement("a"); button.id = "quark-json-generator-btn"; button.className = "btn"; button.href = "javascript:;"; button.textContent = "转存到123云盘"; if (isMainPage) { button.style.cssText = "width: 100px; height: 30px; padding: 0; border-radius: 4px; line-height: 30px; color: #fff; text-align: center; font-size: 12px; background: #52c41a; border: 1px solid #43a413; position: relative; display: block; margin-right: 12px;"; } else { button.style.cssText = "width: 140px; height: 36px; padding: 0; border-radius: 4px; line-height: 36px; color: #fff; text-align: center; font-size: 14px; background: #52c41a; border: 1px solid #43a413; position: relative; display: block;margin-right:20px;"; } container.insertBefore(button, container.firstChild); if (!isMainPage) { const styleId = "quark-json-flex-style"; if (!document.getElementById(styleId)) { const style = document.createElement("style"); style.id = styleId; style.textContent = ` .outlink-box-b .file-operate { display: flex !important; flex-wrap: nowrap !important; justify-content: flex-end !important; align-items: center !important; /* Override conflicting styles */ float: none !important; text-align: unset !important; } .btn-save-as{ margin-left: 0 !important; } `; document.head.appendChild(style); } } button.onclick = generateAndSaveTo123Pan; } else if (hostname.includes("quark.cn")) { const path = location.pathname; const isSharePage = /^\/(s|share)\//.test(path); if (isSharePage) { container = document.querySelector(".share-btns"); if (!container) { const alternatives = [ ".ant-layout-content .operate-bar", ".share-detail-header .operate-bar", ".share-header-btns", ".share-operate-btns", "[class*='share'][class*='btn']", ".ant-btn-group", ]; for (const selector of alternatives) { container = document.querySelector(selector); if (container) break; } } } else { container = document.querySelector(".btn-operate .btn-main"); } if (!container) return; const buttonWrapper = document.createElement("div"); buttonWrapper.id = "quark-json-generator-btn"; buttonWrapper.className = "ant-dropdown-trigger pl-button-json"; const isSharePageQuark = /^\/(s|share)\//.test(location.pathname); if (isSharePageQuark) { buttonWrapper.style.cssText = "display: inline-block; margin-left: 16px;"; buttonWrapper.innerHTML = ` `; container.appendChild(buttonWrapper); } else { buttonWrapper.style.cssText = "display: inline-block; margin-right: 16px;"; buttonWrapper.innerHTML = `
`; container.insertBefore(buttonWrapper, container.firstChild); } buttonWrapper.querySelector("button").onclick = generateAndSaveTo123Pan; } } // 全局变量 let mutationObserver = null; function init() { const SCRIPT_VERSION = GM_info.script.version; const LAST_VERSION = GM_getValue("last_version", "0"); if (SCRIPT_VERSION > LAST_VERSION) { GM_setValue("last_version", SCRIPT_VERSION); } // 检查123云盘认证信息 const auth = utils.get123PanAuth(); if (!auth.authToken || !auth.loginUuid) { // 首次使用或认证信息为空,提示用户设置 const hasPrompted = GM_getValue("pan123_auth_prompted", false); if (!hasPrompted) { setTimeout(() => { if ( confirm( "首次使用转存到123云盘功能,请先设置认证信息。\n\n点击确定进入设置,点击取消稍后在需要时设置。" ) ) { utils.show123PanAuthDialog(); } GM_setValue("pan123_auth_prompted", true); }, 1000); } } const hostname = location.hostname; if (hostname.includes("quark.cn") || hostname.includes("cloud.189.cn")) { // 创建MutationObserver并保存引用 mutationObserver = new MutationObserver(() => { addButton(); }); mutationObserver.observe(document.body, { childList: true, subtree: true, }); addButton(); } } // 清理函数,在脚本卸载时调用 function cleanup() { // 断开MutationObserver if (mutationObserver) { mutationObserver.disconnect(); mutationObserver = null; } // 清理对话框缓存 if (utils._dialogCache) { Object.values(utils._dialogCache).forEach((dialog) => { if (dialog && dialog.parentNode) { dialog.parentNode.removeChild(dialog); } }); utils._dialogCache = {}; } } // 监听页面卸载事件,清理资源 window.addEventListener("unload", cleanup); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();