// ==UserScript== // @name 123分享社区增强 // @namespace http://tampermonkey.net/ // @version 0.1.1 // @description 123分享社区功能增强脚本,自动回复,链接检测,一键保存 // @author Bao-qing // @match https://pan1.me/* // @match https://123panfx.com/* // @match https://*.123pan.com/* // @match https://*.123pan.cn/* // @match https://*.123684.com/* // @match https://*.123865.com/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/UserScript== (function () { 'use strict'; const DEFAULT_OPTIONS = { defaultReplyContent: "好人一生平安,谢谢分享!", getShareInfoRetry: 3, checkLinkHost: 'www.123pan.com', shareApiHost: 'www.123865.com', panHosts: ["www.123pan.com", "www.123865.com", "www.123pan.cn"], floatButtonText: "记录" } /** * UI管理器类 * @class UIManager * @param {Object} options - 配置选项 * @param {string} options.defaultReplyContent - 默认回复内容 * @param {string} options.styleId - 样式标签ID * @param {Object} options.defaultButtonOptions - 默认按钮选项 * @param {Object} options.toastColors - Toast颜色配置 * @param {Object} options.loadingMaskDefaults - Loading遮罩默认配置 * @param {number} options.toastDuration - Toast显示时长(毫秒) */ class UIManager { constructor(options = {}) { const defaultButtonOptions = { text: '按钮', onClick: () => console.log('点击'), id: '', className: '', style: '', type: 'default', size: 'medium', disabled: false, icon: '' }; const toastColors = { success: '#16a34a', error: '#dc2626', warning: '#ea580c', info: '#2563eb' }; const loadingMaskDefaults = { text: '加载中...', background: 'rgba(0, 0, 0, 0.6)', color: 'white', fullscreen: true }; this.config = { defaultReplyContent: options.defaultReplyContent || DEFAULT_OPTIONS.defaultReplyContent, // 默认回复内容 styleId: options.styleId || 'ui-manager-styles', // 样式标签ID defaultButtonOptions: {...defaultButtonOptions, ...(options.defaultButtonOptions || {})}, toastColors: {...toastColors, ...(options.toastColors || {})}, loadingMaskDefaults: {...loadingMaskDefaults, ...(options.loadingMaskDefaults || {})}, toastDuration: options.toastDuration || 3000 }; this.loadingCount = 0; this.loadingMask = null; } init() { this.addStyles(); } /** * 在指定位置添加按钮 */ addButton(parentElement, options = {}, position = 'beforeend') { const config = {...this.config.defaultButtonOptions, ...options}; if (!parentElement) return null; const button = document.createElement('button'); button.type = 'button'; // 组装 Class const classes = ['ui-btn', `ui-btn-${config.type}`, `ui-btn-${config.size}`]; if (config.disabled) classes.push('ui-btn-disabled'); if (config.className) classes.push(config.className); button.className = classes.join(' '); if (config.id) button.id = config.id; if (config.style) button.style.cssText = config.style; button.disabled = config.disabled; // 图标与文本处理 let content = ''; if (config.icon) { const isImg = config.icon.match(/\.(png|jpg|jpeg|svg|gif|webp)|data:image/i); content += isImg ? `` : ``; } content += `${config.text}`; button.innerHTML = content; if (!config.disabled) { button.addEventListener('click', config.onClick); } const positions = { 'beforebegin': () => parentElement.before(button), 'afterbegin': () => parentElement.prepend(button), 'beforeend': () => parentElement.append(button), 'afterend': () => parentElement.after(button) }; (positions[position] || positions['beforeend'])(); return button; } /** * 核心 CSS 样式(包含按钮、Loading、Toast) */ addStyles() { if (document.getElementById(this.config.styleId)) return; const style = document.createElement('style'); style.id = this.config.styleId; style.textContent = ` :root { --ui-primary: #2563eb; --ui-success: #16a34a; --ui-warning: #ea580c; --ui-danger: #dc2626; --ui-info: #0891b2; --ui-gray: #f3f4f6; } /* 按钮基础样式 */ .ui-btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); border: 1px solid transparent; outline: none; user-select: none; font-family: inherit; line-height: 1.5; } .ui-btn:active { transform: scale(0.96); } /* 尺寸 */ .ui-btn-small { padding: 4px 10px; font-size: 13px; } .ui-btn-medium { padding: 8px 16px; font-size: 14px; } .ui-btn-large { padding: 12px 24px; font-size: 16px; } /* 类型配色 */ .ui-btn-default { background: white; border-color: #d1d5db; color: #374151; } .ui-btn-default:hover { background: #f9fafb; border-color: #9ca3af; } .ui-btn-primary { background: var(--ui-primary); color: white; } .ui-btn-primary:hover { background: #1d4ed8; box-shadow: 0 4px 12px rgba(37,99,235,0.2); } .ui-btn-success { background: var(--ui-success); color: white; } .ui-btn-danger { background: var(--ui-danger); color: white; } .ui-btn-disabled { opacity: 0.5; cursor: not-allowed; transform: none !important; } .ui-icon { display: flex; align-items: center; width: 1.1em; height: 1.1em; } .ui-icon img { width: 100%; height: 100%; object-fit: contain; } /* Loading 遮罩 */ .ui-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; transition: opacity 0.3s; backdrop-filter: blur(4px); } .ui-spinner { width: 32px; height: 32px; border: 3px solid rgba(255,255,255,0.3); border-radius: 50%; border-top-color: #fff; animation: ui-spin 0.8s linear infinite; } @keyframes ui-spin { to { transform: rotate(360deg); } } /* Toast 提示 */ .ui-toast { position: fixed; top: 24px; left: 50%; transform: translateX(-50%); z-index: 10001; padding: 10px 20px; border-radius: 10px; color: white; font-size: 14px; font-weight: 500; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); transition: all 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28); opacity: 0; transform: translate(-50%, -20px); } .ui-toast.show { opacity: 1; transform: translate(-50%, 0); } `; document.head.appendChild(style); } postReply(content = this.config.defaultReplyContent || "感谢分享!") { const pageIdMatch = document.location.href.match(/thread-(\d+)\.htm/); if (!pageIdMatch) return Promise.reject("Page ID not found"); const pageId = pageIdMatch[1]; const fullUrl = `${document.location.origin}${document.location.pathname}?post-create-${pageId}-1.htm=`; const data = new URLSearchParams({ "doctype": "1", "return_html": "1", "quotepid": "0", "message": content, "quick_reply_message": "1" }); return fetch(fullUrl, { method: 'POST', body: data, credentials: 'same-origin', headers: {"x-requested-with": "XMLHttpRequest"} }).then(res => res.text()); } postReplyAndRefresh() { // 检查是否成功提交 this.postReply().then(responseText => { const jsonData = JSON.parse(responseText); if (jsonData.code === "0") { this.showToast("回复成功,页面即将刷新", "success"); setTimeout(() => location.reload(), 200); } else { this.showToast(`回复失败:${jsonData.message || '未知错误'}`, "error"); } }) } changeLinkStyle(linkElement, type, message = "链接无效") { if (!linkElement) return; const color = type === "success" ? "#16a34a" : "#dc2626"; const icon = type === "success" ? "✅ " : "❌ "; linkElement.style.color = color; linkElement.style.fontWeight = "bold"; linkElement.innerHTML = icon + linkElement.innerHTML + (type === "error" ? ` (${message})` : ""); } extractMultipleInfos(html) { const urlRegex = /https?:\/\/[^\s"<>]*\/s\/[A-Za-z0-9-]{4,}[^\s"<>?]*/g; let matches = []; let match; while ((match = urlRegex.exec(html)) !== null) { matches.push({ url: match[0].replace(/[?&]+$/, ''), // 去掉末尾的 ? 或 & index: match.index, length: match[0].length }); } // 去重 const uniqueMatches = []; matches.forEach(m => { const lastMatch = uniqueMatches[uniqueMatches.length - 1]; // 判断是否是同一个链接的重复出现(距离阈值 300) if (lastMatch && lastMatch.url === m.url && (m.index - (lastMatch.index + lastMatch.length) < 300)) { // 提取码在显示文本后面,从这里开始找。 uniqueMatches[uniqueMatches.length - 1] = m; } else { uniqueMatches.push(m); } }); // 3. 提取提取码 const codeRegex = /(?:提取码|查询码|密码|访问码|码)[::\s]*([a-zA-Z0-9]{4})/i; return uniqueMatches.map((m, i) => { // 搜索范围:从当前链接结束,到下一个链接开始 const nextMatchIndex = uniqueMatches[i + 1] ? uniqueMatches[i + 1].index : html.length; const searchZone = html.substring(m.index + m.length, nextMatchIndex); const codeMatch = searchZone.match(codeRegex); return { link: m.url, code: codeMatch ? codeMatch[1] : null }; }); } showLoadingMask(options = {}) { const config = {...this.config.loadingMaskDefaults, ...options}; if (this.loadingMask) { this.loadingCount++; return this.loadingMask; } // this.addLoadingMaskStyles(); // 确保样式存在 const mask = document.createElement('div'); mask.className = 'ui-mask'; mask.style.backgroundColor = config.background; mask.style.position = config.fullscreen ? 'fixed' : 'absolute'; mask.innerHTML = `
${config.text}
`; const target = config.parentSelector ? document.querySelector(config.parentSelector) : document.body; if (!config.fullscreen && target) target.style.position = 'relative'; target.appendChild(mask); setTimeout(() => mask.style.opacity = '1', 10); this.loadingMask = mask; this.loadingCount = 1; return mask; } hideLoadingMask(force = false) { this.loadingCount--; if ((force || this.loadingCount <= 0) && this.loadingMask) { this.loadingMask.style.opacity = '0'; setTimeout(() => { this.loadingMask?.remove(); this.loadingMask = null; this.loadingCount = 0; }, 300); } } async withLoadingMask(asyncFunc, options = {}) { this.showLoadingMask(options); try { return await asyncFunc(); } finally { this.hideLoadingMask(); } } showToast(message, type = 'info', duration = this.config.toastDuration) { // this.addStyles(); const toast = document.createElement('div'); toast.className = `ui-toast`; const colors = this.config.toastColors; toast.style.backgroundColor = colors[type] || colors.info; toast.textContent = message; document.body.appendChild(toast); // 触发重绘以执行动画 requestAnimationFrame(() => toast.classList.add('show')); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, duration); } } /** * 链接管理器类 * @class linkManager * @param {Object} options - 配置选项 */ class linkManager { constructor(options = {}) { const defaults = { host: DEFAULT_OPTIONS.checkLinkHost, // 链接检测api主机名 也可以是www.123865.com等 checkPath: '/gsb/s/', // 检查路径 shareApiHost: DEFAULT_OPTIONS.shareApiHost, // 分享API主机名 shareGetPath: '/b/api/share/get', // 分享获取路径 savePath: '/b/api/restful/goapi/v1/file/copy/save', // 保存到网盘api路径 shareGetLimit: '100', // 分享获取单页限制,最大100 requestRetry: DEFAULT_OPTIONS.getShareInfoRetry, // 获取分享内容时的请求重试次数 gmStorageKey: 'panLoginInfo', // GM存储键 defaultParentFileId: '0' // 保存文件默认父文件ID,根目录为0,一般不会使用 }; this.config = {...defaults, ...options}; this.host = this.config.host; this.check_path = this.config.checkPath; } // 生成签名函数 getSign(path, platform = 'web') { function A(t, e = 10) { const n = (function () { const t = []; for (let e = 0; e < 256; e++) { let n = e; for (let r = 0; r < 8; r++) n = 1 & n ? 3988292384 ^ (n >>> 1) : n >>> 1; t[e] = n; } return t; })(); const r = t.replace(/\r\n/g, "\n"); let a = -1; for (let i = 0; i < r.length; i++) a = (a >>> 8) ^ n[(255 & (a ^ r.charCodeAt(i)))]; return ((-1 ^ a) >>> 0).toString(e); } function generateTimestamp() { return Math.round(((new Date).getTime() + 60 * (new Date).getTimezoneOffset() * 1e3 + 288e5) / 1e3).toString(); } function adjustTimestamp(o, timestamp) { if (timestamp) { var i = timestamp; var m = i; if (20 <= Math.abs(1e3 * o - 1e3 * m) / 1e3 / 60) { return i; } } return o; } function formatDate(t, e, n) { var r; n = 8 t = 1000 * Number.parseInt(t); t += 60000 * new Date(t).getTimezoneOffset(); var data = { y: (r = new Date(t + 36e5 * n)).getFullYear(), m: r.getMonth() + 1 < 10 ? "0".concat(r.getMonth() + 1) : r.getMonth() + 1, d: r.getDate() < 10 ? "0".concat(r.getDate()) : r.getDate(), h: r.getHours() < 10 ? "0".concat(r.getHours()) : r.getHours(), f: r.getMinutes() < 10 ? "0".concat(r.getMinutes()) : r.getMinutes() } return data } function generateSignature(a, o, e, n, r) { var s = ["a", "d", "e", "f", "g", "h", "l", "m", "y", "i", "j", "n", "o", "p", "k", "q", "r", "s", "t", "u", "b", "c", "v", "w", "s", "z"]; var u = formatDate(o); var h = u.y; var g = u.m; var l = u.d; var c = u.h; var u = u.f; var d = [h, g, l, c, u].join(""); var f = []; for (var p in d) { f.push(s[Number(d[p])]); } h = A(f.join("")); g = A("".concat(o, "|").concat(a, "|").concat(e, "|").concat(n, "|").concat(r, "|").concat(h)); return [h, "".concat(o, "-").concat(a, "-").concat(g)]; } var a = Math.round(1e7 * Math.random()); var o = generateTimestamp(); o = adjustTimestamp(o, new Date().getTime() / 1000); var r = 3; return generateSignature(a, o, path, platform, r); } buildUrl(url, params) { const resolvedParams = params || {}; const signData = this.getSign(new URL(url).pathname, 'web'); resolvedParams[signData[0]] = signData[1]; const queryString = new URLSearchParams(resolvedParams).toString(); return url + "?" + queryString; } /** * HTTP请求函数,自动加签,使用GM_xmlhttpRequest发送请求 * @param {string} method - HTTP方法(GET, POST, PUT, DELETE等) * @param {string} url - 请求URL * @param {Object|string|null} data - 请求数据(可选) * @param {Object} options - 其他请求选项(可选) * @param {Object} options.params - URL查询参数对象 * @param {Object} options.headers - 额外请求头对象 * @param {string} options.responseType - 响应类型('json', 'text', 'blob', 'arraybuffer', 'document') * @param {number} options.timeout - 请求超时时间(毫秒) * @returns {Promise} 返回一个Promise,解析为响应数据 */ async sendRequest(method, url, data = null, options = {}) { return new Promise((resolve, reject) => { const params = options.params || {}; url = this.buildUrl(url, params); const requestOptions = { method: method, url: url, headers: { 'Content-Type': 'application/json', ...options.headers // 允许覆盖或添加额外请求头 }, responseType: options.responseType || 'json', // 可选项:'json', 'text', 'blob', 'arraybuffer', 'document' timeout: options.timeout || 30000, // 默认30秒超时 onload: function (response) { if (response.status >= 200 && response.status < 300) { // 请求成功 if (options.responseType === 'json') { try { const jsonResponse = JSON.parse(response.responseText); console.log(`Success (${response.status}):`, jsonResponse); resolve(jsonResponse); } catch (error) { console.log(`Success (${response.status}, non-JSON):`, response.responseText); resolve(response.responseText); } } else if (options.responseType === 'text') { resolve(response.responseText); } else if (options.responseType === 'blob') { resolve(new Blob([response.response])); } else if (options.responseType === 'arraybuffer') { resolve(response.response); } else { resolve(response.responseText); } } else { // 请求失败(HTTP错误状态码) console.error(`Request failed with status ${response.status}:`, response.responseText); reject(new Error(`Request failed with status ${response.status}`)); } }, onerror: function (error) { console.error('Request network error:', error); reject(new Error(`Network error: ${error}`)); }, ontimeout: function () { console.error('Request timeout'); reject(new Error('Request timeout')); }, onabort: function () { console.error('Request aborted'); reject(new Error('Request aborted')); } }; // 如果有数据,添加到请求中 if (data) { if (typeof data === 'string') { requestOptions.data = data; } else { requestOptions.data = JSON.stringify(data); } } // 添加其他选项 if (options.timeout) requestOptions.timeout = options.timeout; if (options.responseType) requestOptions.responseType = options.responseType; if (options.overrideMimeType) requestOptions.overrideMimeType = options.overrideMimeType; if (options.anonymous !== undefined) requestOptions.anonymous = options.anonymous; if (options.user !== undefined) requestOptions.user = options.user; if (options.password !== undefined) requestOptions.password = options.password; // 发送请求 GM_xmlhttpRequest(requestOptions); }); } async linkCheckByKey(shareId) { this.fullUrl = `https://${this.config.host}${this.config.checkPath}${shareId}`; const checkData = await this.sendRequest('GET', this.fullUrl, null, {responseType: 'json'}); if (checkData == null) { console.log('Error:', checkData); return { code: -1, message: '请求失败' }; } if (checkData.info.code !== 0) { console.error('链接失效:', checkData.message); return { code: checkData.info.code, message: checkData.info.message }; } else { console.log('链接有效:', checkData); return { code: 0, message: '链接有效', data: checkData.info.data } } } /** * 通过分享链接解析并检查链接有效性 * @param {string} shareUrl - 分享链接URL * @returns {Object} 包含检查结果的对象,格式为 { code: number, message: string } * code: 0表示有效,-1表示无效或解析失败 */ async linkCheckByUrl(shareUrl) { const match = await this.linkParse(shareUrl); if (!match) { console.log("链接解析失败"); return { code: -1, message: "链接解析失败" } } const host = match[1]; // www.123865.com const shareKey = match[2]; // NaGDVv-Mg6Y const password = match[3]; // 8888 console.log({host, shareId: shareKey, password}); // const shareId = shareIdMatch[1]; console.log("提取到的分享ID:", shareKey); const result = await this.linkCheckByKey(shareKey); return result; } async linkParse(shareUrl) { const pattern = /https?:\/\/([^/]+)\/s\/([A-Za-z0-9_-]+)(?:\?pwd=([A-Za-z0-9_-]{4}))?/; const match = shareUrl.match(pattern); return match } async shareGetOnePage(shareKey, sharePwd, host, ParentFileId, page) { const resolvedHost = host || this.config.shareApiHost; const resolvedParentId = (ParentFileId ?? this.config.defaultParentFileId).toString(); const resolvedPage = (page || 1).toString(); const shareGetUrl = `https://${resolvedHost}${this.config.shareGetPath}`; const params = { limit: this.config.shareGetLimit.toString(), next: "0", orderBy: "file_name", orderDirection: "asc", shareKey, ParentFileId: resolvedParentId, Page: resolvedPage, event: "homeListFile", operateType: resolvedParentId === "0" ? "1" : "4", OrderId: "", superAdmin: "null", SharePwd: sharePwd }; const resData = await this.sendRequest("get", shareGetUrl, null, { params, responseType: 'json' }) if (resData.code !== 0) { console.log("获取文件列表错误", resData.message); return { code: resData.code, data: {}, message: resData.message } } return resData console.log(resData); } /** * 获取分享链接的所有文件列表,处理分页 * @param {string} shareKey - 分享链接的Key * @param {string} sharePwd - 分享链接的密码(如果有) * @param {string} host - 服务器主机名 * @returns {Object} 包含所有文件信息的对象,格式为 { code: number, data: Array, message: string , skipPage: number } * code: 0表示成功,-1表示失败,-2表示提取码错误 * skipPage: 如果在获取过程中发生错误并跳过分页,则为1,否则为0 * */ async shareGetAllPages(shareKey, sharePwd, host) { let InfoList = []; let page = 1; let skipPage = 0; let nextPage = true; while (nextPage) { // 三次重试机制 let attempt = 0; let resData = null; while (attempt < this.config.requestRetry) { try { resData = await this.shareGetOnePage(shareKey, sharePwd, host, "0", page); if (resData.code !== 0) { console.log("获取文件列表错误", resData.message); if (resData.code == 5103) { // 提取码错误,停止获取 skipPage = 1; console.error("获取文件列表错误,提取码错误,停止获取", resData.message); return { code: -2, data: {}, message: resData.message, skipPage: skipPage } } throw new Error(resData.message); } break; // 成功,跳出重试循环 } catch (error) { attempt++; console.log(`获取文件列表失败,正在重试 (${attempt}/3)...`, error); if (attempt === this.config.requestRetry) { resData = { code: -1, data: {}, message: "获取文件列表失败,已达最大重试次数" }; skipPage = 1; console.error("获取文件列表错误,已达最大重试次数", resData.message); } } } InfoList = InfoList.concat(resData.data.InfoList); // 如果不是最后一页,继续获取下一页 (不知道为什么123要叫这个“IsFirst”) // IsFirst = false 继续获取,null 或 true 停止获取 nextPage = resData.data.IsFirst === false; page += 1; } return { code: 0, data: InfoList, message: "获取成功", skipPage: skipPage }; }; /** * 保存文件到用户网盘 * @param {string} shareKey - 分享链接的Key * @param {string} sharePwd - 分享链接的密码(如果有) * @param {string} host - 服务器主机名 * @param {Array} InfoList - 要保存的文件信息列表 * @param {string} parentFileID - 保存到的目标文件夹ID * @param {string} authorization - 用户的Authorization令牌 * @param {string} loginUuid - 用户的LoginUuid * @returns {Object} 保存结果对象,格式为 { code: number, message: string } * code: 0 表示成功,或 resData.code,来自123云盘api,具体含义暂不知 */ async saveFile(shareKey, sharePwd, host, InfoList, parentFileID, authorization, loginUuid) { const resolvedHost = host || this.config.shareApiHost; const saveUrl = `https://${resolvedHost}${this.config.savePath}`; let fileList = []; for (const info of InfoList) { fileList.push({ "fileID": info.FileId, "size": info.Size, "etag": info.Etag, "type": info.Type, "parentFileID": parentFileID, "fileName": info.FileName, "driveID": 0 }); } const fileData = { fileList: fileList, shareKey: shareKey, sharePwd: sharePwd, currentLevel: 0, superAdmin: null } const resData = await this.sendRequest("post", saveUrl, fileData, { responseType: 'json', headers: { //'Content-Type': 'application/json' Authorization: "Bearer " + authorization, LoginUuid: loginUuid }, data: fileData }) if (resData.code !== 0) { console.log("保存文件错误", resData.message); return { code: resData.code, message: resData.message } } return { code: 0, message: "保存成功" } } /** * 保存分享链接的所有文件到用户网盘 * */ async saveAllFilesFromShareKey(shareKey, sharePwd, host, parentFileID, authorization, loginUuid) { const getRes = await this.shareGetAllPages(shareKey, sharePwd, host); if (getRes.code !== 0) { return { code: getRes.code, message: getRes.message } } const saveRes = await this.saveFile(shareKey, sharePwd, host, getRes.data, parentFileID, authorization, loginUuid); if (saveRes.code !== 0) { return { code: saveRes.code, message: saveRes.message } } return { code: 0, message: "保存成功" } } getAuthInfo() { const panlogInfo = GM_getValue(this.config.gmStorageKey); if (!panlogInfo) { return { authorization: null, loginUuid: null, parentFileId: null } } return panlogInfo } /** * 保存分享链接的所有文件到用户网盘(通过分享链接URL) * @param {string} shareUrl - 分享链接的完整URL * @param {string} parentFileID - 保存到的目标文件夹ID * @returns {Object} 保存结果对象,格式为 { code: number, message: string } * code: 0 表示成功,-1 解析失败,-2 未找到授权信息,saveRes.code 来自保存文件的结果 */ async saveAllFilesFromShareUrl(shareUrl, parentFileID, authorization, loginUuid) { const parseRes = await this.linkParse(shareUrl); if (!parseRes) { return { code: -1, message: "链接解析失败" } } // 链接检查 const checkRes = await this.linkCheckByKey(parseRes[2]); if (checkRes.code !== 0) { return { code: checkRes.code, message: checkRes.message } } const host = parseRes[1]; const shareKey = parseRes[2]; const sharePwd = parseRes[3] || null; // const panLogInfo = this.getAuthInfo(); // const authorization = panLogInfo.authorization; // const loginUuid = panLogInfo.loginUuid; // const parentFileId = parentFileID || panLogInfo.parentFileId || "0"; if (!authorization || !loginUuid) { return { code: -2, message: "未找到授权信息,请先前往设置" } } const saveRes = await this.saveAllFilesFromShareKey(shareKey, sharePwd, host, parentFileID, authorization, loginUuid); return saveRes;// 401:未授权,请检查Authorization和LoginUuid } async fastSave(shareUrl) { const panLogInfo = this.getAuthInfo(); const authorization = panLogInfo.authorization; const loginUuid = panLogInfo.loginUuid; const parentFileId = panLogInfo.parentFileId; if (!authorization || !loginUuid || !parentFileId) { return { code: -2, message: "未初始化信息,请先前往设置" } } const saveRes = await this.saveAllFilesFromShareUrl(shareUrl, parentFileId, authorization, loginUuid); return saveRes; } } /** * 浮动按钮管理器类 * @class panWebManager * @param {Object} options - 配置选项 * @param {string} options.gmStorageKey - GM存储键 * @param {Object} options.storageKeys - 存储键对象 * @param {Object} options.button - 按钮配置对象 */ class panWebManager { constructor(options = {}) { const defaults = { gmStorageKey: 'panLoginInfo', storageKeys: { authorization: 'authorToken', // 存储Authorization的键 loginUuid: 'LoginUuid', // 存储LoginUuid的键 filePath: 'filePath' // 存储默认文件路径的键 }, button: { text: DEFAULT_OPTIONS.floatButtonText, // 按钮文本 successText: 'OK', // 成功后文本 width: '50px', // 按钮宽度 height: '50px', // 按钮高度 idleColor: '#007bff', // 空闲颜色 successColor: '#28a745', // 成功颜色 shrinkDelay: 2000, // 收缩延迟时间(毫秒) initialTop: '20%', // 初始顶部位置 errText: 'Err', // 错误后文本 errColor: '#dc3545' // 错误颜色 } }; this.config = { ...defaults, ...options, storageKeys: {...defaults.storageKeys, ...(options.storageKeys || {})}, button: {...defaults.button, ...(options.button || {})} }; } init() { this.addFloatingButton(); } addFloatingButton() { const btnCfg = this.config.button; const button = document.createElement('div'); button.id = 'pan-floating-button'; button.innerText = btnCfg.text; Object.assign(button.style, { position: 'fixed', top: btnCfg.initialTop, right: '0px', width: btnCfg.width, height: btnCfg.height, backgroundColor: btnCfg.idleColor, color: '#fff', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'move', zIndex: '9999', boxShadow: '0 4px 10px rgba(0,0,0,0.3)', userSelect: 'none', fontSize: '14px', transition: 'all 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28)', overflow: 'hidden', whiteSpace: 'nowrap' }); document.body.appendChild(button); let isDragging = false; let isHovering = false; let side = 'right'; // 记录在哪一侧: 'left' | 'right' // --- 功能函数:执行收缩 --- const shrink = () => { if (isDragging || isHovering) return; button.style.width = '30px'; button.style.borderRadius = side === 'right' ? '20px 0 0 20px' : '0 20px 20px 0'; button.style.opacity = '0.6'; button.innerText = ''; if (side === 'right') { button.style.left = `${window.innerWidth - 15}px`; // 露出一半 } else { button.style.left = `-15px`; } }; // --- 功能函数:执行展开 --- const expand = () => { button.style.width = btnCfg.width; button.style.borderRadius = '50%'; button.style.opacity = '1'; button.innerText = btnCfg.text; const rect = button.getBoundingClientRect(); if (side === 'right') { button.style.left = `${window.innerWidth - 60}px`; } else { button.style.left = '10px'; } }; // --- 功能函数:吸附边界 --- const stickyAdsorb = () => { const rect = button.getBoundingClientRect(); const threshold = window.innerWidth / 2; side = (rect.left + rect.width / 2 < threshold) ? 'left' : 'right'; expand(); // 3秒后如果没有操作则收缩 setTimeout(shrink, btnCfg.shrinkDelay); }; // 鼠标悬停处理 button.addEventListener('mouseenter', () => { isHovering = true; expand(); }); button.addEventListener('mouseleave', () => { isHovering = false; setTimeout(shrink, 300); }); // 拖拽逻辑 button.addEventListener('mousedown', (e) => { let hasMoved = false; const startX = e.clientX; const startY = e.clientY; const initialRect = button.getBoundingClientRect(); button.style.transition = 'none'; // 拖拽时关闭动画 const onMouseMove = (moveEvent) => { hasMoved = true; isDragging = true; button.style.left = `${initialRect.left + (moveEvent.clientX - startX)}px`; button.style.top = `${initialRect.top + (moveEvent.clientY - startY)}px`; }; const onMouseUp = () => { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); button.style.transition = 'all 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28)'; if (hasMoved) { stickyAdsorb(); } setTimeout(() => isDragging = false, 50); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); // 点击事件 button.addEventListener('click', () => { if (!isDragging) { let loginInfo = this.getInformation(); if (!loginInfo.authorization || !loginInfo.loginUuid) { // 获取授权信息失败 button.innerText = btnCfg.errText; button.style.backgroundColor = btnCfg.errColor; setTimeout(() => { button.innerText = btnCfg.text; button.style.backgroundColor = btnCfg.idleColor; shrink(); }, 1000); return; } button.innerText = btnCfg.successText; button.style.backgroundColor = btnCfg.successColor; setTimeout(() => { button.innerText = btnCfg.text; button.style.backgroundColor = btnCfg.idleColor; shrink(); }, 1000); } }); // 初始化:先吸附再收缩 setTimeout(stickyAdsorb, 500); } getParentFileId() { const filePathKey = this.config.storageKeys.filePath; try { const storageValue = sessionStorage[filePathKey]; if (!storageValue) return "0"; const homeFilePath = JSON.parse(storageValue).homeFilePath || []; return (homeFilePath[homeFilePath.length - 1] || 0).toString(); } catch { return "0"; } } getInformation() { const {authorization: authKey, loginUuid: loginKey} = this.config.storageKeys; const authorization = localStorage.getItem(authKey); const loginUuid = localStorage.getItem(loginKey); const parentFileId = this.getParentFileId(); if (typeof GM_setValue !== 'undefined') { GM_setValue(this.config.gmStorageKey, { authorization, loginUuid, parentFileId: parentFileId }); } return {authorization, loginUuid, parentFileId} } } class mainController { constructor(options = {}) { const defaults = { panHosts: DEFAULT_OPTIONS.panHosts, linkManager: {}, uiManager: {}, panWebManager: {} }; this.config = { ...defaults, ...options, linkManager: {...(defaults.linkManager || {}), ...(options.linkManager || {})}, uiManager: {...(defaults.uiManager || {}), ...(options.uiManager || {})}, panWebManager: {...(defaults.panWebManager || {}), ...(options.panWebManager || {})} }; } async init() { const panHosts = this.config.panHosts; if (panHosts.includes(window.location.hostname)) { this.panWebManager = new panWebManager(this.config.panWebManager); this.panWebManager.init(); } // 排除 分享页面 if (window.location.pathname.startsWith('/s/')) { console.log("分享页面"); return; } this.link_manager = new linkManager(this.config.linkManager); this.ui_manager = new UIManager(this.config.uiManager); this.ui_manager.init(); const warningDivCollect = document.querySelectorAll(".alert.alert-warning[role='alert']"); if (warningDivCollect.length > 0) { for (const warningDiv of warningDivCollect) { console.log("找到了回复提示元素:", warningDiv); // 添加快捷回复按钮 this.ui_manager.addButton(warningDiv, { text: '回复', type: 'success', onClick: async () => { // 使用withLoadingMask包装异步操作 await this.ui_manager.withLoadingMask( () => this.ui_manager.postReplyAndRefresh(), { text: '正在回复中...', spinnerType: 'circle' } ); }, size: "small" }); } } // 提取链接和验证码 var extractedInfos; const cardBody = document.querySelector(".card-body"); if (cardBody) { const htmlContent = cardBody.innerHTML; extractedInfos = this.ui_manager.extractMultipleInfos(htmlContent); console.log("提取的链接和提取码信息:", extractedInfos); } // 获取链接提示 const successDivCollect = document.querySelectorAll(".alert.alert-success"); if (successDivCollect.length > 0) { for (const successDiv of successDivCollect) { console.log("找到了链接提示元素:", successDiv); const linkElementCollect = successDiv.querySelectorAll("a"); if (linkElementCollect.length < 1) { console.error("未找到链接元素"); continue; } for (const linkElement of linkElementCollect) { const linkText = linkElement.href; if (!linkText || linkText.length < 1) { console.error("链接文本为空"); continue; } // 如果是空元素,跳过 if (linkElement.textContent.trim().length < 1) { console.error("文本内容为空"); continue; } console.log("检查链接有效性:", linkText); const result = await this.link_manager.linkCheckByUrl(linkText); if (result.code === 0) { this.ui_manager.changeLinkStyle(linkElement, "success"); this.ui_manager.showToast("链接有效!", "success"); // 找到提取码 //// 先从url中提取 let extractCode = null; let urlWithPwd = null; const urlObj = new URL(linkText); extractCode = urlObj.searchParams.get("pwd"); // 如果url中没有,从提取的信息中找 if (!extractCode) { for (const info of extractedInfos) { const urlObj_extract = new URL(info.link); if (urlObj_extract.path === urlObj.path && info.code) { extractCode = info.code; console.log("从提取信息中找到提取码:", extractCode); urlWithPwd = urlObj.origin + urlObj.pathname + "?pwd=" + extractCode; break; } } } if (!urlWithPwd) { urlWithPwd = linkText; } // 添加保存按钮 this.ui_manager.addButton(linkElement, { text: '保存到网盘', type: 'primary', onClick: async () => { await this.ui_manager.withLoadingMask( async () => { const saveResult = await this.link_manager.fastSave(urlWithPwd); if (saveResult.code === 0) { this.ui_manager.showToast("保存成功!", "success"); } else { this.ui_manager.showToast(`保存失败:${saveResult.message}`, "error"); } }, { text: '正在保存中...', spinnerType: 'circle' } ); }, size: "small", style: "margin-left: 10px;" }, "afterend" ); } else { if (result.code === -1) { // 解析失败,认为是无效链接 this.ui_manager.showToast("链接解析失败!", "error"); continue; } this.ui_manager.changeLinkStyle(linkElement, "error", result.message); this.ui_manager.showToast(`链接失效:${result.message}`, "error"); } } } } } } window.main_controller = new mainController(); main_controller.init(); })();