您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
访问123panfx.com 123云盘资源社区时,点击发新帖按钮旁的「123云盘分享」图标,一键检索个人云盘资源并生成分享链接。
// ==UserScript== // @name 123 云盘资源快搜助手 | 一键生成分享链接 // @namespace http://tampermonkey.net/ // @version 0.3.4 // @description 访问123panfx.com 123云盘资源社区时,点击发新帖按钮旁的「123云盘分享」图标,一键检索个人云盘资源并生成分享链接。 // @author Walking // @match *://123panfx.com/* // @match *://www.123panfx.com/* // @match *://pan1.me/* // @match *://www.123pan.com/* // @grant GM_setValue // @grant GM_getValue // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; // 样式配置 const StyleConfig = { BG_COLOR: "#f5f5f7", ACCENT_COLOR: "#2c7be5", TEXT_COLOR: "#333333", LIGHT_TEXT: "#666666", BORDER_COLOR: "#e0e0e0", HOVER_COLOR: "#e8f0fe", SELECT_COLOR: "#d0e2ff", FONT_FAMILY: "Microsoft YaHei, sans-serif", FONT_SIZE: "14px", PADDING: "12px", MARGIN: "8px", SPACING: "10px" }; // 全局存储 const Storage = { get(key) { const value = GM_getValue(key, ''); console.log(`[存储读取] ${key}=${value}`); return value; }, set(key, value) { GM_setValue(key, value); console.log(`[存储写入] ${key}=${value}`); }, getObj(key) { const val = GM_getValue(key, '{}'); try { return JSON.parse(val); } catch (e) { console.error(`[存储解析失败] ${key}`, e); return {}; } }, setObj(key, obj) { GM_setValue(key, JSON.stringify(obj)); console.log(`[存储写入对象] ${key}=${JSON.stringify(obj)}`); }, clearToken() { GM_setValue('crossDomainToken', ''); console.log(`[存储清除] 已清除token`); } }; // 目标域名集合(同一网站的不同域名) const targetDomains = new Set([ '123panfx.com', 'www.123panfx.com', 'pan1.me' ]); const tokenSourceDomain = 'www.123pan.com'; // token来源域名 // 域名判断工具函数 function getCurrentDomain() { return window.location.hostname.replace(/^www\./, ''); // 移除www.前缀统一判断 } const currentDomain = getCurrentDomain(); const isTargetDomain = targetDomains.has(currentDomain); // 是否为目标使用域名 const isTokenSource = currentDomain === tokenSourceDomain.replace(/^www\./, ''); // 是否为token来源域名 console.log(`[域名信息] 当前域名=${window.location.hostname},处理后=${currentDomain},是否目标域名=${isTargetDomain},是否token来源=${isTokenSource}`); // 目标域名接收并缓存token(从URL参数) function handleTokenFromURL() { if (!isTargetDomain) return; const urlParams = new URLSearchParams(window.location.search); const token = urlParams.get('token'); const loginUuid = urlParams.get('loginUuid'); console.log(`[目标域名接收token] token=${token ? '存在' : '不存在'},loginUuid=${loginUuid || '空'}`); if (token) { // 持久化存储token Storage.set('crossDomainToken', token); if (loginUuid) Storage.set('loginUuid', loginUuid); // 移除URL中的token参数 urlParams.delete('token'); urlParams.delete('loginUuid'); const cleanUrl = `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`; window.history.replaceState({}, document.title, cleanUrl); alert('✅ token获取成功,可以开始搜索了'); } } // token来源域名(www.123pan.com)处理逻辑(静默优化) function fetchTokenAndRedirect() { if (!isTokenSource) return; // 从URL参数中获取原目标域名的跳转地址 const urlParams = new URLSearchParams(window.location.search); const redirectUrl = urlParams.get('redirect'); console.log(`[token来源域名] redirectUrl=${redirectUrl || '空'}`); // 无跳转参数时,完全静默(不提示、不操作) if (!redirectUrl) { console.log(`[www.123pan.com] 无跳转参数,静默模式`); return; } // 有跳转参数时,读取token并跳转(仅此时执行操作) const token = localStorage.getItem('authorToken'); const loginUuid = localStorage.getItem('LoginUuid'); console.log(`[来源域名读取token] authorToken=${token ? '存在' : '不存在'},LoginUuid=${loginUuid || '空'}`); // 即使token不存在,也不提示(避免干扰用户),仅在控制台输出日志 if (!token) { console.log(`[www.123pan.com] 未找到token,无法完成跳转`); return; } // 跳转回目标域名,并携带token try { const targetUrl = new URL(decodeURIComponent(redirectUrl)); targetUrl.searchParams.set('token', token); if (loginUuid) targetUrl.searchParams.set('loginUuid', loginUuid); console.log(`[准备跳转回目标域名] url=${targetUrl.toString()}`); window.location.href = targetUrl.toString(); } catch (e) { console.error('[跳转地址解析失败]', e); // 解析失败也不提示,仅日志记录 } } // 获取缓存的token(无效则返回null) function getCachedToken() { return Storage.get('crossDomainToken') || null; } // 处理token过期(清除缓存并重新获取) function handleTokenExpired() { console.log('[token已过期] 准备重新获取'); Storage.clearToken(); alert('⚠️ token已过期,请重新获取授权'); // 跳转至token来源域名 const currentUrl = window.location.href; window.location.href = `https://${tokenSourceDomain}?redirect=${encodeURIComponent(currentUrl)}`; } // 全局状态 let globalState = { clientID: Storage.get('clientID'), clientSecret: Storage.get('clientSecret'), accessToken: getCachedToken(), loginUuid: Storage.get('loginUuid'), folderCache: Storage.getObj('folderCache') }; // 保存状态 function saveState() { Storage.set('clientID', globalState.clientID); Storage.set('clientSecret', globalState.clientSecret); Storage.set('loginUuid', globalState.loginUuid); Storage.setObj('folderCache', globalState.folderCache); } // 创建UI(所有目标域名都显示) function createUI() { if (!isTargetDomain) return; const newPostBtn = Array.from(document.querySelectorAll('a, button, span')).find(el => el.textContent.trim().includes('发新帖') ); const container = document.createElement('div'); container.style.cssText = 'position: relative; display: inline-block; margin-left: 8px;'; const shareBtn = document.createElement('button'); shareBtn.style.cssText = ` padding: 6px 12px; background-color: ${StyleConfig.ACCENT_COLOR}; color: white; border: none; border-radius: 4px; cursor: pointer; font-family: ${StyleConfig.FONT_FAMILY}; font-size: 14px; `; shareBtn.textContent = '123云盘分享'; const searchBox = document.createElement('div'); searchBox.id = 'shareSearchBox'; searchBox.style.cssText = ` position: absolute; top: 100%; right: 0; margin-top: 4px; width: 400px; background: ${StyleConfig.BG_COLOR}; border-radius: 6px; padding: ${StyleConfig.PADDING}; box-shadow: 0 4px 20px rgba(0,0,0,0.15); z-index: 999998; display: none; `; searchBox.innerHTML = ` <div style="margin-bottom: ${StyleConfig.SPACING}px; font-size: 16px; font-weight: bold;"> 请输入关键词搜索 </div> <input type="text" id="searchInput" style=" width: 100%; padding: 8px; border: 1px solid ${StyleConfig.BORDER_COLOR}; border-radius: 4px; margin-bottom: ${StyleConfig.SPACING}px; " placeholder="例如:权力的游戏..."> <button id="searchBtn" style=" background: ${StyleConfig.ACCENT_COLOR}; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; ">搜索</button> `; const folderList = document.createElement('div'); folderList.id = 'folderListContainer'; folderList.style.cssText = ` position: absolute; top: 100%; right: 0; margin-top: 4px; width: 600px; max-height: 500px; overflow-y: auto; background: ${StyleConfig.BG_COLOR}; border-radius: 6px; padding: ${StyleConfig.PADDING}; box-shadow: 0 4px 20px rgba(0,0,0,0.15); z-index: 999998; display: none; `; container.appendChild(shareBtn); container.appendChild(searchBox); container.appendChild(folderList); // 插入页面 if (newPostBtn && newPostBtn.parentNode) { newPostBtn.parentNode.insertBefore(container, newPostBtn.nextSibling); } else { shareBtn.style.cssText = ` position: fixed; bottom: 20px; right: 20px; width: 50px; height: 50px; background: ${StyleConfig.ACCENT_COLOR}; border-radius: 50%; box-shadow: 0 2px 10px rgba(0,0,0,0.2); cursor: pointer; z-index: 999999; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; `; shareBtn.textContent = '123'; document.body.appendChild(shareBtn); document.body.appendChild(searchBox); document.body.appendChild(folderList); } // 事件绑定 shareBtn.addEventListener('click', () => { const isVisible = searchBox.style.display === 'block'; searchBox.style.display = isVisible ? 'none' : 'block'; folderList.style.display = 'none'; if (!isVisible) document.getElementById('searchInput')?.focus(); }); document.getElementById('searchBtn')?.addEventListener('click', () => { const keyword = document.getElementById('searchInput')?.value.trim(); if (keyword) searchFolders(keyword); }); document.getElementById('searchInput')?.addEventListener('keypress', (e) => { if (e.key === 'Enter') document.getElementById('searchBtn')?.click(); }); } // 搜索文件夹(处理token有效性) async function searchFolders(keyword) { if (!isTargetDomain) return; const cachedToken = getCachedToken(); if (!cachedToken) { // 无有效token,跳转获取 const currentUrl = window.location.href; console.log(`[无有效token] 准备跳转至${tokenSourceDomain},原地址=${currentUrl}`); alert('需要从www.123pan.com获取授权,请点击确定跳转'); window.location.href = `https://${tokenSourceDomain}?redirect=${encodeURIComponent(currentUrl)}`; return; } let allFiles = []; let lastFileId = 0; try { for (let i = 0; i < 3; i++) { const response = await fetch(`https://open-api.123pan.com/api/v2/file/list?parentFileId=0&searchData=${encodeURIComponent(keyword)}&searchMode=1&limit=100&lastFileId=${lastFileId}`, { method: 'GET', headers: { 'Authorization': `Bearer ${cachedToken}`, 'Platform': 'open_platform', 'LoginUuid': globalState.loginUuid || '' } }); const data = await response.json(); // 检查API返回的错误是否为token过期 if (data.code === 401 || data.message?.includes('expired')) { throw new Error('token expired'); } if (data.code !== 0) throw new Error(`[${data.code}] ${data.message}`); allFiles = allFiles.concat(data.data.fileList); lastFileId = data.data.lastFileId; if (lastFileId === -1) { break; } } const folders = allFiles .filter(item => item.type === 1) .slice(0, 20); for (const folder of folders) { folder.fullPath = await buildFullPath(folder); } showFolderList(folders); } catch (e) { console.error('[搜索失败]', e); // 若错误为token过期,触发重新获取 if (e.message.includes('expired')) { handleTokenExpired(); } else { alert(`搜索失败: ${e.message}`); } } } // 获取文件详情(处理token过期) async function getFileDetail(fileId) { if (globalState.folderCache[fileId]) { return globalState.folderCache[fileId]; } try { const cachedToken = getCachedToken(); const response = await fetch(`https://open-api.123pan.com/api/v1/file/detail?fileID=${fileId}`, { method: 'GET', headers: { 'Authorization': `Bearer ${cachedToken}`, 'Platform': 'open_platform', 'LoginUuid': globalState.loginUuid || '' } }); const data = await response.json(); // 检查token过期 if (data.code === 401 || data.message?.includes('expired')) { throw new Error('token expired'); } if (data.code !== 0) throw new Error(`[${data.code}] ${data.message}`); const detail = { filename: data.data.filename, parentFileID: data.data.parentFileID }; globalState.folderCache[fileId] = detail; Storage.setObj('folderCache', globalState.folderCache); return detail; } catch (e) { console.error(`获取文件${fileId}详情失败:`, e); if (e.message.includes('expired')) { handleTokenExpired(); } return null; } } // 构建文件路径 async function buildFullPath(folder) { const pathParts = [folder.filename]; let currentId = folder.parentFileId; for (let i = 0; i < 2; i++) { if (!currentId || currentId === 0) break; const parent = await getFileDetail(currentId); if (!parent) break; pathParts.unshift(parent.filename); currentId = parent.parentFileID; } return pathParts.join('/'); } // 显示文件夹列表 function showFolderList(folders) { const container = document.getElementById('folderListContainer'); const searchBox = document.getElementById('shareSearchBox'); if (folders.length === 0) { container.innerHTML = '<div style="padding: 10px; color: #666;">没有找到匹配的文件夹</div>'; container.style.display = 'block'; searchBox.style.display = 'none'; return; } let html = ` <div style="margin-bottom: 10px; font-weight: bold;"> 请选择文件夹(共${folders.length}个) </div> <div style="margin-bottom: 10px;"> <input type="text" id="folderIndex" style=" width: 60px; padding: 4px; border: 1px solid ${StyleConfig.BORDER_COLOR}; border-radius: 4px; " placeholder="序号"> <button id="goBtn" style=" background: ${StyleConfig.ACCENT_COLOR}; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; margin-left: 5px; ">确认</button> </div> <div style="border-top: 1px solid ${StyleConfig.BORDER_COLOR}; padding-top: 10px;"> `; folders.forEach((folder, index) => { const idx = index + 1; html += ` <div class="folderItem" data-index="${index}" style=" padding: 10px; margin-bottom: 8px; border-radius: 4px; background: white; border: 1px solid ${StyleConfig.BORDER_COLOR}; cursor: pointer; "> <div style="margin-bottom: 4px;">${idx}. ${folder.fullPath}</div> <div style="font-size: 12px; color: ${StyleConfig.LIGHT_TEXT}">ID: ${folder.fileId}</div> </div> `; }); html += '</div>'; container.innerHTML = html; container.style.display = 'block'; searchBox.style.display = 'none'; // 绑定事件 document.querySelectorAll('.folderItem').forEach(item => { item.addEventListener('click', () => { const index = parseInt(item.dataset.index); handleFolderSelect(folders[index]); }); item.addEventListener('mouseover', () => item.style.backgroundColor = StyleConfig.HOVER_COLOR); item.addEventListener('mouseout', () => item.style.backgroundColor = 'white'); }); document.getElementById('goBtn').addEventListener('click', () => { const num = parseInt(document.getElementById('folderIndex').value) - 1; if (num >= 0 && num < folders.length) { handleFolderSelect(folders[num]); } else { alert('请输入有效的序号'); } }); document.getElementById('folderIndex').addEventListener('keypress', (e) => { if (e.key === 'Enter') document.getElementById('goBtn').click(); }); } // 处理文件夹选择(处理token过期) async function handleFolderSelect(folder) { const container = document.getElementById('folderListContainer'); const folderName = folder.filename; const folderId = folder.fileId; try { const existingLink = await getExistingShareLink(folderName); if (existingLink) { copyToClipboard(existingLink); alert(`已找到现有分享链接,已复制:\n${existingLink}`); container.style.display = 'none'; return; } const newLink = await createShareLink(folderId, folderName); if (newLink) { copyToClipboard(newLink); alert(`分享链接已创建,已复制:\n${newLink}`); } container.style.display = 'none'; } catch (e) { console.error('[处理文件夹选择失败]', e); if (e.message.includes('expired')) { handleTokenExpired(); } else { alert(`操作失败: ${e.message}`); } } } // 获取已有分享链接(处理token过期) async function getExistingShareLink(targetName) { let lastShareId = 0; try { const cachedToken = getCachedToken(); while (true) { const response = await fetch(`https://open-api.123pan.com/api/v1/share/list?limit=100&lastShareId=${lastShareId}`, { method: 'GET', headers: { 'Authorization': `Bearer ${cachedToken}`, 'Platform': 'open_platform' } }); const data = await response.json(); if (data.code === 401 || data.message?.includes('expired')) { throw new Error('token expired'); } if (data.code !== 0) throw new Error(`[${data.code}] ${data.message}`); for (const share of data.data.shareList) { if (share.shareName === targetName && !share.sharePwd && !share.expired) { return `https://www.123pan.com/s/${share.shareKey}`; } } if (data.data.lastShareId === -1) break; lastShareId = data.data.lastShareId; } return null; } catch (e) { console.error('[查询已有分享失败]', e); if (e.message.includes('expired')) { throw e; // 抛给上层处理 } alert(`查询已有分享失败: ${e.message}`); return null; } } // 创建分享链接(处理token过期) async function createShareLink(fileId, name) { try { const cachedToken = getCachedToken(); const response = await fetch('https://open-api.123pan.com/api/v1/share/create', { method: 'POST', headers: { 'Authorization': `Bearer ${cachedToken}`, 'Platform': 'open_platform', 'Content-Type': 'application/json' }, body: JSON.stringify({ shareName: name, shareExpire: 0, fileIDList: [fileId].join(','), sharePwd: null }) }); const data = await response.json(); if (data.code === 401 || data.message?.includes('expired')) { throw new Error('token expired'); } if (data.code !== 0) throw new Error(`[${data.code}] ${data.message}`); return `https://www.123pan.com/s/${data.data.shareKey}`; } catch (e) { console.error('[创建分享链接失败]', e); if (e.message.includes('expired')) { throw e; // 抛给上层处理 } alert(`创建分享链接失败: ${e.message}`); return null; } } // 复制到剪贴板 function copyToClipboard(text) { navigator.clipboard.writeText(text).catch(() => { const textarea = document.createElement('textarea'); textarea.value = text; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); }); } // 初始化执行 (function init() { // 目标域名:接收token并创建UI if (isTargetDomain) { handleTokenFromURL(); createUI(); } // token来源域名:仅在有跳转参数时执行逻辑(完全静默) if (isTokenSource) { fetchTokenAndRedirect(); } })(); })();