// ==UserScript== // @name 网盘秒传助手(支持百度/光鸭/迅雷/阿里云盘/夸克/UC/腾讯微云/小飞机/蓝奏云/蓝奏云优享版/115/城通/天翼云盘/联通云盘/移动云盘/123云盘) // @namespace gy.miaochuan.assistant // @version 1.11.0 // @description 支持对百度/阿里/夸克/UC/115/光鸭/微云/123/小飞机/中国移动/天翼/城通网盘的导入/导出 和 迅雷/蓝奏云/优享版/联通云盘的导出 , 阿里/夸克/UC/115秒传互通 , ui仿123FastLink。 // @author Serena Lee // @match https://guangyapan.com/* // @match https://*.guangyapan.com/* // @match https://pan.xunlei.com/* // @match https://pan.baidu.com/* // @match https://openapi.baidu.com/* // @match https://www.aliyundrive.com/* // @match https://*.aliyundrive.com/* // @match https://www.alipan.com/* // @match https://*.alipan.com/* // @match https://pan.quark.cn/* // @match https://*.quark.cn/* // @match https://drive.uc.cn/* // @match https://pan.uc.cn/* // @match https://www.weiyun.com/* // @match https://www.feejii.com/* // @match https://*.feejii.com/* // @match https://www.ilanzou.com/* // @match https://*.ilanzou.com/* // @match https://up.woozooo.com/* // @match https://pc.woozooo.com/* // @match https://115.com/* // @match https://www.115.com/* // @match https://drive.ctfile.com/* // @match https://cloud.189.cn/* // @match https://m.cloud.189.cn/* // @match https://pan.wo.cn/* // @match https://yun.139.com/w/* // @match https://*.yun.139.com/w/* // @match https://www.123pan.com/* // @match https://123pan.com/* // @match https://*.123pan.com/* // @match https://www.123pan.cn/* // @match https://123pan.cn/* // @match https://*.123pan.cn/* // @run-at document-start // @noframes // @grant GM_setClipboard // @grant GM_getValue // @grant GM_setValue // @grant GM_openInTab // @grant GM_xmlhttpRequest // @connect d.pcs.baidu.com // @connect *.baidupcs.com // @connect openapi.baidu.com // @connect api.youxiaohou.com // @connect *.aliyundrive.com // @connect *.alipan.com // @connect *.aliyundrive.net // @connect *.quark.cn // @connect drive-pc.quark.cn // @connect drive.quark.cn // @connect *.uc.cn // @connect pc-api.uc.cn // @connect cloud.189.cn // @connect api.cloud.189.cn // @connect upload.cloud.189.cn // @connect download.cloud.189.cn // @connect * // @grant unsafeWindow // @antifeature privacy // 隐私说明:百度网盘秒传的「云令牌授权」(首启弹窗 + 设置开关,需用户主动授权后才启用)会把当前账号的 // access_token(OAuth 下载凭证)发送至第三方云服务器 http://103.91.208.143:38546,用于按文件 md5 提供 // 256KB 验证块实现跨账号秒传;选择「仅导出」可完全禁用令牌采集(导入功能随之禁用)。 // 另注:脚本会请求第三方 api.youxiaohou.com(POST /config/v2)动态拉取百度接口配置, // 该请求不携带任何账号令牌或文件数据,仅返回接口配置项。 // @license MIT // ==/UserScript== (function () { 'use strict'; const SCRIPT_VERSION = (() => { try { return GM_info.script.version; } catch (e) { return '0.7.0'; } })(); const SCRIPT_NAME = '光鸭云盘秒传助手'; const KEY_SETTINGS = 'gy_miaochuan_settings'; // ---- 本地日志(出错时导出供排查;不依赖任何在线服务)---- const MC_LOG = []; function mcLog(scope, msg) { const line = `[${new Date().toLocaleTimeString()}] [${scope}] ${String(msg)}`; MC_LOG.push(line); if (MC_LOG.length > 500) MC_LOG.shift(); try { console.log(line); } catch (e) { /* ignore */ } } /** 导出日志为文本文件(下载) */ function exportMcLog() { try { const blob = new Blob([MC_LOG.join('\n')], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = '光鸭秒传日志_' + Date.now() + '.txt'; a.click(); URL.revokeObjectURL(url); return true; } catch (e) { return false; } } const CONFIG = { debug: false, md5Delay: 80, // 获取文件MD5间隔(ms) listPageDelay: 150, // 获取文件列表分页间隔(ms) mkdirDelay: 100, // 创建目录间隔(ms) transferDelay: 120, // 逐条秒传间隔(ms) md5BatchSize: 10, // 批量获取MD5的大小 defaultExportName: '光鸭秒传_Export', fileListPageSize: 100, // 百度平台 baiduWarningBigFileMd5: true, // 大文件(分块上传)md5 可能非内容哈希,导出时提示 // 云块服务器(v0.9):百度秒传协作(contentMd5 池;阿里已改为夸克/UC 中转,不再依赖服务器) cloudServer: 'http://103.91.208.143:38546', // 生产:ChmlFrp 枣庄 BGP 穿透 → cn-qingdao ECS cloudRegister: true, // 导出时是否向服务器注册(令牌+文件表) // 夸克/UC(v1.3):download 接口返回的 md5 是账号私有哈希(仅同账号秒传可用)。 // 跨账号秒传需要真实 SHA1(全局键):导出时流式下载文件计算(不落盘,内存恒定)。 pdsComputeSha1: true, // 导出时是否下载计算真实 SHA1(支持跨账号秒传;大文件耗时) // 城通(v1.8):checksum=md5(前1MB),≤1MB 即完整内容 MD5;>1MB 仅前1MB 哈希(ctChecksum,不下载计算完整 MD5) }; // ============================== 平台检测 ============================== const PLATFORM = (() => { const h = location.hostname; if (/guangyapan\.com$/i.test(h) || h === 'guangyapan.com' || /\.guangyapan\.com$/i.test(h)) return 'guangya'; if (h === 'pan.xunlei.com') return 'xunlei'; if (h === 'pan.baidu.com') return 'baidu'; if (/aliyundrive\.com$/i.test(h) || /alipan\.com$/i.test(h) || /aliyundrive\.net$/i.test(h)) return 'aliyun'; if (h === 'pan.quark.cn' || /\.quark\.cn$/i.test(h)) return 'quark'; if (h === 'drive.uc.cn' || h === 'pan.uc.cn' || /\.uc\.cn$/i.test(h)) return 'uc'; if (h === 'www.weiyun.com' || h === 'weiyun.com') return 'weiyun'; if (h === 'www.feejii.com' || h === 'feejii.com' || /\.feejii\.com$/i.test(h)) return 'feejii'; if (h === 'www.ilanzou.com' || h === 'ilanzou.com' || /\.ilanzou\.com$/i.test(h)) return 'ilanzou'; if (h === 'up.woozooo.com' || h === 'pc.woozooo.com') return 'lanzou'; if (h === '115.com' || h === 'www.115.com') return 'p115'; if (h === 'drive.ctfile.com' || h === 'home.ctfile.com') return 'ctfile'; if (h === 'cloud.189.cn' || h === 'm.cloud.189.cn') return 'tianyi'; if (h === 'pan.wo.cn') return 'wopan'; if (h === 'yun.139.com' || /\.yun\.139\.com$/i.test(h)) return 'y139'; if (h === 'www.123pan.com' || h === '123pan.com' || /\.123pan\.com$/i.test(h) || h === 'www.123pan.cn' || h === '123pan.cn' || /\.123pan\.cn$/i.test(h)) return 'p123'; return null; })(); function isGuangya() { return PLATFORM === 'guangya'; } function isXunlei() { return PLATFORM === 'xunlei'; } function isBaidu() { return PLATFORM === 'baidu'; } function isAliyun() { return PLATFORM === 'aliyun'; } function isQuark() { return PLATFORM === 'quark'; } function isUc() { return PLATFORM === 'uc'; } function isWeiyun() { return PLATFORM === 'weiyun'; } function isFeejii() { return PLATFORM === 'feejii'; } function isIlanzou() { return PLATFORM === 'ilanzou'; } function isLanzou() { return PLATFORM === 'lanzou'; } function isP115() { return PLATFORM === 'p115'; } function isCtfile() { return PLATFORM === 'ctfile'; } function isTianyi() { return PLATFORM === 'tianyi'; } function isWopan() { return PLATFORM === 'wopan'; } function isY139() { return PLATFORM === 'y139'; } function isP123() { return PLATFORM === 'p123'; } // ============================== 工具函数 ============================== function log(...args) { if (CONFIG.debug) console.log('[秒传助手]', ...args); } function sleep(ms) { return new Promise(res => setTimeout(res, ms)); } function formatSize(bytes) { const n = Number(bytes) || 0; if (n < 1024) return `${n} B`; const units = ['KB', 'MB', 'GB', 'TB']; let v = n / 1024; let i = 0; while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; } return `${v.toFixed(2)} ${units[i]}`; } function basenameFromPath(filePath) { const s = String(filePath || '').replace(/\\/g, '/'); const parts = s.split('/').filter(Boolean); return parts.length ? parts[parts.length - 1] : 'file'; } function dirSegmentsFromPath(filePath) { const s = String(filePath || '').replace(/\\/g, '/'); const parts = s.split('/').map(p => String(p || '').trim()).filter(p => p.length > 0); return parts.length <= 1 ? [] : parts.slice(0, -1); } /** 规范化MD5:32位hex直接小写;兼容base64url变体解码 */ function normalizeMd5(md5) { const s = String(md5 == null ? '' : md5).trim(); if (!s) return ''; if (/^[a-fA-F0-9]{32}$/.test(s)) return s.toLowerCase(); try { const normalized = s.replace(/-/g, '+').replace(/_/g, '/'); const padLen = (4 - (normalized.length % 4)) % 4; const binary = atob(normalized + '='.repeat(padLen)); if (binary.length !== 16) return ''; return Array.from(binary, ch => ch.charCodeAt(0).toString(16).padStart(2, '0')).join(''); } catch { return ''; } } /** 规范化 GCID:40位大/小写十六进制 → 统一大写 */ function normalizeGcid(gcid) { const s = String(gcid == null ? '' : gcid).trim(); if (/^[a-fA-F0-9]{40}$/.test(s)) return s.toUpperCase(); return ''; } /** 规范化 SHA256:64位十六进制 → 统一小写(移动云盘 contentHash 用) */ function normalizeSha256(s) { const v = String(s == null ? '' : s).trim(); return /^[a-fA-F0-9]{64}$/.test(v) ? v.toLowerCase() : ''; } function createTraceparent() { const hex = len => { const u = new Uint8Array(len); crypto.getRandomValues(u); return [...u].map(b => b.toString(16).padStart(2, '0')).join(''); }; return `00-${hex(16)}-${hex(8)}-01`; } function loadSettings() { try { const raw = GM_getValue(KEY_SETTINGS, null); if (raw && typeof raw === 'object') Object.assign(CONFIG, raw); } catch (e) { /* ignore */ } } function saveSettings() { try { GM_setValue(KEY_SETTINGS, { ...CONFIG }); } catch (e) { /* ignore */ } } // ============================== 光鸭平台(页面拦截 + API) ============================== const GUANGYA = { API_BASE: 'https://api.guangyapan.com', CODE_INSTANT: 156, CODE_DIR_EXISTS: 159, KEY_TOKEN: 'gy_miaochuan_access_token', PAGE_STATE_KEY: '__GYMS_PAGE_STATE__', }; function guangyaPageHookSource() { // 以下代码运行在页面上下文(非沙箱) const KEY = '__GYMS_PAGE_STATE__'; const st = window[KEY] = window[KEY] || { headers: {}, lists: [], lastParentId: '', lastListAt: 0 }; const captureHeaders = (headersLike) => { try { if (!headersLike) return; let get = (name) => ''; if (typeof headersLike.get === 'function') { get = (name) => headersLike.get(name) || ''; } else if (Array.isArray(headersLike)) { const arr = headersLike; get = (name) => { const f = arr.find(p => String(p[0]).toLowerCase() === name); return f ? String(f[1]) : ''; }; } else if (typeof headersLike === 'object') { const obj = headersLike; get = (name) => obj[name] || obj[String(name).toLowerCase()] || ''; } const auth = get('authorization'); if (auth && /^Bearer\s+/i.test(auth)) st.headers.authorization = auth; const did = get('did'); if (did) st.headers.did = did; const dt = get('dt'); if (dt) st.headers.dt = dt; const smid = get('smid'); if (smid) st.headers.smid = smid; } catch (e) { /* ignore */ } }; const captureListBody = (body) => { try { if (!body) return; const b = typeof body === 'string' ? JSON.parse(body) : body; if (b && b.parentId !== undefined) st.lastParentId = String(b.parentId || ''); } catch (e) { /* ignore */ } }; const pushList = (list) => { try { if (!Array.isArray(list) || !list.length) return; const items = list.map(it => ({ fileId: String(it.fileId || ''), fileName: String(it.fileName || ''), fileSize: Number(it.fileSize || 0), gcid: String(it.gcid || ''), dirType: Number(it.dirType || 1), resType: Number(it.resType || 1), ext: String(it.ext || ''), })); st.lists.push(...items); if (st.lists.length > 30000) st.lists.splice(0, st.lists.length - 30000); st.lastListAt = Date.now(); } catch (e) { /* ignore */ } }; const looksLikeList = (url) => typeof url === 'string' && /\/get_file_list/.test(url); const originalFetch = window.fetch ? window.fetch.bind(window) : null; if (originalFetch) { window.fetch = async function (input, init) { try { const url = typeof input === 'string' ? input : (input && (input.url || input.href)) || ''; if (url.includes('api.guangyapan.com')) { captureHeaders(init && init.headers); if (looksLikeList(url) && init && init.body) captureListBody(init.body); } } catch (e) { /* ignore */ } const resp = await originalFetch(input, init); try { if (resp && resp.ok && looksLikeList(resp.url || '') && typeof resp.clone === 'function') { resp.clone().json().then(j => { const list = j && j.data && Array.isArray(j.data.list) ? j.data.list : []; pushList(list); }).catch(() => {}); } } catch (e) { /* ignore */ } return resp; }; } // 光鸭勾选框仅 hover 渲染,change 事件记录勾选集合(页面上下文也挂一份,保险) document.addEventListener('change', function (e) { try { const input = e.target; if (!input || input.tagName !== 'INPUT' || !/swangpan-checkbox__input/.test(input.className || '')) return; const row = input.closest('.swangpan-file-list-table__row'); const checked = !!(input.checked || input.getAttribute('aria-checked') === 'true'); if (!row) { st.selectAll = checked; st.selectedNames = {}; } else { const nameEl = row.querySelector('.swangpan-file-list-table__titleCell'); const name = nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : ''; if (name) { st.selectedNames = st.selectedNames || {}; st.selectedNames[name] = checked ? 1 : 0; } } st.selectionVersion = (st.selectionVersion || 0) + 1; } catch (err) { /* ignore */ } }, true); const origOpen = XMLHttpRequest.prototype.open; const origSetHeader = XMLHttpRequest.prototype.setRequestHeader; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this.__gymsUrl = url; return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.setRequestHeader = function (name, value) { try { if (/^authorization$/i.test(name) && /^Bearer\s+/i.test(String(value))) st.headers.authorization = String(value); if (/^did$/i.test(name)) st.headers.did = String(value); if (/^dt$/i.test(name)) st.headers.dt = String(value); if (/^smid$/i.test(name)) st.headers.smid = String(value); } catch (e) { /* ignore */ } return origSetHeader.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { try { const url = String(this.__gymsUrl || ''); if (looksLikeList(url) && body) captureListBody(body); } catch (e) { /* ignore */ } this.addEventListener('load', function () { try { const url = String(this.__gymsUrl || ''); if (looksLikeList(url) && this.status >= 200 && this.status < 300 && this.responseText) { const j = JSON.parse(this.responseText); const list = j && j.data && Array.isArray(j.data.list) ? j.data.list : []; pushList(list); } } catch (e) { /* ignore */ } }); return origSend.apply(this, arguments); }; } function guangyaInjectPageHook() { try { if (window[GUANGYA.PAGE_STATE_KEY]) return; } catch (e) { /* ignore */ } const script = document.createElement('script'); script.textContent = `(${guangyaPageHookSource.toString()})();`; (document.documentElement || document.head || document.body).appendChild(script); script.remove(); } function guangyaGetPageState() { try { const w = (typeof unsafeWindow !== 'undefined' && unsafeWindow) || window; return w[GUANGYA.PAGE_STATE_KEY] || {}; } catch (e) { return {}; } } class GuangyaApi { constructor() { this.token = ''; this.tokenSource = ''; } getStoredToken() { try { const t = String(GM_getValue(GUANGYA.KEY_TOKEN, '') || '').trim(); if (t) return { token: t, source: 'GM' }; } catch (e) { /* ignore */ } try { for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && k.startsWith('credentials_')) { const j = JSON.parse(localStorage.getItem(k)); if (j && typeof j.access_token === 'string' && j.access_token.length > 20) { return { token: j.access_token.trim(), source: 'localStorage' }; } } } } catch (e) { /* ignore */ } const st = guangyaGetPageState(); const auth = st.headers && st.headers.authorization; if (auth) { const t = String(auth).replace(/^Bearer\s+/i, '').trim(); if (t) return { token: t, source: 'captured' }; } return null; } async getToken(forcePrompt = false) { if (!forcePrompt && this.token) return this.token; const found = this.getStoredToken(); if (found) { this.token = found.token; this.tokenSource = found.source; return this.token; } const t = await this.promptPasteToken(); if (t) { this.token = t; this.tokenSource = 'manual'; try { GM_setValue(GUANGYA.KEY_TOKEN, t); } catch (e) { /* ignore */ } return t; } return ''; } clearToken() { this.token = ''; this.tokenSource = ''; try { GM_setValue(GUANGYA.KEY_TOKEN, null); } catch (e) { /* ignore */ } } promptPasteToken() { return new Promise(resolve => { const overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);z-index:2147483647;display:flex;align-items:center;justify-content:center;font-family:system-ui,sans-serif;'; overlay.innerHTML = `
未找到光鸭登录凭证
请在已登录状态下打开开发者工具(F12) → Network,点选任意发往 api.guangyapan.com 的请求,在请求头里复制 Authorization 的 Bearer 后面整段 token 粘贴到下面。
`; document.body.appendChild(overlay); const input = overlay.querySelector('#gy-ms-token-input'); const cleanup = () => overlay.remove(); overlay.querySelector('#gy-ms-token-cancel').onclick = () => { cleanup(); resolve(''); }; overlay.querySelector('#gy-ms-token-ok').onclick = () => { let v = String(input.value || '').trim(); if (/^Bearer\s+/i.test(v)) v = v.replace(/^Bearer\s+/i, '').trim(); cleanup(); resolve(v); }; input.focus(); }); } async request(path, body, opts = {}) { const allowCodes = opts.allowCodes || [0]; const token = await this.getToken(); if (!token) throw new Error('未获取到登录凭证,请刷新光鸭页面或手动粘贴 token'); // did/smid 头会干扰部分接口鉴权(实测 get_res_download_url 带 did/smid 会 401),不发送 const headers = { 'Content-Type': 'application/json;charset=utf-8', 'Authorization': 'Bearer ' + token, 'dt': '4', 'traceparent': createTraceparent(), 'Origin': location.origin, 'Referer': location.href, }; let resp; try { resp = await fetch(GUANGYA.API_BASE + path, { method: 'POST', headers, body: JSON.stringify(body) }); } catch (e) { throw new Error(`网络请求失败: ${e.message}`); } let j; try { j = await resp.json(); } catch (e) { throw new Error(`HTTP ${resp.status} 响应解析失败`); } if (resp.status < 200 || resp.status >= 300) { throw new Error(j.msg || j.message || `HTTP ${resp.status}`); } const code = j.code; if (code != null && !allowCodes.includes(code)) { const err = new Error(j.msg || `接口错误 code=${code}`); err.code = code; throw err; } return j; } async getFileList(parentId, page = 0) { const j = await this.request('/userres/v1/file/get_file_list', { pageSize: CONFIG.fileListPageSize, orderBy: 3, sortType: 1, parentId: String(parentId || ''), page, }); return (j.data && Array.isArray(j.data.list)) ? j.data.list : []; } /** 获取文件哈希/大小:实测 get_file_detail 返回 md5/gcid/fileSize */ async getFileDetail(fileId) { const j = await this.request('/userres/v1/file/get_file_detail', { fileId: String(fileId) }); const info = (j.data && j.data.fileInfo) || {}; return { md5: info.md5 ? String(info.md5).toLowerCase() : '', gcid: info.gcid ? String(info.gcid).toUpperCase() : '', fileSize: Number(info.fileSize || 0), }; } async createDir(parentId, dirName) { const j = await this.request('/userres/v1/file/create_dir', { parentId: String(parentId || ''), dirName: String(dirName), failIfNameExist: true, }, { allowCodes: [0, GUANGYA.CODE_DIR_EXISTS] }); const id = j.data && j.data.fileId; if (!id) throw new Error('创建目录未返回 ID'); return String(id); } /** 秒传(MD5 优先):返回 {hit, taskId} */ async flashUpload(name, md5, gcid, size, parentId) { // 优先用 MD5 秒传(实测 capacity:30 + res:{md5, fileSize} → code 156 命中) if (md5) { return await this.flashUploadByMd5(name, md5, size, parentId); } // 兜底:只有 GCID 时走 GCID 检测 if (gcid) { return await this.flashUploadByGcid(name, gcid, size, parentId); } return { hit: false, taskId: '' }; } async flashUploadByMd5(name, md5, size, parentId) { const j = await this.request('/userres/v2/get_res_center_token', { capacity: 30, name: String(name), res: { md5: String(md5), fileSize: Number(size || 0) }, parentId: String(parentId || ''), }, { allowCodes: [0, GUANGYA.CODE_INSTANT] }); const taskId = j.data && (j.data.taskId != null ? String(j.data.taskId) : ''); if (j.code === GUANGYA.CODE_INSTANT) { return { hit: true, taskId }; } if (taskId) { try { await this.request('/userres/v1/file/delete_upload_task', { taskIds: [taskId] }); } catch (e) { /* ignore */ } } return { hit: false, taskId }; } /** GCID 路径:get_res_center_token(不带md5) → check_can_flash_upload */ async flashUploadByGcid(name, gcid, size, parentId) { const j = await this.request('/userres/v2/get_res_center_token', { capacity: 30, name: String(name), res: { fileSize: Number(size || 0) }, parentId: String(parentId || ''), }, { allowCodes: [0] }); const taskId = j.data && (j.data.taskId != null ? String(j.data.taskId) : ''); if (taskId) { try { const cj = await this.request('/userres/v1/check_can_flash_upload', { taskId: String(taskId), gcid: String(gcid) }, { allowCodes: [0] }); if (cj.data && cj.data.canFlashUpload) { return { hit: true, taskId }; } } catch (e) { /* ignore */ } try { await this.request('/userres/v1/file/delete_upload_task', { taskIds: [taskId] }); } catch (e) { /* ignore */ } } return { hit: false, taskId }; } } // 光鸭勾选读取 function guangyaGetSelectionFromFiber() { const roots = Array.from(document.querySelectorAll('.swangpan-file-list-table__root')); let best = null; for (const root of roots) { const fk = Object.keys(root).find(k => k.startsWith('__reactFiber$') || k.startsWith('__reactContainer$')); if (!fk) continue; let node = root[fk]; for (let i = 0; i < 25 && node; i++, node = node.return) { const mp = node.memoizedProps; if (mp && Array.isArray(mp.selectedItems)) { const dsLen = Array.isArray(mp.dataSource) ? mp.dataSource.length : 0; const bestLen = best && Array.isArray(best.dataSource) ? best.dataSource.length : 0; if (dsLen > bestLen) best = mp; break; } } } return best; } function guangyaGetCheckedItems() { const fiber = guangyaGetSelectionFromFiber(); const source = fiber && Array.isArray(fiber.dataSource) ? fiber.dataSource : (() => { const st = guangyaGetPageState(); const seen = new Set(); return (Array.isArray(st.lists) ? st.lists : []).filter(x => { const k = String(x.fileId || ''); if (!k || seen.has(k)) return false; seen.add(k); return true; }); })(); const headCb = document.querySelector('[class*="file-list-table"] [class*="head"] .swangpan-checkbox__input, [class*="file-list-table"] [class*="header"] .swangpan-checkbox__input'); const headAria = headCb ? headCb.getAttribute('aria-checked') : null; const st = guangyaGetPageState(); const names = st.selectedNames || {}; document.querySelectorAll('.swangpan-file-list-table__row').forEach(row => { const cb = row.querySelector('.swangpan-checkbox__input'); if (cb && (cb.checked || cb.getAttribute('aria-checked') === 'true')) { const nameEl = row.querySelector('.swangpan-file-list-table__titleCell'); const name = (nameEl ? nameEl.textContent : '').replace(/\s+/g, ' ').trim(); if (name) names[name] = 1; } }); if (headAria === 'true') { st.selectAll = true; return { all: true, items: source.slice() }; } let items; if (headAria === 'mixed') { items = st.selectAll === true ? source.filter(x => names[x.fileName] !== 0) : source.filter(x => names[x.fileName] === 1); } else { items = source.filter(x => names[x.fileName] === 1); } return { all: false, items }; } function guangyaGetLastParentId() { const st = guangyaGetPageState(); return st.lastParentId != null ? String(st.lastParentId) : ''; } // ============================== 迅雷平台(API + Vuex 读取) ============================== const XUNLEI = { API_BASE: 'https://api-pan.xunlei.com', PAGE_STATE_KEY: '__XLMS_PAGE_STATE__', CLIENT_ID: 'Xqp0kJBXWhwaTpB6', // 迅雷云盘网页 client_id XDRIVE_CLIENT_ID: 'ZUBzD9J_XPXfn7f7', // 迅雷浏览器 client_id(openlist 同款) }; /** 从 localStorage 读取迅雷云盘凭证(access_token / captcha_token / device_id) */ function xunleiGetCredentials() { let accessToken = ''; let captchaToken = ''; let deviceId = ''; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); const v = localStorage.getItem(k); if (!v) continue; if (k.startsWith('credentials_')) { try { const j = JSON.parse(v); if (j.access_token) accessToken = j.access_token; } catch (e) {} } if (k.startsWith('captcha_')) { try { const j = JSON.parse(v); if (j.token) captchaToken = j.token; } catch (e) {} } if (k === 'deviceid' && v && v.length > 10) { deviceId = v.replace(/^wdi10\./, '').slice(0, 40); } } return { accessToken, captchaToken, deviceId }; } function xunleiGetHeaders() { const cred = xunleiGetCredentials(); const h = { 'Authorization': 'Bearer ' + cred.accessToken, 'x-captcha-token': cred.captchaToken, 'x-client-id': XUNLEI.CLIENT_ID, }; if (cred.deviceId) h['x-device-id'] = cred.deviceId; h['Content-Type'] = 'application/json'; h['Referer'] = location.origin + '/'; return h; } /** 迅雷文件列表(分页) */ async function xunleiGetFileList(parentId, pageToken) { const filters = encodeURIComponent(JSON.stringify({ phase: { eq: 'PHASE_TYPE_COMPLETE' }, trashed: { eq: false } })); const qs = new URLSearchParams({ parent_id: String(parentId || ''), usage: 'DISPLAY', filters, with_audit: 'true', thumbnail_size: 'SIZE_SMALL', limit: String(CONFIG.fileListPageSize), }); if (pageToken) qs.set('page_token', pageToken); const resp = await fetch(`${XUNLEI.API_BASE}/drive/v1/files?${qs.toString()}`, { method: 'GET', headers: xunleiGetHeaders() }); const j = await resp.json(); if (!resp.ok) throw new Error(j.error_description || j.error || `HTTP ${resp.status}`); return { list: j.files || [], nextPageToken: j.next_page_token || '' }; } /** 迅雷文件详情(获取 md5/gcid/size;列表条目通常已含,此接口兜底) */ async function xunleiGetFileDetail(fileId) { const fid = encodeURIComponent(String(fileId)); const resp = await fetch(`${XUNLEI.API_BASE}/drive/v1/files/${fid}?usage=DISPLAY`, { method: 'GET', headers: xunleiGetHeaders() }); const j = await resp.json(); if (!resp.ok) throw new Error(j.error_description || j.error || `HTTP ${resp.status}`); return { md5: j.md5_checksum ? String(j.md5_checksum).toLowerCase() : '', gcid: j.hash ? normalizeGcid(j.hash) : '', fileSize: Number(j.size || 0), kind: j.kind || '', phase: j.phase || '', }; } /** 从 Vuex drive.all 读取当前目录文件列表(id→file 的 map,迅雷文件对象自带 md5/hash) */ function xunleiGetVuexDriveList() { const all = document.querySelectorAll('*'); for (const el of all) { const v = el.__vue__; if (v && v.$store && v.$store.state && v.$store.state.drive) { const drive = v.$store.state.drive; const driveAll = drive.all; if (driveAll && typeof driveAll === 'object') { const list = []; for (const k of Object.keys(driveAll)) { const f = driveAll[k]; if (f && typeof f === 'object' && f.id) list.push(f); } return list; } } } return null; } /** 迅雷勾选读取:$props.selected 为全局广播的选中 fileId 数组(每行同值), * 这是最可靠的选中数据源;从 Vuex drive.all 按 id 匹配取完整对象(含 md5/hash)。 * 仅返回 drive#folder / drive#file(过滤 drive#task 等任务记录)。 */ function xunleiGetCheckedItems() { const raw = xunleiGetVuexDriveList() || []; const source = raw.filter(f => f && (f.kind === 'drive#folder' || f.kind === 'drive#file')); // 读取全局广播的选中 id 数组(任意一行即可,取第一个非空) let selectedIds = null; const rows = Array.from(document.querySelectorAll('li.SourceListItem__item--XxpOC')); for (const li of rows) { const v = li.__vue__; if (v && v.$props && Array.isArray(v.$props.selected)) { selectedIds = v.$props.selected.map(String); break; } } if (!selectedIds || !selectedIds.length) { // 兜底:行内 checkbox checked const nameSet = new Set(); for (const li of rows) { const cb = li.querySelector('input[type="checkbox"]'); const v = li.__vue__; const info = v && v.$props && v.$props.info; if (cb && (cb.checked || cb.getAttribute('aria-checked') === 'true') && info && (info.kind === 'drive#folder' || info.kind === 'drive#file') && info.name) { nameSet.add(info.name); } } if (nameSet.size) { const items = source.filter(f => nameSet.has(f.name)); return { all: items.length > 0 && items.length === source.length, items }; } return { all: false, items: [] }; } const items = source.filter(f => selectedIds.includes(String(f.id))); const all = items.length > 0 && items.length === source.length; return { all, items }; } /** 迅雷当前目录 parentId(从 Vuex drive.parentId 或 URL) */ function xunleiGetLastParentId() { const all = document.querySelectorAll('*'); for (const el of all) { const v = el.__vue__; if (v && v.$store && v.$store.state && v.$store.state.drive && v.$store.state.drive.parentId != null) { return String(v.$store.state.drive.parentId); } } const m = location.hash.match(/\/\/([^/]+)/); return m ? m[1] : ''; } /** 迅雷本地 pageState(勾选记录,由用户脚本层维护) */ function xunleiGetPageState() { try { const w = (typeof unsafeWindow !== 'undefined' && unsafeWindow) || window; return w[XUNLEI.PAGE_STATE_KEY] || {}; } catch (e) { return {}; } } // ============================== 百度平台(API + DOM,接口实测 2026-10) ============================== const BAIDU = { API_BASE: 'https://pan.baidu.com', APP_ID: '250528', CHANNEL: 'chunlei', PAGE_STATE_KEY: '__GYMS_BAIDU_STATE__', _bdstoken: '', _bdstokenAt: 0, /** * 获取 bdstoken(实测从页面 script 标签提取有效;带 10 分钟缓存) */ getBdstoken() { try { const sp = new URLSearchParams(location.search); const t = sp.get('bdstoken') || sp.get('token'); if (t && t.length > 10) return t; } catch (e) { /* ignore */ } if (BAIDU._bdstoken && Date.now() - BAIDU._bdstokenAt < 10 * 60 * 1000) return BAIDU._bdstoken; try { const scripts = document.querySelectorAll('script'); const max = Math.min(scripts.length, 80); for (let i = 0; i < max; i++) { const text = scripts[i].textContent || ''; const m = text.match(/"bdstoken"\s*[=:]\s*"([^"]{20,64})"/); if (m && m[1]) { BAIDU._bdstoken = m[1]; BAIDU._bdstokenAt = Date.now(); return m[1]; } } } catch (e) { /* ignore */ } return ''; }, /** * 百度网盘现行 md5 加密(与 Alist drivers/baidu_netdisk EncryptMd5 一致,2026 实测有效): * 重组(swap前后8位块) → 逐位 XOR(15&i) → 第9位替换为 chr('g'+val) */ encryptMd5(plain) { const p = String(plain || '').toLowerCase(); if (!/^[0-9a-f]{32}$/.test(p)) return p; const swapped = p.slice(8, 16) + p.slice(0, 8) + p.slice(24, 32) + p.slice(16, 24); let r = ''; for (let i = 0; i < 32; i++) r += (parseInt(swapped[i], 16) ^ (15 & i)).toString(16); const s = String.fromCharCode('g'.charCodeAt(0) + parseInt(r[9], 16)); return r.slice(0, 9) + s + r.slice(10); }, /** * 解密(逆过程,Alist 现行 DecryptMd5)。已是 32 位 hex 则原样返回; * 加密串第 9 位为 'g'~'v' 映射 0~15。 */ decryptMd5(enc) { const e = String(enc == null ? '' : enc).toLowerCase(); if (/^[0-9a-f]{32}$/.test(e)) return e; if (e.length !== 32) return e; let o = ''; for (let i = 0; i < 32; i++) { let n; if (i === 9) { n = e.charCodeAt(i) - 'g'.charCodeAt(0); if (n < 0 || n > 15) return e; } else { const c = e[i]; if (!/^[0-9a-f]$/.test(c)) return e; n = parseInt(c, 16); } o += (n ^ (15 & i)).toString(16); } return o.slice(8, 16) + o.slice(0, 8) + o.slice(24, 32) + o.slice(16, 24); }, /** * 标准字符串 MD5(用于 data_offset 伪随机公式;逐字自 blueimp-md5,已验证)。 */ md5Hex(str) { // blueimp rstrMD5 + rstr2hex(原始字符串,不转 UTF-8;ASCII 与字节串均正确) const safeAdd = (x, y) => { const lsw = (x & 0xffff) + (y & 0xffff); const msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xffff); }; const bitRotateLeft = (num, cnt) => (num << cnt) | (num >>> (32 - cnt)); const md5cmn = (q, a, b, x, s, t) => safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b); const md5ff = (a, b, c, d, x, s, t) => md5cmn((b & c) | (~b & d), a, b, x, s, t); const md5gg = (a, b, c, d, x, s, t) => md5cmn((b & d) | (c & ~d), a, b, x, s, t); const md5hh = (a, b, c, d, x, s, t) => md5cmn(b ^ c ^ d, a, b, x, s, t); const md5ii = (a, b, c, d, x, s, t) => md5cmn(c ^ (b | ~d), a, b, x, s, t); function binlMD5(x, len) { x[len >> 5] |= 0x80 << (len % 32); x[(((len + 64) >>> 9) << 4) + 14] = len; let i, olda, oldb, oldc, oldd; let a = 1732584193, b = -271733879, c = -1732584194, d = 271733878; for (i = 0; i < x.length; i += 16) { olda = a; oldb = b; oldc = c; oldd = d; a = md5ff(a, b, c, d, x[i], 7, -680876936); d = md5ff(d, a, b, c, x[i + 1], 12, -389564586); c = md5ff(c, d, a, b, x[i + 2], 17, 606105819); b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330); a = md5ff(a, b, c, d, x[i + 4], 7, -176418897); d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426); c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341); b = md5ff(b, c, d, a, x[i + 7], 22, -45705983); a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416); d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417); c = md5ff(c, d, a, b, x[i + 10], 17, -42063); b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162); a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682); d = md5ff(d, a, b, c, x[i + 13], 12, -40341101); c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290); b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329); a = md5gg(a, b, c, d, x[i + 1], 5, -165796510); d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632); c = md5gg(c, d, a, b, x[i + 11], 14, 643717713); b = md5gg(b, c, d, a, x[i], 20, -373897302); a = md5gg(a, b, c, d, x[i + 5], 5, -701558691); d = md5gg(d, a, b, c, x[i + 10], 9, 38016083); c = md5gg(c, d, a, b, x[i + 15], 14, -660478335); b = md5gg(b, c, d, a, x[i + 4], 20, -405537848); a = md5gg(a, b, c, d, x[i + 9], 5, 568446438); d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690); c = md5gg(c, d, a, b, x[i + 3], 14, -187363961); b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501); a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467); d = md5gg(d, a, b, c, x[i + 2], 9, -51403784); c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473); b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734); a = md5hh(a, b, c, d, x[i + 5], 4, -378558); d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463); c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562); b = md5hh(b, c, d, a, x[i + 14], 23, -35309556); a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060); d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353); c = md5hh(c, d, a, b, x[i + 7], 16, -155497632); b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640); a = md5hh(a, b, c, d, x[i + 13], 4, 681279174); d = md5hh(d, a, b, c, x[i], 11, -358537222); c = md5hh(c, d, a, b, x[i + 3], 16, -722521979); b = md5hh(b, c, d, a, x[i + 6], 23, 76029189); a = md5hh(a, b, c, d, x[i + 9], 4, -640364487); d = md5hh(d, a, b, c, x[i + 12], 11, -421815835); c = md5hh(c, d, a, b, x[i + 15], 16, 530742520); b = md5hh(b, c, d, a, x[i + 2], 23, -995338651); a = md5ii(a, b, c, d, x[i], 6, -198630844); d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415); c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905); b = md5ii(b, c, d, a, x[i + 5], 21, -57434055); a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571); d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606); c = md5ii(c, d, a, b, x[i + 10], 15, -1051523); b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799); a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359); d = md5ii(d, a, b, c, x[i + 15], 10, -30611744); c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380); b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649); a = md5ii(a, b, c, d, x[i + 4], 6, -145523070); d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379); c = md5ii(c, d, a, b, x[i + 2], 15, 718787259); b = md5ii(b, c, d, a, x[i + 9], 21, -343485551); a = safeAdd(a, olda); b = safeAdd(b, oldb); c = safeAdd(c, oldc); d = safeAdd(d, oldd); } return [a, b, c, d]; } function rstr2binl(input) { const output = []; output[(input.length >> 2) - 1] = undefined; for (let i = 0; i < output.length; i += 1) { output[i] = 0; } const length8 = input.length * 8; for (let i = 0; i < length8; i += 8) { output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << (i % 32); } return output; } function binl2rstr(input) { let output = ''; const length32 = input.length * 32; for (let i = 0; i < length32; i += 8) { output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xff); } return output; } function rstr2hex(input) { const hexTab = '0123456789abcdef'; let output = ''; for (let i = 0; i < input.length; i += 1) { const x = input.charCodeAt(i); output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f); } return output; } function rstrMD5(s) { return binl2rstr(binlMD5(rstr2binl(s), s.length * 8)); } return rstr2hex(rstrMD5(String(str))); }, /** * 当前账号 uk(oper_id)。实测从 filemetas 响应取;缓存 1 小时。 */ async getUk() { if (BAIDU._uk && Date.now() - BAIDU._ukAt < 3600 * 1000) return BAIDU._uk; try { const j = await BAIDU.apiGet('/api/list', { dir: '/', order: 'name', desc: '0', showempty: '0', web: '1', page: '1', num: '10', channel: BAIDU.CHANNEL, app_id: BAIDU.APP_ID, }); const first = (j.list || []).find(x => Number(x.isdir) !== 1) || (j.list || [])[0]; if (first && first.fs_id) { const m = await BAIDU.apiGet('/api/filemetas', { fsids: JSON.stringify([Number(first.fs_id)]), dlink: '0', thumb: '0', extra: '0', needmedia: '0', detail: '1', channel: BAIDU.CHANNEL, web: '1', app_id: BAIDU.APP_ID, }); const it = (m.info || [])[0]; const uk = it && (it.oper_id || it.owner_id); if (uk) { BAIDU._uk = String(uk); BAIDU._ukAt = Date.now(); return BAIDU._uk; } } } catch (e) { /* ignore */ } return ''; }, /** * rapidupload 的 data_offset 伪随机公式(实测逆向自官方上传器): * offset = parseInt(md5(uk + contentMd5 + data_time).slice(0,8), 16) % (size - 256KB + 1) * 服务器校验该一致性(错误 offset → errno 2;字节不匹配 → errno 404;匹配 → errno 0 命中) */ computeDataOffset(uk, contentMd5, dataTime, size) { const SIZE256 = 262144; const n = Number(size) || 0; if (!uk || !contentMd5 || n <= SIZE256) return 0; const i = String(uk) + String(contentMd5) + String(dataTime); const hash = BAIDU.md5Hex(i); return parseInt(hash.slice(0, 8), 16) % (n - SIZE256 + 1); }, /** * 通过 dlink 下载文件片段(Range),供秒传校验字节与 slice-md5 计算。 * 照抄「网盘直链下载助手」/OpenList 官方规程: * - 可下载直链 = dlink + '&access_token=' + 下载凭证 token(OAuth 获取,形如 123.xxx) * - UA 用 "netdisk"(OpenList CustomCrackUA 默认值) * 无 token 时仅尝试原 dlink(可能 403 sign error,此时请按提示配置 token)。 */ downloadSlice(dlink, offset, length) { const end = Number(offset) + Number(length) - 1; const rangeHeader = `bytes=${offset}-${end}`; const token = (() => { try { return GM_getValue('baidu_access_token', '') || ''; } catch (e) { return ''; } })(); const finalUrl = token ? dlink + (dlink.includes('?') ? '&' : '?') + 'access_token=' + encodeURIComponent(token) : dlink; const cfgUa = (() => { try { const c = GM_getValue('baidu_cfg', null); return c && c.ua ? c.ua : ''; } catch (e) { return ''; } })(); const attempts = [ { 'User-Agent': cfgUa || 'pan.baidu.com', 'Referer': location.origin + '/disk/main', 'Range': rangeHeader }, { 'User-Agent': 'netdisk', 'Referer': location.origin + '/disk/main', 'Range': rangeHeader }, { 'User-Agent': 'netdisk', 'Range': rangeHeader }, { 'User-Agent': 'pan.baidu.com', 'Referer': location.origin + '/disk/main', 'Range': rangeHeader }, { 'User-Agent': navigator.userAgent, 'Referer': location.origin + '/disk/main', 'Range': rangeHeader }, { 'User-Agent': cfgUa || 'pan.baidu.com' }, ]; const doRequest = (headers) => new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('当前环境无 GM_xmlhttpRequest,无法下载文件片段')); return; } GM_xmlhttpRequest({ method: 'GET', url: finalUrl, headers, responseType: 'arraybuffer', timeout: 60000, onload: (resp) => { if (resp.status >= 200 && resp.status < 300) { try { resolve(new Uint8Array(resp.response)); } catch (e) { resolve(new Uint8Array(resp.responseText.split('').map(c => c.charCodeAt(0) & 0xFF))); } } else { const err = new Error(`下载失败 HTTP ${resp.status}(${String(resp.responseText || '').slice(0, 100)})`); err.status = resp.status; reject(err); } }, onerror: () => reject(new Error('下载网络错误')), ontimeout: () => reject(new Error('下载超时')), }); }); return (async () => { let lastErr = null; for (const h of attempts) { try { const bytes = await doRequest(h); if (bytes && bytes.length) return bytes; } catch (e) { lastErr = e; } } throw lastErr || new Error('下载失败'); })(); }, /** * 探测 dlink 响应头中的真实内容 md5(大文件 filemetas 的 md5 字段可能非内容哈希)。 * 百度下载响应可能带 Content-MD5 / ETag / X-Bs-* 等;有则返回 32 位 hex,无则返回空串。 */ probeDlinkMd5(dlink) { return new Promise((resolve) => { if (typeof GM_xmlhttpRequest !== 'function') { resolve(''); return; } const done = (v) => resolve(v || ''); GM_xmlhttpRequest({ method: 'GET', url: dlink, headers: { 'User-Agent': 'pan.baidu.com', 'Referer': location.origin + '/disk/main', 'Range': 'bytes=0-0' }, timeout: 20000, onload: (resp) => { try { const h = String(resp.responseHeaders || '').toLowerCase(); // 常见 md5 响应头字段 for (const key of ['content-md5', 'x-bs-md5', 'x-md5', 'x-checksum-md5', 'etag', 'content-checksum-md5']) { const m = h.match(new RegExp('^' + key + '\\s*:\\s*(.+)', 'm')); if (m) { const val = String(m[1]).trim().replace(/["';]/g, ''); const md5 = val.match(/[0-9a-f]{32}/i); if (md5) { done(md5[0].toLowerCase()); return; } } } done(''); } catch (e) { done(''); } }, onerror: () => done(''), ontimeout: () => done(''), }); }); }, /** 获取文件 dlink(filemetas dlink=1,签名 URL) */ /** * 获取 dlink。照抄 OpenList linkCrack:target=["/path"]&dlink=1&web=5&origin=dlna * (网页版生成可下载 dlink 的方式;fsIds 方式的 dlink 下载会 403 sign error)。 * @param fsId 文件 fs_id * @param path 文件绝对路径(如 /33.7z),linkCrack 需要 */ /** * 获取 dlink(官方接口,照抄「网盘直链下载助手」pcs[0]): * rest/2.0/xpan/multimedia?method=filemetas&dlink=1&fsids=[...]&access_token=TOKEN * 返回的 dlink 带完整签名(FDtAERK + vuk),配合 access_token 可直接下载(已实测 206)。 */ async getDlink(fsId, path) { const token = BAIDU.getAccessTokenCached(); const cfg = BAIDU.getCachedConfig(); const base = (cfg && cfg.filemetasUrl) || 'https://pan.baidu.com/rest/2.0/xpan/multimedia?method=filemetas&dlink=1'; const qs = new URLSearchParams({ fsids: JSON.stringify([Number(fsId)]) }); if (token) qs.set('access_token', token); const url = base + (base.includes('?') ? '&' : '?') + qs.toString(); const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 15000); let resp; try { resp = await fetch(url, { credentials: 'include', headers: { 'Referer': location.origin + '/disk/main', 'User-Agent': 'pan.baidu.com' }, signal: ctrl.signal }); } finally { clearTimeout(timer); } const j = await resp.json(); const it = (j.list || [])[0]; return it && it.dlink ? String(it.dlink) : ''; }, /** * 百度下载凭证 access_token 自动获取(照抄「网盘直链下载助手」OAuth 流程): * - 已缓存则直接用;未缓存则 GM_openInTab 打开 openapi authorize 页(用户已登录百度 → 自动/一键授权) * - 脚本在 openapi 页(oauthPageHook)自动点确认 + 从 login_success URL 提取 token 存 GM * - 这里轮询等待 token;超时后降级为手动粘贴提示 */ authorizeUrl: 'https://openapi.baidu.com/oauth/2.0/authorize?client_id=IlLqBbU3GjQ0t46TRwFateTprHWl39zF&response_type=token&redirect_uri=oob&confirm_login=0&scope=basic,netdisk', // ============ 云令牌授权(首启二选一:仅导出 / 继续) ============ // gy_cloud_consent:未设置=首启待选择;'1'=同意提供令牌(完整功能);'0'=仅导出(不采集令牌、禁用导入) cloudConsentState() { try { const v = GM_getValue('gy_cloud_consent', ''); if (v === '1') return 'yes'; if (v === '0') return 'no'; } catch (e) { /* ignore */ } return ''; }, needsConsentPrompt() { return BAIDU.cloudConsentState() === ''; }, cloudConsentEnabled() { return BAIDU.cloudConsentState() === 'yes'; }, setCloudConsent(v) { try { GM_setValue('gy_cloud_consent', v ? '1' : '0'); } catch (e) { /* ignore */ } }, getAccessTokenCached() { try { return GM_getValue('baidu_access_token', '') || ''; } catch (e) { return ''; } }, /** token 获取时间戳(GMT 存储,用于过期前自动刷新) */ getAccessTokenAt() { try { return Number(GM_getValue('baidu_access_token_at', 0)) || 0; } catch (e) { return 0; } }, /** 保存 token + 时间戳(持久化;30 天有效,25 天自动刷新) */ setAccessToken(token) { if (!token) return; try { GM_setValue('baidu_access_token', token); GM_setValue('baidu_access_token_at', Date.now()); } catch (e) { /* ignore */ } }, /** * 配置缓存(GM 存储):{ authorizeUrl, filemetasUrl, ua, at } —— client_id 会变,需动态刷新 */ getCachedConfig() { try { const c = GM_getValue('baidu_cfg', null); if (c && c.authorizeUrl) return c; } catch (e) { /* ignore */ } return null; }, /** GM_xmlhttpRequest 封装(跨域请求,@connect * 已放行) */ gmRequest(opts) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('无 GM_xmlhttpRequest')); return; } GM_xmlhttpRequest(Object.assign({ timeout: 30000, onload: (resp) => resolve(resp), onerror: () => reject(new Error('GM 请求网络错误')), ontimeout: () => reject(new Error('GM 请求超时')), }, opts)); }); }, /** * 上传一个临时文件(256KB 验证块)到当前账号网盘(precreate → locateupload → superfile2 → create)。 * 实测:superfile2 分片需 Content-Type: application/octet-stream(否则 403);create 的 block_list 用上传返回的真实 md5。 * @returns {fsid, path, md5} */ async uploadBlock(bytes, name) { const bdstoken = BAIDU.getBdstoken(); const path = '/' + name; const size = bytes.length; const fakeBlockList = JSON.stringify(['00000000000000000000000000000000']); // precreate(同源) const pc = await fetch(BAIDU.API_BASE + '/api/precreate?rtype=1&bdstoken=' + encodeURIComponent(bdstoken) + '&app_id=' + BAIDU.APP_ID + '&channel=' + BAIDU.CHANNEL + '&web=1&clienttype=0', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': location.origin + '/disk/main' }, body: new URLSearchParams({ path, autoinit: '1', size: String(size), block_list: fakeBlockList, target_path: '/' }).toString() }); const pcj = await pc.json(); if (!pcj.uploadid) throw new Error('precreate 失败(' + (pcj.errmsg || ('errno=' + pcj.errno)) + ')'); // locateupload(跨域 → GM) const luResp = await BAIDU.gmRequest({ method: 'GET', url: 'https://d.pcs.baidu.com/rest/2.0/pcs/file?method=locateupload&appid=' + BAIDU.APP_ID + '&path=' + encodeURIComponent(path) + '&uploadid=' + pcj.uploadid + '&upload_version=2.0', headers: { 'User-Agent': 'pan.baidu.com' }, }); let luj; try { luj = JSON.parse(luResp.responseText); } catch (e) { throw new Error('locateupload 解析失败'); } const server = (luj.servers && luj.servers[0] && luj.servers[0].server) || 'https://d.pcs.baidu.com/rest/2.0/pcs/superfile2'; // 上传分片(跨域 → GM;octet-stream raw) const upUrl = server + '/rest/2.0/pcs/superfile2?method=upload&app_id=' + BAIDU.APP_ID + '&path=' + encodeURIComponent(path) + '&uploadid=' + pcj.uploadid + '&type=tmpfile&partseq=0&partsize=' + size; const upResp = await BAIDU.gmRequest({ method: 'POST', url: upUrl, headers: { 'Content-Type': 'application/octet-stream', 'User-Agent': 'netdisk' }, data: bytes.buffer ? bytes : new Uint8Array(bytes).buffer, }); let upj; try { upj = JSON.parse(upResp.responseText); } catch (e) { throw new Error('上传解析失败'); } const realMd5 = upj.md5 || '00000000000000000000000000000000'; // create(同源;block_list = 真实 md5) const cr = await fetch(BAIDU.API_BASE + '/api/create?rtype=1&bdstoken=' + encodeURIComponent(bdstoken) + '&app_id=' + BAIDU.APP_ID + '&channel=' + BAIDU.CHANNEL + '&web=1&clienttype=0', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': location.origin + '/disk/main' }, body: new URLSearchParams({ path, size: String(size), uploadid: pcj.uploadid, block_list: JSON.stringify([realMd5]), target_path: '/' }).toString() }); const crj = await cr.json(); if (crj.errno !== 0) throw new Error('create 失败(' + JSON.stringify(crj).slice(0, 140) + ')'); // errno=0 但可能未返回 fsid:尝试 list 目录按 path 查(文件可能已创建) if (!crj.fsid) { try { const r2 = await BAIDU.listDir('/'); const hit = (r2.list || []).find(x => String(x.path) === path); if (hit) return { fsid: String(hit.fs_id), path, md5: crj.md5 || '' }; } catch (e) { /* ignore */ } throw new Error('create 成功但未返回 fsid(' + JSON.stringify(crj).slice(0, 140) + ')'); } return { fsid: String(crj.fsid), path, md5: crj.md5 }; }, /** * 创建分享链接(实测 POST /share/pset;fid_list 为数字数组;public=0 私密带提取码)。 * @returns {url, pwd, shareid, surl} */ async createShare(fsidList, passwd) { const bdstoken = BAIDU.getBdstoken(); const pwd = passwd || ('mc' + Math.random().toString(36).slice(2, 8)); const body = new URLSearchParams({ is_knowledge: '0', public: '0', period: '1', pwd, eflag_disable: 'true', auto_transfer: '0', linkOrQrcode: 'link', channel_list: '[]', schannel: '4', fid_list: JSON.stringify(fsidList.map(Number)), }).toString(); const r = await fetch(BAIDU.API_BASE + '/share/pset?channel=' + BAIDU.CHANNEL + '&bdstoken=' + encodeURIComponent(bdstoken) + '&clienttype=0&app_id=' + BAIDU.APP_ID + '&web=1', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': location.origin + '/disk/main' }, body }); const j = await r.json(); if (j.errno !== 0 || !j.link) throw new Error('创建分享失败(' + (j.errmsg || ('errno=' + j.errno)) + ')'); const surlMatch = String(j.link).match(/s\/([A-Za-z0-9_-]+)/); return { url: String(j.link), pwd, shareid: j.shareid, surl: surlMatch ? surlMatch[1] : '' }; }, /** 读分享访问密钥 sekey(打开分享页后由页面种入 BDCLND cookie) */ getSeKey() { try { const m = document.cookie.match(/(?:^|;\s*)BDCLND=([^;]+)/); return m ? m[1] : ''; } catch (e) { return ''; } }, /** * 转存分享文件到当前账号(实测 POST /share/transfer,sekey 来自 BDCLND)。 * @returns 转存响应(info/duplicated 含 fs_id) */ async transferFromShare(shareid, from, sekey, fsidList, targetPath) { const bdstoken = BAIDU.getBdstoken(); const r = await fetch(BAIDU.API_BASE + '/share/transfer?shareid=' + shareid + '&from=' + from + '&sekey=' + encodeURIComponent(sekey) + '&channel=' + BAIDU.CHANNEL + '&web=1&app_id=' + BAIDU.APP_ID + '&bdstoken=' + encodeURIComponent(bdstoken) + '&clienttype=0', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': location.origin + '/disk/main' }, body: new URLSearchParams({ fsidlist: JSON.stringify(fsidList.map(Number)), path: targetPath || '/' }).toString() }); return r.json(); }, /** 删除文件(块用完即弃;实测 /api/filemanager?method=delete) */ async deleteFiles(paths) { const bdstoken = BAIDU.getBdstoken(); const r = await fetch(BAIDU.API_BASE + '/api/filemanager?method=delete&bdstoken=' + encodeURIComponent(bdstoken) + '&app_id=' + BAIDU.APP_ID + '&channel=' + BAIDU.CHANNEL + '&web=1&clienttype=0', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': location.origin + '/disk/main' }, body: new URLSearchParams({ async: '0', filelist: JSON.stringify(paths || []) }).toString() }); return r.json(); }, // ============ 云块服务器(v0.9:令牌池按文件 md5 归并 + 块代理 + 滚雪球) ============ // 注意:全部用 GM_xmlhttpRequest(油猴扩展后台请求)——百度网盘为 HTTPS 页面, // 直接 fetch 到 http 云服务器会被浏览器「混合内容」策略阻止(Failed to fetch)。 /** 导出/持有者注册:把 {token, fsId} 凭据按文件 md5 归并入服务器池;返回 {ok, registered} */ async cloudRegister(server, token, files) { if (!server) return { ok: false, err: '未配置云服务器' }; try { const resp = await BAIDU.gmRequest({ method: 'POST', url: server.replace(/\/+$/, '') + '/v1/register', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ token, files: (files || []).map(f => ({ contentMd5: f.contentMd5, fsId: f.fsId, size: Number(f.size) || 0, path: String(f.path || '') })) }), }); return JSON.parse(resp.responseText || '{}'); } catch (e) { return { ok: false, err: e.message }; } }, /** 导入:从云服务器取验证块(服务器按 md5 查池、轮询凭据拉块;返回 Uint8Array 或 null) */ async cloudBlock(server, contentMd5, offset, size) { if (!server) return null; try { const resp = await BAIDU.gmRequest({ method: 'GET', url: server.replace(/\/+$/, '') + '/v1/block?contentMd5=' + encodeURIComponent(contentMd5) + '&offset=' + Number(offset) + '&size=' + Number(size || 262144), headers: { 'Accept': 'application/octet-stream' }, responseType: 'arraybuffer', }); if (resp.status < 200 || resp.status >= 300) return null; try { return new Uint8Array(resp.response); } catch (e) { return null; } } catch (e) { return null; } }, /** 导入成功滚雪球:把「当前账号网盘里该文件的 {token, fsId}」加入服务器池 */ async cloudPool(server, token, contentMd5, fsId) { if (!server) return; try { await BAIDU.gmRequest({ method: 'POST', url: server.replace(/\/+$/, '') + '/v1/pool', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ token, contentMd5, fsId: String(fsId || '') }), }); } catch (e) { /* ignore */ } }, /** 按路径查 fsId(秒传成功后定位新文件;用 target 方式 filemetas) */ async getFsidByPath(pathStr) { const bdstoken = BAIDU.getBdstoken(); const p = '/' + String(pathStr || '').replace(/^\/+/, ''); const qs = new URLSearchParams({ target: JSON.stringify([p]), dlink: '0', channel: BAIDU.CHANNEL, web: '1', app_id: BAIDU.APP_ID }); qs.set('bdstoken', bdstoken); const r = await fetch(BAIDU.API_BASE + '/api/filemetas?' + qs.toString(), { credentials: 'include', headers: { 'Referer': location.origin + '/disk/main' } }); const j = await r.json(); const it = (j.list || [])[0]; return it ? String(it.fs_id || '') : ''; }, /** * 动态拉取官方配置(照抄「网盘直链下载助手」:POST https://api.youxiaohou.com/config/v2 → base64 JSON, * 含 pcs[0]=filemetas 接口、pcs[3]=authorize URL(最新 client_id)、pcs[4]=login_success、ua)。 * 配置缓存 24h;失败时退回内置常量。 */ fetchBaiduConfig() { return new Promise((resolve) => { const cached = BAIDU.getCachedConfig(); if (typeof GM_xmlhttpRequest !== 'function') { resolve(cached); return; } GM_xmlhttpRequest({ method: 'POST', url: 'https://api.youxiaohou.com/config/v2', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: 'ver=6.2.7&a=eroslp', timeout: 20000, onload: (res) => { try { const dec = atob(String(res.responseText || '').trim()); const j = JSON.parse(dec); if (j && j.pcs && j.pcs['3'] && j.pcs['0']) { const cfg = { authorizeUrl: String(j.pcs['3']), filemetasUrl: String(j.pcs['0']), loginSuccessUrl: String(j.pcs['4'] || ''), ua: String(j.ua || 'pan.baidu.com'), at: Date.now() }; try { GM_setValue('baidu_cfg', cfg); } catch (e) {} resolve(cfg); return; } } catch (e) { /* ignore */ } resolve(BAIDU.getCachedConfig()); }, onerror: () => resolve(BAIDU.getCachedConfig()), ontimeout: () => resolve(BAIDU.getCachedConfig()), }); }); }, /** * 获取/自动获取百度下载凭证 access_token(已实测可 206 下载): * 1) 缓存命中直接用(30 天有效); * 2) 动态拉配置拿最新 client_id → 静默 OAuth:GM_xmlhttpRequest GET authorize * (confirm_login=0,已登录百度即 302 → login_success#access_token=xxx)→ 提取缓存; * 3) 失败降级:GM_openInTab 打开授权页由 oauthPageHook 收尾 / prompt 粘贴。 */ /** * 获取百度下载凭证 access_token(已实测可 206 下载): * 1) 缓存命中且 < 25 天 → 直接用(30 天有效,过期前自动刷新,参考直链助手 getToken 持久化逻辑); * 2) 无缓存或临近过期 → 静默 OAuth:GET authorize(confirm_login=0,已登录百度即 * 302 → login_success#access_token=xxx)→ 提取并持久化(含时间戳); * 3) 失败降级:返回旧 token(若有)。 */ ensureAccessToken() { const cached = BAIDU.getAccessTokenCached(); const at = BAIDU.getAccessTokenAt(); const TTL = 30 * 24 * 3600 * 1000; // 30 天 const REFRESH_BEFORE = 5 * 24 * 3600 * 1000; // 提前 5 天刷新 if (cached && at && (Date.now() - at) < (TTL - REFRESH_BEFORE)) { return Promise.resolve(cached); } if (typeof GM_xmlhttpRequest !== 'function') return Promise.resolve(cached || ''); return (async () => { // 动态配置(最新 client_id) let cfg = BAIDU.getCachedConfig(); if (!cfg || Date.now() - (cfg.at || 0) > 86400000) cfg = await BAIDU.fetchBaiduConfig(); const authorizeUrl = (cfg && cfg.authorizeUrl) || BAIDU.authorizeUrl; // 静默 OAuth 拿 token(已登录即 302 → access_token) return await new Promise((resolve) => { let done = false; const finish = (tok) => { if (!done) { done = true; resolve(tok || cached || ''); } }; GM_xmlhttpRequest({ method: 'GET', url: authorizeUrl, timeout: 25000, onload: (res) => { try { const fu = String(res.finalUrl || ''); const m = fu.match(/access_token=([^&]+)/); if (m && m[1]) { BAIDU.setAccessToken(m[1]); mcLog('token', '自动刷新成功(' + m[1].slice(0, 10) + '...)'); finish(m[1]); return; } } catch (e) { /* ignore */ } finish(BAIDU.getAccessTokenCached()); }, onerror: () => finish(''), ontimeout: () => finish(''), }); }); })(); }, /** * OAuth 页面自动处理(照抄插件的 initAuthorize): * - authorize 页:自动点击 #auth-allow 确认授权 * - login_success 页:从 URL 提取 access_token 存入 GM 并关闭标签 */ oauthPageHook() { if (!/openapi\.baidu\.com\/oauth/.test(location.href)) return; let tries = 0; const timer = setInterval(() => { try { if (/oauth\/2\.0\/authorize/.test(location.href)) { const btn = document.querySelector('#auth-allow'); if (btn) { btn.click(); return; } tries++; if (tries > 25) clearInterval(timer); } else if (/oauth\/2\.0\/login_success/.test(location.href)) { clearInterval(timer); if (location.href.includes('access_token')) { const m = location.href.match(/access_token=([^&]+)/); if (m && m[1]) { BAIDU.setAccessToken(m[1]); showToast('百度 access_token 已获取'); } } try { window.close(); } catch (e) {} } } catch (e) { /* ignore */ } }, 300); }, /** 同源 GET 请求(凭 cookie 鉴权,无需 token 头) */ async apiGet(path, params) { const bdstoken = BAIDU.getBdstoken(); if (!bdstoken) throw new Error('未能获取 bdstoken,请刷新百度网盘页面后重试'); const qs = new URLSearchParams(params || {}); qs.set('bdstoken', bdstoken); const url = BAIDU.API_BASE + path + '?' + qs.toString(); let resp; try { resp = await fetch(url, { credentials: 'include', headers: { 'Referer': location.origin + '/disk/main' } }); } catch (e) { throw new Error(`网络请求失败: ${e.message}`); } let j; try { j = await resp.json(); } catch (e) { throw new Error(`HTTP ${resp.status} 响应解析失败`); } if (j.errno !== undefined && j.errno !== 0) { const err = new Error(j.errmsg || j.errno_msg || `百度接口错误 errno=${j.errno}`); err.errno = j.errno; throw err; } return j; }, /** 列出目录(分页;实测 /api/list) */ async listDir(dir, page = 1) { const j = await BAIDU.apiGet('/api/list', { dir: String(dir == null || dir === '' ? '/' : dir), order: 'name', desc: '0', showempty: '0', web: '1', page: String(page), num: String(CONFIG.fileListPageSize), channel: BAIDU.CHANNEL, app_id: BAIDU.APP_ID, }); const list = Array.isArray(j.list) ? j.list : []; return { list, hasMore: list.length >= CONFIG.fileListPageSize }; }, /** 批量取元数据(fs_id[] → map;md5 已解密为 32 位 hex) */ async getFileMetas(fsIds) { const result = {}; const ids = (fsIds || []).map(String).filter(id => /^\d+$/.test(id)); if (!ids.length) return result; const batchSize = 100; for (let i = 0; i < ids.length; i += batchSize) { const batch = ids.slice(i, i + batchSize).map(Number); const j = await BAIDU.apiGet('/api/filemetas', { fsids: JSON.stringify(batch), dlink: '0', thumb: '0', extra: '0', needmedia: '0', detail: '1', channel: BAIDU.CHANNEL, web: '1', app_id: BAIDU.APP_ID, }); for (const it of (j.info || [])) { result[String(it.fs_id)] = { fsId: String(it.fs_id), name: String(it.server_filename || ''), path: String(it.path || ''), size: Number(it.size || 0), isdir: it.isdir == 1, // 实测 filemetas 的 isdir 为字符串 "1"/"0" md5: BAIDU.decryptMd5(it.md5 || ''), }; } } return result; }, /** 当前目录(实测 hash 形如 #/index?category=all&path=%2F02 → '/02';根为 '/') */ getCurrentDir() { try { const hash = location.hash || ''; const qi = hash.indexOf('?'); const sp = new URLSearchParams(qi >= 0 ? hash.slice(qi + 1) : ''); const p = sp.get('path'); if (p != null && String(p) !== '') { try { return decodeURIComponent(String(p)); } catch (e) { return String(p); } } } catch (e) { /* ignore */ } return '/'; }, /** 勾选读取:行 class 含 selected(data-id=fs_id)+ 表头全选状态 */ getCheckedItems() { const ids = []; const seen = new Set(); document.querySelectorAll('tr.mouse-choose-item.selected, tr.wp-s-pan-table__body-row.selected, [class*="table__body-row"].selected').forEach(row => { const id = row.getAttribute('data-id'); if (id && /^\d+$/.test(id) && !seen.has(id)) { seen.add(id); ids.push(id); } }); let headState = ''; const headCb = document.querySelector('thead input[type="checkbox"], [class*="head"] input[type="checkbox"], [class*="table__head"] input[type="checkbox"]'); if (headCb) { if (headCb.checked) headState = 'checked'; else { const aria = headCb.getAttribute('aria-checked'); if (aria === 'true') headState = 'checked'; else if (aria === 'mixed') headState = 'mixed'; } } return { ids, headState }; }, }; // ============================== 阿里云盘平台(v1.1:v3 列表 + v2 createWithFolders 快速上传) ============================== // 实测逆向(2026-10): // - 凭证:localStorage['token'] JSON → access_token(JWT Bearer) / refresh_token / default_drive_id / user_id // - 列表:POST api.aliyundrive.com/adrive/v3/file/list?jsonmask=...(含 content_hash) body{drive_id,parent_file_id,limit,...} // - 秒传:POST /adrive/v2/file/createWithFolders 带 content_hash(SHA1 大写)+size+proof_code → rapid_upload:true 一次完成 // - proof 算法(bundle.js 逆向):offset = BigInt("0x"+md5(access_token).slice(0,16)) % size; // proof = base64(文件[offset : offset+8]);服务器用请求者 token 算 offset 校验。JSON 不内嵌文件字节(保持轻量), // 导入者按自己 token 算 offset 后经夸克/UC 中转取 8 字节 proof 块(零服务器依赖) // - 建目录:同 createWithFolders type=folder;name 含 "/" 会自动创建缺失父目录(实测) // - 下载:POST /adrive/v2/file/get_download_url → url(OSS 签名直链,支持 Range) const ALIYUN = { API_BASE: 'https://api.aliyundrive.com', USER_API: 'https://user.aliyundrive.com', PAGE_STATE_KEY: '__ALYMS_PAGE_STATE__', _driveId: '', _driveIdAt: 0, _uk: '', /** 从 localStorage 读取 token JSON(阿里云盘网页版登录态) */ getToken() { try { const raw = localStorage.getItem('token'); if (!raw) return null; const j = JSON.parse(raw); if (!j || !j.access_token) return null; return { access_token: String(j.access_token), refresh_token: String(j.refresh_token || ''), default_drive_id: String(j.default_drive_id || ''), user_id: String(j.user_id || ''), }; } catch (e) { return null; } }, /** 获取当前资源盘 drive_id(v2/user/get 返回 resource_drive_id;缓存 1 小时) */ async getDriveId() { if (ALIYUN._driveId && Date.now() - ALIYUN._driveIdAt < 3600 * 1000) return ALIYUN._driveId; try { const j = await ALIYUN.userRequest('/v2/user/get', {}); const rid = j && (j.resource_drive_id || j.default_drive_id); if (rid) { ALIYUN._driveId = String(rid); ALIYUN._driveIdAt = Date.now(); return ALIYUN._driveId; } } catch (e) { /* ignore */ } const t = ALIYUN.getToken(); if (t && t.default_drive_id) { ALIYUN._driveId = t.default_drive_id; ALIYUN._driveIdAt = Date.now(); return t.default_drive_id; } return ''; }, /** user.aliyundrive.com 请求(POST JSON) */ async userRequest(path, body) { const t = ALIYUN.getToken(); if (!t) throw new Error('未检测到阿里云盘登录态,请刷新页面'); const resp = await fetch(ALIYUN.USER_API + path, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + t.access_token, 'x-canary': 'client=web,app=adrive,version=v6.8.12', 'Referer': location.origin + '/' }, body: JSON.stringify(body || {}), }); const j = await resp.json(); if (!resp.ok && j && j.code && j.message) throw new Error(j.message); return j; }, /** api.aliyundrive.com 请求(POST JSON;v3/v2 接口实测无需 x-signature) */ async request(path, body) { const t = ALIYUN.getToken(); if (!t) throw new Error('未检测到阿里云盘登录态,请刷新页面'); let resp; try { resp = await fetch(ALIYUN.API_BASE + path, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + t.access_token, 'x-canary': 'client=web,app=adrive,version=v6.8.12', 'Referer': location.origin + '/' }, body: JSON.stringify(body || {}), }); } catch (e) { throw new Error(`网络请求失败: ${e.message}`); } let j; try { j = await resp.json(); } catch (e) { throw new Error(`HTTP ${resp.status} 响应解析失败`); } if (resp.status < 200 || resp.status >= 300) { if (j && j.code && j.message) throw new Error(j.message); throw new Error(`HTTP ${resp.status}`); } return j; }, /** 文件列表(v3;jsonmask 含 content_hash/size/file_id) */ async getFileList(driveId, parentFileId, marker) { const fields = 'name,file_id,drive_id,type,size,created_at,updated_at,category,file_extension,parent_file_id,mime_type,starred,thumbnail,url,content_hash,user_tags,user_meta,trashed,sync_meta'; const jsonmask = encodeURIComponent('next_marker,items(' + fields + ')'); const body = { drive_id: driveId, parent_file_id: String(parentFileId || 'root'), limit: 100, marker: marker || '', order_by: 'updated_at', order_direction: 'DESC', fields: '*' }; const j = await ALIYUN.request('/adrive/v3/file/list?jsonmask=' + jsonmask, body); return { list: Array.isArray(j.items) ? j.items : [], nextMarker: j.next_marker || '' }; }, /** 创建文件夹;name 含 "/" 自动创建缺失父目录(实测 createWithFolders 支持) */ async createFolders(driveId, parentFileId, name) { const j = await ALIYUN.request('/adrive/v2/file/createWithFolders', { drive_id: driveId, parent_file_id: String(parentFileId || 'root'), name: String(name), type: 'folder', check_name_mode: 'auto_rename', }); const id = j && j.file_id; if (!id) throw new Error('创建文件夹未返回 ID'); return String(id); }, /** 快速上传(秒传):返回 {rapid, fileId, name, errno?} */ async rapidUpload(driveId, parentFileId, name, size, sha1, proofCode) { const j = await ALIYUN.request('/adrive/v2/file/createWithFolders', { drive_id: driveId, part_info_list: [{ part_number: 1 }], parent_file_id: String(parentFileId || 'root'), name: String(name), type: 'file', check_name_mode: 'auto_rename', size: Number(size || 0), create_scene: 'file_upload', device_name: '', content_hash: String(sha1 || '').toUpperCase(), content_hash_name: 'sha1', proof_code: String(proofCode || ''), proof_version: 'v1', }); return { rapid: !!(j && j.rapid_upload), fileId: j && j.file_id ? String(j.file_id) : '', name: j && j.file_name ? String(j.file_name) : String(name) }; }, /** 获取下载直链(OSS 签名 URL,支持 Range) */ async getDownloadUrl(driveId, fileId) { const j = await ALIYUN.request('/adrive/v2/file/get_download_url', { drive_id: driveId, file_id: String(fileId) }); const url = j && (j.url || j.encrypt_url || ''); if (!url) throw new Error('未获取到下载直链'); return String(url); }, /** 通过直链 Range 下载片段(跨域 → GM_xmlhttpRequest;@connect *.aliyundrive.net 已放行) */ downloadRange(downloadUrl, offset, length) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { // 兜底:页面同域 fetch(若直链同域可直连) fetch(downloadUrl, { headers: { 'Range': `bytes=${offset}-${offset + length - 1}` } }) .then(r => { if (r.status >= 200 && r.status < 300) return r.arrayBuffer(); throw new Error('HTTP ' + r.status); }) .then(buf => resolve(new Uint8Array(buf))) .catch(e => reject(e)); return; } GM_xmlhttpRequest({ method: 'GET', url: downloadUrl, headers: { 'Range': `bytes=${offset}-${offset + length - 1}` }, responseType: 'arraybuffer', timeout: 60000, onload: (resp) => { if (resp.status >= 200 && resp.status < 300) { try { resolve(new Uint8Array(resp.response)); } catch (e) { resolve(new Uint8Array(resp.responseText.split('').map(c => c.charCodeAt(0) & 0xFF))); } } else { reject(new Error(`下载片段失败 HTTP ${resp.status}`)); } }, onerror: () => reject(new Error('下载片段网络错误')), ontimeout: () => reject(new Error('下载片段超时')), }); }); }, /** 完整下载文件字节(小文件嵌入用,v1.1 起不内嵌数据,此方法保留备用:大文件 proof 块直接 Range 取 8 字节) */ async downloadProofRange(driveId, fileId, offset) { const url = await ALIYUN.getDownloadUrl(driveId, fileId); const bytes = await ALIYUN.downloadRange(url, offset, 8); return bytes; }, /** * proof 偏移计算(bundle.js 逆向):offset = BigInt("0x"+md5(access_token).slice(0,16)) % size * md5 用标准字符串 MD5(复用 BAIDU.md5Hex,blueimp 实现,ASCII 正确)。 */ computeProofOffset(accessToken, size) { const n = Number(size) || 0; if (n <= 0 || !accessToken) return 0; const hash = BAIDU.md5Hex(String(accessToken)); const hex16 = hash.slice(0, 16); const big = BigInt('0x' + hex16); return Number(big % BigInt(n)); }, /** 由字节生成 proof(8 字节 base64;不足 8 字节取实际长度) */ proofFromBytes(bytes) { const len = Math.min(8, bytes.length); let bin = ''; for (let i = 0; i < len; i++) bin += String.fromCharCode(bytes[i] & 0xFF); return btoa(bin); }, /** 当前目录 parent_file_id:URL /drive/file/all → 'root';/drive/file/{id} → id */ getCurrentParentId() { try { // 阿里 URL:/drive/file/all(根) 或 /drive/file/all/{folderId}(文件夹内) // 取路径中最后一个非 all 的段作为当前目录;无则 root const m = location.pathname.match(/\/drive\/file\/([^?#]+)/); if (m && m[1]) { const segs = m[1].split('/').filter(Boolean); for (let i = segs.length - 1; i >= 0; i--) { const s = segs[i]; if (s && s !== 'all') return decodeURIComponent(s); } } return 'root'; } catch (e) { /* ignore */ } return 'root'; }, /** 规范化 v3 列表条目(rawFile 展开) */ normalizeListItem(it) { const raw = (it && it.rawFile) || it || {}; return { fileId: String(raw.file_id || it.fileId || ''), driveId: String(raw.drive_id || it.driveId || ''), name: String(raw.name || it.name || ''), type: String(raw.type || it.type || 'file'), size: Number(raw.size != null ? raw.size : (it.size || 0)), contentHash: String(raw.content_hash || it.contentHash || '').toUpperCase(), parentFileId: String(raw.parent_file_id || it.parentFileId || ''), }; }, /** 读取 React fiber dataSource(当前目录完整条目列表,含 fileId/contentHash) * 支持卡片视图(node-card 系列)与列表视图(tr 系列行元素)。 * 安全策略:优先主列表容器;校验条目 parent 与当前目录一致,避免匹配到「最近/全盘」缓存表格。 */ getDataSource() { const viewport = window.innerHeight; // 优先候选:主列表容器(表格/卡片列表的外层),再退回行/卡片元素 const preferred = Array.from(document.querySelectorAll('[class*="node-list-table-view"], [class*="node-list"], [class*="table-view"], [class*="list-view"], [class*="node-card-list"]')).filter((el) => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0 && r.top >= 0 && r.top < viewport; }); const fallback = Array.from(document.querySelectorAll('[class*="node-card-container"], [class*="node-card"], [class*="tr--"], [class*="tr-"], [class*="row--"], [class*="item--"]')).filter((el) => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0 && r.top >= 0 && r.top < viewport; }); const cands = preferred.length ? preferred : fallback; const seen = new Set(); const curParent = (() => { try { return ALIYUN.getCurrentParentId(); } catch (e) { return ''; } })(); for (const el of cands) { const fk = Object.keys(el).find(k => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$')); if (!fk) continue; let node = el[fk]; for (let i = 0; i < 50 && node; i++, node = node.return) { const mp = node.memoizedProps || node.pendingProps || {}; if (mp && (Array.isArray(mp.dataSource) || Array.isArray(mp.list)) && (mp.dataSource || mp.list).length) { const list = Array.isArray(mp.dataSource) ? mp.dataSource : mp.list; // 校验:条目 parent_file_id 应与当前目录一致(root 时 parent 为空或 'root') if (curParent) { const firstParent = String((list[0] && (list[0].parentFileId || (list[0].rawFile && list[0].rawFile.parent_file_id))) || ''); if (firstParent && firstParent !== 'root' && firstParent !== curParent) { // 当前目录是 root 且条目属于某文件夹 → 不是本目录数据源,跳过 if (curParent === 'root') continue; // 条目父目录与当前目录不一致 → 是别的列表缓存,跳过 continue; } } const key = list.length + '|' + (list[0] && (list[0].name || list[0].file_name) || ''); if (seen.has(key)) break; seen.add(key); // selectedKeys:阿里可能是数组或逗号分隔字符串(直链助手 split(',') 证实);兼容两种 let sk = mp.selectedKeys; if (sk != null && typeof sk === 'string') sk = sk.split(','); if (sk == null) sk = mp.selectedRowKeys; if (sk != null && typeof sk === 'string') sk = sk.split(','); return { dataSource: list, selectedKeys: Array.isArray(sk) ? sk : null }; } } } // 再兜底:不限 parent 校验(个别情况条目不带 parent 字段) const seen2 = new Set(); for (const el of fallback) { const fk = Object.keys(el).find(k => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$')); if (!fk) continue; let node = el[fk]; for (let i = 0; i < 50 && node; i++, node = node.return) { const mp = node.memoizedProps || node.pendingProps || {}; if (mp && (Array.isArray(mp.dataSource) || Array.isArray(mp.list)) && (mp.dataSource || mp.list).length) { const list = Array.isArray(mp.dataSource) ? mp.dataSource : mp.list; const key = list.length + '|' + (list[0] && (list[0].name || list[0].file_name) || ''); if (seen2.has(key)) break; seen2.add(key); let sk = mp.selectedKeys; if (sk != null && typeof sk === 'string') sk = sk.split(','); if (sk == null) sk = mp.selectedRowKeys; if (sk != null && typeof sk === 'string') sk = sk.split(','); return { dataSource: list, selectedKeys: Array.isArray(sk) ? sk : null }; } } } return null; }, /** 勾选读取:优先 React 表格原生 selectedKeys(列表/卡片视图通用 + data-is-selected DOM), * 全选时返回当前目录全部条目 */ /** 勾选读取:优先 React fiber selectedKeys(与直链助手一致,列表/卡片通用), * DOM data-is-selected 严格限定在列表行/卡片容器内补充(避免跨容器误收导致全选误判) */ getCheckedItems() { const got = ALIYUN.getDataSource(); const source = got ? got.dataSource : null; // selectedKeys:fiber 下可能是数组,也可能因 React 优化缓存为字符串数组;统一转 String 集合 const rawKeys = got ? got.selectedKeys : null; const keySet = new Set(); if (Array.isArray(rawKeys)) { for (const k of rawKeys) keySet.add(String(k)); } // DOM 勾选:仅当前列表行/卡片容器内的 data-is-selected=true(title 匹配) const rowSelTitles = new Set(); const rowSelFids = new Set(); document.querySelectorAll('[data-is-selected="true"]').forEach(el => { const row = el.closest('[class*="tr--"], [class*="node-card-container"], [class*="node-card"], [class*="row--"], [class*="item--"]'); if (!row) return; // 不在列表行/卡片内 → 忽略(防止误收树/侧栏等) const titleEl = el.closest('[title]'); if (titleEl) { const t = String(titleEl.getAttribute('title') || '').trim(); if (t) rowSelTitles.add(t); } // 行 DOM 上可能直接挂 fid 相关属性(部分视图) const fidAttr = row.getAttribute('data-file-id') || row.getAttribute('data-fid') || row.getAttribute('data-id') || ''; if (fidAttr) rowSelFids.add(String(fidAttr)); }); const items = []; if (Array.isArray(source)) { for (const it of source) { const name = String(it.name || '').trim(); const fid = String(it.fileId || (it.rawFile && it.rawFile.file_id) || ''); const isDir = String(it.type) === 'folder' || String(it.type) === 'backupfolder'; // 选中判定(严格,绝不误全选):DOM 行 title/fid → fiber selectedKeys 精确匹配 let selected = rowSelTitles.has(name) || (fid && rowSelFids.has(fid)); if (!selected && keySet.size && fid) { selected = keySet.has(fid); // 兼容 selectedKeys 存的是 name 的形式 if (!selected) selected = keySet.has(name); } if (selected) { items.push({ fileId: fid, driveId: String(it.driveId || (it.rawFile && it.rawFile.drive_id) || ''), name, type: isDir ? 'folder' : 'file', size: Number(it.size || 0), contentHash: String(it.contentHash || (it.rawFile && it.rawFile.content_hash) || '').toUpperCase(), parentFileId: String(it.parentFileId || (it.rawFile && it.rawFile.parent_file_id) || ''), }); } } } // 兜底:无 fiber 时从 DOM 卡片 title 反查(限定列表行内) if (!items.length && rowSelTitles.size) { for (const t of rowSelTitles) { const card = Array.from(document.querySelectorAll('[class*="tr--"] [title], [class*="node-card-container"] [title], [class*="node-card"] [title]')).find(c => String(c.getAttribute('title') || '').trim() === t); items.push({ name: t, fileId: card ? String(card.closest('[data-file-id]') ? card.closest('[data-file-id]').getAttribute('data-file-id') : '') : '', driveId: '', type: 'file', size: 0, contentHash: '', parentFileId: '' }); } } // 全选判定:只信任「表头全选框真实 DOM 状态」(列表表头区域), // 绝不按 keySet.size >= source.length 猜测(虚拟滚动可见行数≠目录总数,勾选 1 个会误判全选导致拉全盘) let headAll = false; try { const headCb = Array.from(document.querySelectorAll('[class*="checkbox-container"], [class*="checkbox"]')).find((cb) => { const r = cb.getBoundingClientRect(); const within = (r.top >= 0 && r.top < 180) && r.width > 0; if (!within) return false; // 表头全选框:位于列表表格/卡片工具栏上方(排除行内 checkbox 需 top < 行首位置) const row = cb.closest('[class*="tr--"], [class*="node-card-container"], [class*="node-card"]'); return !row; // 不在行内 → 表头/工具栏的全选框 }); if (headCb) { const inner = headCb.querySelector('input[type=checkbox]') || headCb; headAll = inner.checked === true || inner.getAttribute('aria-checked') === 'true' || inner.getAttribute('data-checked') === 'true'; } } catch (e) { /* ignore */ } // 全选兜底:若表头不可判定,但 selectedKeys 精确覆盖了 source 全部条目且行 DOM 也全部标记选中 → 视为全选 let keysCoverAll = false; if (Array.isArray(source) && source.length && keySet.size) { keysCoverAll = source.every(it => { const fid = String(it.fileId || (it.rawFile && it.rawFile.file_id) || ''); const name = String(it.name || ''); return (fid && keySet.has(fid)) || keySet.has(name); }); } return { all: headAll || keysCoverAll, items }; }, }; // ============================== 夸克网盘平台(v1.2:阿里 PDS 体系,sha1+md5 秒传) ============================== // 实测逆向(2026-10,与 UserScript.txt / 光鸭云盘批量助手 参照交叉验证): // - 列表:GET drive-pc.quark.cn/1/clouddrive/file/sort?pr=ucpro&fr=pc&pdir_fid=&_page=&_size=... // 条目含 fid/file_name/size/file/dir(无 hash,hash 需另接口取) // - 取 hash:POST /1/clouddrive/file/download body {fids:[...]} → data[] 含 md5(32hex)+sha1(40hex),均为明文 // - 秒传:POST /1/clouddrive/file/upload/pre body {pdir_fid,file_name,size,sha1,md5,format_type,l_updated_at,l_created_at} // 响应 data.finish=true + upload_id="rapid-..." 即一次完成(PDS 全局去重,跨账号可秒) // - 建目录:POST /1/clouddrive/file body {pdir_fid,file_name,dir_path,dir_init_lock} → data.fid // - 登录态:夸克页面带 cookie(页面同源 fetch + credentials:include 即可),无需 token const QUARK = { API_BASE: 'https://drive-pc.quark.cn', PAGE_STATE_KEY: '__QKMS_PAGE_STATE__', /** 页面请求(同源 + cookie) */ async request(path, options = {}) { const url = QUARK.API_BASE + path + (path.includes('?') ? '&' : '?') + 'pr=ucpro&fr=pc&uc_param_str='; const headers = Object.assign({ 'Referer': location.origin + '/', 'Origin': location.origin, 'Content-Type': 'application/json' }, options.headers || {}); let resp; try { resp = await fetch(url, Object.assign({ method: 'GET', credentials: 'include', headers }, options)); } catch (e) { throw new Error(`网络请求失败: ${e.message}`); } let j; try { j = await resp.json(); } catch (e) { throw new Error(`HTTP ${resp.status} 响应解析失败`); } if (resp.status < 200 || resp.status >= 300 || (j && j.status === 401) || (j && j.code === 31001)) { if (j && j.message) throw new Error(j.message); throw new Error(`HTTP ${resp.status}`); } if (j && j.code !== undefined && j.code !== 0 && j.code !== 200) { throw new Error(j.message || `接口错误 code=${j.code}`); } return j; }, /** 文件列表(分页) */ async getFileList(parentFid, page = 1, size = 100) { const qs = `pdir_fid=${encodeURIComponent(String(parentFid == null || parentFid === '' ? '0' : parentFid))}&_page=${page}&_size=${size}&_fetch_total=1&_fetch_sub_dirs=0&_sort=file_type:asc,updated_at:desc&fetch_all_file=1&fetch_risk_file_name=1`; const j = await QUARK.request('/1/clouddrive/file/sort?' + qs); const list = (j.data && Array.isArray(j.data.list)) ? j.data.list : []; const total = (j.data && j.data._total) || 0; return { list, total, hasMore: list.length >= size && (page * size) < total }; }, /** 批量取文件 hash(download 接口,明文 md5+sha1;分批 15 防限流) */ async getFileHashes(fids) { if (!Array.isArray(fids) || !fids.length) return {}; const map = {}; const batchSize = 15; for (let i = 0; i < fids.length; i += batchSize) { const batch = fids.slice(i, i + batchSize); let j; try { j = await QUARK.request('/1/clouddrive/file/download', { method: 'POST', body: JSON.stringify({ fids: batch }) }); } catch (e) { // 单个失败则跳过,不阻塞整体 continue; } for (const it of (Array.isArray(j.data) ? j.data : [])) { if (!it || !it.fid) continue; map[String(it.fid)] = { md5: String(it.md5 || '').toLowerCase(), sha1: String(it.sha1 || '').toLowerCase(), size: Number(it.size || 0), }; } await sleep(800); } return map; }, /** * 下载文件流式计算真实 SHA1(跨账号秒传的全局键;download 接口不返回 sha1)。 * 分块 Range 下载 + 流式 SHA1,内存占用恒定;onProgress(percent) 进度回调。 */ async getFileSha1ByDownload(fid, size, onProgress) { try { const j = await QUARK.request('/1/clouddrive/file/download', { method: 'POST', body: JSON.stringify({ fids: [fid] }) }); const d = (Array.isArray(j.data) ? j.data : []).find(it => String(it.fid) === String(fid)); const url = d && (d.download_url || d.url); if (!url) return ''; const total = Number(size) || Number(d && d.size) || 0; if (total <= 0) return ''; const CHUNK = 8 * 1024 * 1024; // 8MB/块,减少请求数 const sha = new Sha1Stream(); let offset = 0; while (offset < total) { const end = Math.min(offset + CHUNK - 1, total - 1); let buf; try { buf = await QUARK.downloadRangeBytes(url, offset, end - offset + 1); } catch (e) { return ''; } if (!buf || !buf.length) return ''; sha.update(buf); offset = end + 1; if (onProgress) onProgress(Math.min(100, Math.round(offset / total * 100))); await sleep(30); } return sha.hex(); } catch (e) { return ''; } }, /** Range 下载返回 Uint8Array(夸克直链,同源 fetch + credentials) */ async downloadRangeBytes(url, offset, length) { const resp = await fetch(String(url), { method: 'GET', credentials: 'include', headers: { 'Range': `bytes=${offset}-${offset + length - 1}`, 'Referer': location.origin + '/', 'Origin': location.origin }, }); if (resp.status < 200 || resp.status >= 300) throw new Error('Range 下载失败 HTTP ' + resp.status); const ab = await resp.arrayBuffer(); return new Uint8Array(ab); }, /** 秒传(upload/pre 带 sha1+md5;命中返回 fid,未命中需实际上传) */ async rapidUpload(parentFid, name, size, sha1, md5, formatType) { const MIME_MAP = { txt: 'text/plain', md: 'text/markdown', pdf: 'application/pdf', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', mp4: 'video/mp4', mkv: 'video/x-matroska', avi: 'video/x-msvideo', mov: 'video/quicktime', mp3: 'audio/mpeg', m4a: 'audio/mp4', flac: 'audio/flac', wav: 'audio/wav', zip: 'application/zip', rar: 'application/x-rar-compressed', '7z': 'application/x-7z-compressed', tar: 'application/x-tar', gz: 'application/gzip', torrent: 'application/x-bittorrent', msi: 'application/x-msdownload', exe: 'application/x-msdownload', doc: 'application/msword', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', xls: 'application/vnd.ms-excel', xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ppt: 'application/vnd.ms-powerpoint', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', }; const ext = String(name || '').split('.').pop().toLowerCase(); const resolvedFormat = String(formatType || '') || MIME_MAP[ext] || 'application/octet-stream'; const body = { pdir_fid: String(parentFid == null || parentFid === '' ? '0' : parentFid), dir_name: '', size: Number(size || 0), file_name: String(name || ''), format_type: resolvedFormat, l_updated_at: Date.now(), l_created_at: Date.now(), ccp_hash_update: true, parallel_upload: true, }; if (sha1) body.sha1 = String(sha1).toLowerCase(); if (md5) body.md5 = String(md5).toLowerCase(); let j; try { j = await QUARK.request('/1/clouddrive/file/upload/pre', { method: 'POST', body: JSON.stringify(body) }); } catch (e) { return { ok: false, rapid: false, err: e.message }; } const d = j.data || {}; if (d.finish === true || String(d.upload_id || '').startsWith('rapid')) { return { ok: true, rapid: true, fid: String(d.fid || ''), uploadId: String(d.upload_id || '') }; } return { ok: false, rapid: false, taskId: String(d.task_id || ''), err: '未命中快速上传(flash)' }; }, /** 创建文件夹;若同名目录已存在(code 23008 同名冲突),列出父目录找到已有 fid 复用 */ async createDir(parentFid, name) { let j; try { j = await QUARK.request('/1/clouddrive/file', { method: 'POST', body: JSON.stringify({ pdir_fid: String(parentFid == null || parentFid === '' ? '0' : parentFid), file_name: String(name), dir_path: '', dir_init_lock: false }), }); } catch (e) { // 同名冲突:列出父目录,找到同名目录的 fid if (e && /同名冲突|23008|doloading/i.test(String(e.message || ''))) { const fid = await QUARK.findDirFidByName(parentFid, name); if (fid) return fid; } throw e; } const fid = (j.data && j.data.fid) || ''; if (!fid) throw new Error('创建文件夹未返回 fid'); return String(fid); }, /** 在目录下按名称查找子目录 fid(分页扫描) */ async findDirFidByName(parentFid, name) { let page = 1; for (;;) { let r; try { r = await QUARK.getFileList(parentFid, page, 100); } catch (e) { return ''; } for (const it of (r.list || [])) { if (it.dir === true && String(it.file_name || '') === String(name)) { return String(it.fid || ''); } } if (!r.hasMore) break; page++; await sleep(CONFIG.listPageDelay); } return ''; }, /** 当前目录 pdir_fid:URL query dir_fid(夸克 hash 路由 #/list/{type}?dir_fid=xxx),缺省 0=根 */ getCurrentParentId() { try { const sp = new URLSearchParams(location.search); const dirFid = sp.get('dir_fid'); if (dirFid != null && String(dirFid) !== '' && String(dirFid) !== '0') return String(dirFid); } catch (e) { /* ignore */ } // hash 路由也可能带 dir_fid try { const hash = location.hash || ''; const qi = hash.indexOf('?'); if (qi >= 0) { const sp = new URLSearchParams(hash.slice(qi + 1)); const dirFid = sp.get('dir_fid'); if (dirFid != null && String(dirFid) !== '' && String(dirFid) !== '0') return String(dirFid); } } catch (e) { /* ignore */ } return '0'; }, /** 勾选读取:优先夸克 Redux store(store.getState().file.{section}.list + selectedRowKeys), * 回退页面全局 API / React fiber / DOM 勾选 */ getCheckedItems() { const items = []; const seen = new Set(); const pushItem = (fid, name, isDir, size) => { const key = String(fid || name || ''); if (!key || seen.has(key)) return; seen.add(key); items.push({ fid: String(fid || ''), name: String(name || ''), isDir: isDir === true, size: Number(size || 0) }); }; // 1) Redux store(最可靠) try { const uw = (typeof unsafeWindow !== 'undefined' && unsafeWindow) || window; if (uw.store && typeof uw.store.getState === 'function') { const st = uw.store.getState(); const f = st && st.file; if (f) { // 当前列表 section:根据 URL 判断(all/img/video/doc/music/bt/recent/myshare 等) const m = location.hash.match(/\/list\/([^/?#]+)/); const section = m && m[1] ? String(m[1]) : String(f.listType || 'all'); const sec = f[section] || f.all || {}; const list = Array.isArray(sec.list) ? sec.list : []; const selected = Array.isArray(sec.selectedRowKeys) ? sec.selectedRowKeys.map(k => String(k)) : []; if (selected.length) { // 先按选中 keys 匹配 list for (const it of list) { if (!it || !it.fid) continue; if (selected.includes(String(it.fid))) { pushItem(it.fid, it.file_name, it.dir === true, it.size); } } // 补:选中但不在当前 list(如全选后的虚拟列表外)无法取信息,仅记录 fid return { all: false, items }; } // 未选中但有 selectedRowKeys 说明只读当前;无选中则返回空 } } } catch (e) { /* ignore */ } // 2) 页面全局 API try { const uw = (typeof unsafeWindow !== 'undefined' && unsafeWindow) || window; const isShare = /^\/(s|share)\//.test(location.pathname); const apiList = isShare ? (uw.shareUser && uw.shareUser.getSelectedFileList && uw.shareUser.getSelectedFileList()) : (uw.file && uw.file.getSelectedFileList && uw.file.getSelectedFileList()); if (Array.isArray(apiList) && apiList.length) { for (const it of apiList) pushItem(it.fid, it.file_name || it.name, it.dir === true, it.size); return { all: false, items }; } } catch (e) { /* ignore */ } // 3) React fiber 回退 const candidates = [document.querySelector('.file-list'), document.querySelector('[class*="file-list"]'), document.querySelector('table')]; for (const dom of candidates) { if (!dom || items.length) continue; const fk = Object.keys(dom).find(k => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$')); if (!fk) continue; let node = dom[fk]; for (let i = 0; i < 40 && node; i++, node = node.return) { const mp = node.memoizedProps || node.pendingProps; if (mp && Array.isArray(mp.list) && mp.selectedRowKeys !== undefined) { const list = mp.list; const selected = mp.selectedRowKeys || []; for (const it of list) { if (!it || !it.fid) continue; const fid = String(it.fid); if (selected.map(k => String(k)).includes(fid)) { pushItem(fid, it.file_name, it.dir === true, it.size); } } if (items.length) return { all: false, items }; } } } // 4) 兜底:DOM 行勾选 if (!items.length) { const selTitles = new Set(); document.querySelectorAll('input[type=checkbox]:checked, [role=checkbox][aria-checked=true]').forEach(cb => { const row = cb.closest('tr') || cb.closest('[class*="row"]') || cb.closest('[class*="item"]'); if (row) { const nameEl = row.querySelector('[class*="name"], [class*="title"], [class*="file_name"]'); const nm = nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : ''; if (nm) selTitles.add(nm); } }); if (selTitles.size) { for (const nm of selTitles) items.push({ fid: '', name: nm, isDir: false, size: 0 }); } } return { all: false, items }; }, }; // ============================== UC 网盘平台(v1.3:与夸克同源 PDS,base64 md5) ============================== // 实测逆向(2026-10,与夸克同源 pc-api.uc.cn): // - 列表:GET pc-api.uc.cn/1/clouddrive/file/sort?pr=UCBrowser&fr=pc&pdir_fid=&_page=&_size=... // - 取 hash:POST /1/clouddrive/file/download body {fids:[...]} → data[] 含 md5(**base64 编码**,非 hex) // - 秒传:POST /1/clouddrive/file/upload/pre body {pdir_fid,file_name,size,md5,format_type,...} // md5 必须是 base64(实测 hex md5 不命中,base64 命中 finish:true);小文件 md5 即可(大文件需 sha1,同夸克) // - 建目录:POST /1/clouddrive/file body {pdir_fid,file_name,...} → data.fid(同名冲突 23008 复用已有) // - 勾选:UC 无 Redux store,走 React fiber list + selectedRowKeys(与夸克回退同源) const UC = { API_BASE: 'https://pc-api.uc.cn', PR: 'UCBrowser', /** 页面请求(同源 + cookie) */ async request(path, options = {}) { const sep = path.includes('?') ? '&' : '?'; const url = UC.API_BASE + path + sep + 'pr=' + UC.PR + '&fr=pc'; const headers = Object.assign({ 'Referer': location.origin + '/', 'Origin': location.origin, 'Content-Type': 'application/json' }, options.headers || {}); let resp; try { resp = await fetch(url, Object.assign({ method: 'GET', credentials: 'include', headers }, options)); } catch (e) { throw new Error(`网络请求失败: ${e.message}`); } let j; try { j = await resp.json(); } catch (e) { throw new Error(`HTTP ${resp.status} 响应解析失败`); } if (resp.status < 200 || resp.status >= 300 || (j && j.status === 401) || (j && j.code === 31001)) { if (j && j.message) throw new Error(j.message); throw new Error(`HTTP ${resp.status}`); } if (j && j.code !== undefined && j.code !== 0 && j.code !== 200) { throw new Error(j.message || `接口错误 code=${j.code}`); } return j; }, /** 文件列表(分页) */ async getFileList(parentFid, page = 1, size = 100) { const qs = `pdir_fid=${encodeURIComponent(String(parentFid == null || parentFid === '' ? '0' : parentFid))}&_page=${page}&_size=${size}&_fetch_total=1&_fetch_sub_dirs=0&_sort=file_type:asc,updated_at:desc&fetch_all_file=1&fetch_risk_file_name=1`; const j = await UC.request('/1/clouddrive/file/sort?' + qs); const list = (j.data && Array.isArray(j.data.list)) ? j.data.list : []; const total = (j.data && j.data._total) || 0; return { list, total, hasMore: list.length >= size && (page * size) < total }; }, /** 批量取 hash(download 接口;md5 为 base64 原样保留,不做 hex 转换) */ async getFileHashes(fids) { if (!Array.isArray(fids) || !fids.length) return {}; const map = {}; const batchSize = 15; for (let i = 0; i < fids.length; i += batchSize) { const batch = fids.slice(i, i + batchSize); let j; try { j = await UC.request('/1/clouddrive/file/download', { method: 'POST', body: JSON.stringify({ fids: batch }) }); } catch (e) { continue; } for (const it of (Array.isArray(j.data) ? j.data : [])) { if (!it || !it.fid) continue; map[String(it.fid)] = { md5: String(it.md5 || ''), // base64 md5 sha1: String(it.sha1 || '').toLowerCase(), size: Number(it.size || 0), }; } await sleep(800); } return map; }, /** * 下载文件流式计算真实 SHA1(跨账号秒传的全局键;UC download 接口同样不返回 sha1)。 * 分块 Range 下载 + 流式 SHA1,内存占用恒定;onProgress(percent) 进度回调。 */ async getFileSha1ByDownload(fid, size, onProgress) { try { const j = await UC.request('/1/clouddrive/file/download', { method: 'POST', body: JSON.stringify({ fids: [fid] }) }); const d = (Array.isArray(j.data) ? j.data : []).find(it => String(it.fid) === String(fid)); const url = d && (d.download_url || d.url); if (!url) return ''; const total = Number(size) || Number(d && d.size) || 0; if (total <= 0) return ''; const CHUNK = 8 * 1024 * 1024; // 8MB/块,减少请求数 const sha = new Sha1Stream(); let offset = 0; while (offset < total) { const end = Math.min(offset + CHUNK - 1, total - 1); let buf; try { buf = await UC.downloadRangeBytes(url, offset, end - offset + 1); } catch (e) { return ''; } if (!buf || !buf.length) return ''; sha.update(buf); offset = end + 1; if (onProgress) onProgress(Math.min(100, Math.round(offset / total * 100))); await sleep(30); } return sha.hex(); } catch (e) { return ''; } }, /** Range 下载返回 Uint8Array(UC 直链,同源 fetch + credentials) */ async downloadRangeBytes(url, offset, length) { const resp = await fetch(String(url), { method: 'GET', credentials: 'include', headers: { 'Range': `bytes=${offset}-${offset + length - 1}`, 'Referer': location.origin + '/', 'Origin': location.origin }, }); if (resp.status < 200 || resp.status >= 300) throw new Error('Range 下载失败 HTTP ' + resp.status); const ab = await resp.arrayBuffer(); return new Uint8Array(ab); }, /** 秒传(upload/pre,md5 用 base64;命中返回 fid) */ async rapidUpload(parentFid, name, size, md5, formatType) { const MIME_MAP = { txt: 'text/plain', md: 'text/markdown', pdf: 'application/pdf', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', mp4: 'video/mp4', mkv: 'video/x-matroska', avi: 'video/x-msvideo', mov: 'video/quicktime', mp3: 'audio/mpeg', m4a: 'audio/mp4', flac: 'audio/flac', wav: 'audio/wav', zip: 'application/zip', rar: 'application/x-rar-compressed', '7z': 'application/x-7z-compressed', tar: 'application/x-tar', gz: 'application/gzip', torrent: 'application/x-bittorrent', msi: 'application/x-msdownload', exe: 'application/x-msdownload', dmg: 'application/x-apple-diskimage', doc: 'application/msword', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', xls: 'application/vnd.ms-excel', xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ppt: 'application/vnd.ms-powerpoint', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', }; const ext = String(name || '').split('.').pop().toLowerCase(); const resolvedFormat = String(formatType || '') || MIME_MAP[ext] || 'application/octet-stream'; const body = { pdir_fid: String(parentFid == null || parentFid === '' ? '0' : parentFid), dir_name: '', size: Number(size || 0), file_name: String(name || ''), format_type: resolvedFormat, l_updated_at: Date.now(), l_created_at: Date.now(), ccp_hash_update: true, parallel_upload: true, }; if (md5) body.md5 = String(md5); let j; try { j = await UC.request('/1/clouddrive/file/upload/pre', { method: 'POST', body: JSON.stringify(body) }); } catch (e) { return { ok: false, rapid: false, err: e.message }; } const d = j.data || {}; if (d.finish === true || String(d.upload_id || '').startsWith('rapid')) { return { ok: true, rapid: true, fid: String(d.fid || ''), uploadId: String(d.upload_id || '') }; } return { ok: false, rapid: false, taskId: String(d.task_id || ''), err: '未命中快速上传(flash)' }; }, /** 秒传(带 sha1,大文件必需;md5 可选,hex 会被转成 base64) */ async rapidUploadWithSha1(parentFid, name, size, sha1, md5, formatType) { const MIME_MAP = { txt: 'text/plain', md: 'text/markdown', pdf: 'application/pdf', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', mp4: 'video/mp4', mkv: 'video/x-matroska', avi: 'video/x-msvideo', mov: 'video/quicktime', mp3: 'audio/mpeg', m4a: 'audio/mp4', flac: 'audio/flac', wav: 'audio/wav', zip: 'application/zip', rar: 'application/x-rar-compressed', '7z': 'application/x-7z-compressed', tar: 'application/x-tar', gz: 'application/gzip', torrent: 'application/x-bittorrent', msi: 'application/x-msdownload', exe: 'application/x-msdownload', dmg: 'application/x-apple-diskimage', doc: 'application/msword', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', xls: 'application/vnd.ms-excel', xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ppt: 'application/vnd.ms-powerpoint', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', }; const ext = String(name || '').split('.').pop().toLowerCase(); const resolvedFormat = String(formatType || '') || MIME_MAP[ext] || 'application/octet-stream'; const body = { pdir_fid: String(parentFid == null || parentFid === '' ? '0' : parentFid), dir_name: '', size: Number(size || 0), file_name: String(name || ''), format_type: resolvedFormat, l_updated_at: Date.now(), l_created_at: Date.now(), ccp_hash_update: true, parallel_upload: true, }; if (sha1 && /^[0-9a-f]{40}$/i.test(String(sha1))) body.sha1 = String(sha1).toLowerCase(); if (md5) { // 兼容 hex → base64 const m = String(md5); if (/^[0-9a-f]{32}$/i.test(m)) { const bytes = new Uint8Array(16); for (let i = 0; i < 16; i++) bytes[i] = parseInt(m.substr(i * 2, 2), 16); body.md5 = btoa(String.fromCharCode.apply(null, bytes)); } else { body.md5 = m; } } let j; try { j = await UC.request('/1/clouddrive/file/upload/pre', { method: 'POST', body: JSON.stringify(body) }); } catch (e) { return { ok: false, rapid: false, err: e.message }; } const d = j.data || {}; if (d.finish === true || String(d.upload_id || '').startsWith('rapid')) { return { ok: true, rapid: true, fid: String(d.fid || ''), uploadId: String(d.upload_id || '') }; } return { ok: false, rapid: false, taskId: String(d.task_id || ''), err: '未命中快速上传(flash)' }; }, /** 创建文件夹;同名冲突(23008)时查找已有目录复用 */ async createDir(parentFid, name) { let j; try { j = await UC.request('/1/clouddrive/file', { method: 'POST', body: JSON.stringify({ pdir_fid: String(parentFid == null || parentFid === '' ? '0' : parentFid), file_name: String(name), dir_path: '', dir_init_lock: false }), }); } catch (e) { if (e && /同名冲突|23008|doloading/i.test(String(e.message || ''))) { const fid = await UC.findDirFidByName(parentFid, name); if (fid) return fid; } throw e; } const fid = (j.data && j.data.fid) || ''; if (!fid) throw new Error('创建文件夹未返回 fid'); return String(fid); }, /** 在目录下按名称查找子目录 fid */ async findDirFidByName(parentFid, name) { let page = 1; for (;;) { let r; try { r = await UC.getFileList(parentFid, page, 100); } catch (e) { return ''; } for (const it of (r.list || [])) { if (it.dir === true && String(it.file_name || '') === String(name)) { return String(it.fid || ''); } } if (!r.hasMore) break; page++; await sleep(CONFIG.listPageDelay); } return ''; }, /** 当前目录 pdir_fid(URL/hash 的 dir_fid,缺省 0=根) */ getCurrentParentId() { try { const sp = new URLSearchParams(location.search); const dirFid = sp.get('dir_fid'); if (dirFid != null && String(dirFid) !== '' && String(dirFid) !== '0') return String(dirFid); } catch (e) { /* ignore */ } try { const hash = location.hash || ''; const qi = hash.indexOf('?'); if (qi >= 0) { const sp = new URLSearchParams(hash.slice(qi + 1)); const dirFid = sp.get('dir_fid'); if (dirFid != null && String(dirFid) !== '' && String(dirFid) !== '0') return String(dirFid); } } catch (e) { /* ignore */ } return '0'; }, /** 勾选读取:UC 无 Redux store → React fiber list + selectedRowKeys(与夸克回退同源) */ getCheckedItems() { const items = []; const seen = new Set(); const candidates = [document.querySelector('.file-list, [class*="file-list"]'), document.querySelector('table')]; for (const dom of candidates) { if (!dom) continue; const fk = Object.keys(dom).find(k => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$')); if (!fk) continue; let node = dom[fk]; for (let i = 0; i < 40 && node; i++, node = node.return) { const mp = node.memoizedProps || node.pendingProps; if (mp && Array.isArray(mp.list) && mp.selectedRowKeys !== undefined) { const list = mp.list; const selected = mp.selectedRowKeys || []; for (const it of list) { if (!it || !it.fid) continue; const fid = String(it.fid); if (selected.map(k => String(k)).includes(fid)) { const key = fid; if (!seen.has(key)) { seen.add(key); items.push({ fid, name: String(it.file_name || ''), isDir: it.dir === true, size: Number(it.size || 0) }); } } } if (items.length) return { all: false, items }; } } } // 兜底:DOM 行勾选 if (!items.length) { const selTitles = new Set(); document.querySelectorAll('input[type=checkbox]:checked, [role=checkbox][aria-checked=true]').forEach(cb => { const row = cb.closest('tr') || cb.closest('[class*="row"]') || cb.closest('[class*="item"]'); if (row) { const nameEl = row.querySelector('[class*="name"], [class*="title"], [class*="file_name"]'); const nm = nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : ''; if (nm) selTitles.add(nm); } }); if (selTitles.size) { for (const nm of selTitles) items.push({ fid: '', name: nm, isDir: false, size: 0 }); } } return { all: false, items }; }, }; // ============================== 腾讯微云平台(v1.4:SHA1 分块秒传,已实测 2026-10) ============================== // 实测逆向结论(页面同源 fetch + cookie 即可,无需手动 token): // - 凭证:g_tk=wyctoken cookie;qq_openid=weiyun_qq_openid cookie;token_info 从 cookie 派生 // - 列表:POST /webapp/json/weiyunQdisk/DiskDirBatchList (cmd 2209) // body:{req_header:<串>, req_body:<串>}(req_header/req_body 均为 JSON 字符串,实测必需) // dir_list:[{dir_key, get_type:0, start, count, sort_field:2, reverse_order:false, get_abstract_url, get_dir_detail_info}] // 响应 data.rsp_body.RspMsg_body.dir_list[0]={dir_list:[...], file_list:[...], total_*, finish_flag} // 文件条目含 file_sha(完整 SHA1) / file_id / file_size / filename;目录含 dir_key // - 建目录:/webapp/json/weiyunQdiskClient/DiskDirCreate (cmd 2614) // {ppdir_key, pdir_key, dir_name, file_exist_option:2, create_type:1} → dir_key // - 下载直链:/webapp/json/weiyunQdiskClient/DiskFileBatchDownload (cmd 2402) // {file_list:[{ppdir_key,pdir_key,file_id}], download_type:0} → download_url+cookie_name+cookie_value // - 秒传预检:POST https://www.weiyun.com/api/v3/ftn_pre_upload?g_tk=..&cmd=247120 // body:{req_header:{cmd:247120,appid:30013,major_version:3,minor_version:0,fix_version:0,version:3,user_flag:0}, // req_body:{ReqMsg_body:{weiyun.PreUploadMsgReq_body:{ // common_upload_req:{ppdir_key,pdir_key,file_size,filename,file_exist_option,use_mutil_channel:true}, // upload_scr:0, channel_count:4, block_size:1048576, check_sha, check_data, // block_info_list:[{sha,offset,size}]}}}} // 响应 result.rsp_body.RspMsg_body.weiyunPreUploadMsgRsp_body:file_exist:true=命中(新文件已秒传入库, // 返回 common_upload_rsp.file_id);false=未命中(创建了空壳条目,需 DiskDirFileBatchDeleteEx 删除) // - 删除:/webapp/json/weiyunQdiskClient/DiskDirFileBatchDeleteEx (cmd 2509) file_list/dir_list // - 目录路径:/webapp/json/weiyunFileLibClient/LibDirPathGet (cmd 26150) dir_key → items[] 最后一项 pdir_key=父目录 // - 用户信息:/webapp/json/weiyunQdiskClient/DiskUserInfoGet (cmd 2201) → main_dir_key/root_dir_key // - 秒传分块算法(alist/weiyun-sdk-go 同款): // 按 1MB 分段流式 SHA1(Go sha1 上下文,中间状态 = 5×uint32 小端字节序 hex 40 位); // block_info_list[i].sha=截至该段的中间状态;check_sha=读到"最后一块-校验块大小"处的中间状态; // check_data=文件末尾 ≤128 字节 base64;最后一块 sha=完整文件 SHA1。服务器校验继续写 check_data 后 == 最后块 sha。 // lastBlockSize=size%1MB(0→1MB);checkBlockSize=lastBlockSize%128(0→128) const WEIYUN = { API_BASE: 'https://www.weiyun.com', PAGE_STATE_KEY: '__WYMS_PAGE_STATE__', _mainDirKey: '', _rootDirKey: '', _pathCache: new Map(), // dirKey → { pdirKey, name, path } _cachedList: null, // 最近一次列表 { dirKey, dirs, files } cookie(name) { try { const m = document.cookie.match(new RegExp('(?:^|;\\s*)' + String(name).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=([^;]+)')); return m ? decodeURIComponent(m[1]) : ''; } catch (e) { return ''; } }, /** token_info 派生(参照 weiyun-sdk-go ParseTokenInfo) */ tokenInfo() { const wyUf = WEIYUN.cookie('wy_uf'); const qqOpenid = WEIYUN.cookie('weiyun_qq_openid'); const wxOpenid = WEIYUN.cookie('weiyun_wx_openid'); const openid = WEIYUN.cookie('openid'); if (wyUf === '2' && (qqOpenid || wxOpenid)) { return { tokenInfo: { token_type: 3, login_key_type: 1540 }, qqOpenid }; } if (wyUf === '1' && openid) { return { tokenInfo: { token_type: 1, openid, open_appid: WEIYUN.cookie('wy_appid'), access_token: WEIYUN.cookie('access_token'), login_key_type: 192, login_key_value: WEIYUN.cookie('access_token') }, qqOpenid: '' }; } return { tokenInfo: { token_type: 0, login_key_type: 27, login_key_value: WEIYUN.cookie('p_skey') || '', openid: '' }, qqOpenid: '' }; }, /** 普通接口请求(webapp/json 协议) */ async request(protocol, name, cmd, body, { rnd = true } = {}) { const gt = WEIYUN.cookie('wyctoken'); if (!gt) throw new Error('未获取到微云 wyctoken(登录态 cookie),请刷新微云页面后重试'); const tk = WEIYUN.tokenInfo(); const seq = Math.floor(Date.now() / 1000); const reqHeader = JSON.stringify({ seq, type: 1, cmd, appid: 30013, version: 3, major_version: 3, minor_version: 3, fix_version: 3, qq_openid: tk.qqOpenid || '', user_flag: 0, env_id: '', }); const reqBody = JSON.stringify({ ReqMsg_body: { ext_req_head: { token_info: tk.tokenInfo, language_info: { language_type: 2052 } }, ['.weiyun.' + name + 'MsgReq_body']: body || {}, }, }); const qs = '?g_tk=' + encodeURIComponent(gt) + '&cmd=' + cmd + (rnd ? '&r=' + Math.random() : ''); const url = WEIYUN.API_BASE + '/webapp/json/' + protocol + '/' + name + qs; let resp; try { resp = await fetch(url, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', 'Referer': location.origin + '/' }, body: JSON.stringify({ req_header: reqHeader, req_body: reqBody }), }); } catch (e) { throw new Error('微云网络请求失败: ' + e.message); } if (resp.status < 200 || resp.status >= 300) { throw new Error('微云 HTTP ' + resp.status + '(' + name + ')'); } let j; try { j = await resp.json(); } catch (e) { throw new Error('微云 HTTP ' + resp.status + ' 响应解析失败'); } const core = (j && (j.data || j.result)) || null; if (!core) throw new Error('微云接口无返回数据(ret=' + j.ret + ' msg=' + (j.msg || '') + ')'); const rh = core.rsp_header || {}; if (rh.retcode !== undefined && rh.retcode !== 0) { const err = new Error('微云接口错误 ' + rh.retcode + '(' + (rh.retmsg || '') + ')cmd=' + name); err.retcode = rh.retcode; throw err; } const rsp = core.rsp_body || {}; return rsp.RspMsg_body != null ? rsp.RspMsg_body : rsp; }, /** 响应里取 RspMsg_body(webapp 接口最终数据) */ async getUserInfo() { const body = await WEIYUN.request('weiyunQdiskClient', 'DiskUserInfoGet', 2201, { is_get_upload_flow_flag: false, is_get_high_speed_flow_info: false, is_get_weiyun_flag: false, is_get_space_clean_info: false, is_get_user_reward_info: false, }); if (body && body.main_dir_key) { WEIYUN._mainDirKey = String(body.main_dir_key); } if (body && body.root_dir_key) { WEIYUN._rootDirKey = String(body.root_dir_key); } return body || {}; }, async getMainDirKey() { if (!WEIYUN._mainDirKey) await WEIYUN.getUserInfo(); return WEIYUN._mainDirKey; }, async getRootDirKey() { if (!WEIYUN._rootDirKey) await WEIYUN.getUserInfo(); return WEIYUN._rootDirKey; }, /** 当前目录 dir_key:URL /disk → main;/disk/folder/{key} → key */ getCurrentDirKey() { try { const m = location.pathname.match(/\/disk\/folder\/([^/?#]+)/); if (m && m[1]) return decodeURIComponent(m[1]); } catch (e) { /* ignore */ } return ''; // 空 = 根(main),由调用方 getMainDirKey 解析 }, /** 目录完整路径(LibDirPathGet):返回 items[],最后一项 pdir_key=父目录;带缓存 */ async getDirPath(dirKey) { if (WEIYUN._pathCache.has(dirKey)) return WEIYUN._pathCache.get(dirKey); let items = []; try { const body = await WEIYUN.request('weiyunFileLibClient', 'LibDirPathGet', 26150, { dir_key: String(dirKey) }); items = Array.isArray(body.items) ? body.items : []; } catch (e) { items = []; } const folded = { dirKey: String(dirKey), pdirKey: items.length ? String(items[items.length - 1].pdir_key || '') : '', name: items.length ? String(items[items.length - 1].dir_name || '') : '', path: items.map(x => String(x.dir_name || '')).filter(Boolean), }; WEIYUN._pathCache.set(dirKey, folded); return folded; }, /** 目标目录的 ppdir/pdir:父目录 key 经 LibDirPathGet 解析(PreUpload 实测 ppdir 不可空) */ async getPdirPair(dirKey) { const resolved = await WEIYUN.resolveDirKey(dirKey); const p = await WEIYUN.getDirPath(resolved); let ppdir = p.pdirKey; if (!ppdir) { ppdir = await WEIYUN.getRootDirKey(); } return { ppdir_key: ppdir, pdir_key: resolved }; }, /** 目录 key 规范化:空/根 → main_dir_key */ async resolveDirKey(dirKey) { if (dirKey && String(dirKey) !== '' && String(dirKey) !== '0' && String(dirKey) !== 'root') return String(dirKey); return await WEIYUN.getMainDirKey(); }, /** 列表(分页:start 偏移从头拉) */ async getFileList(dirKey, start = 0, count = 500) { const dk = await WEIYUN.resolveDirKey(dirKey); const body = await WEIYUN.request('weiyunQdisk', 'DiskDirBatchList', 2209, { pdir_key: dk, dir_list: [{ dir_key: dk, get_type: 0, start, count, sort_field: 2, reverse_order: false, get_abstract_url: true, get_dir_detail_info: true, }], }); const first = Array.isArray(body.dir_list) ? body.dir_list[0] : null; const dirs = (first && Array.isArray(first.dir_list) ? first.dir_list : []).map(d => ({ kind: 'dir', dirKey: String(d.dir_key || ''), name: String(d.dir_name || ''), ctime: Number(d.dir_ctime || 0), mtime: Number(d.dir_mtime || 0), })); const files = (first && Array.isArray(first.file_list) ? first.file_list : []).map(f => ({ kind: 'file', fileId: String(f.file_id || ''), name: String(f.filename || ''), size: Number(f.file_size || f.file_cursize || 0), sha1: String(f.file_sha || '').toLowerCase(), md5: String(f.file_md5 || '').toLowerCase(), mtime: Number(f.file_mtime || 0), })); return { dirKey: dk, dirs, files, finishFlag: !!(first && first.finish_flag), totalDir: Number(first && first.total_dir_count) || 0, totalFile: Number(first && first.total_file_count) || 0, }; }, /** 建目录(同名已存在会加后缀,实测 file_exist_option:2 即可) */ async createDir(parentDirKey, dirName) { const pair = await WEIYUN.getPdirPair(parentDirKey); const body = await WEIYUN.request('weiyunQdiskClient', 'DiskDirCreate', 2614, { ppdir_key: pair.ppdir_key, pdir_key: pair.pdir_key, dir_name: String(dirName), file_exist_option: 2, create_type: 1, }); const key = body && (body.dir_key || body.dirKey); if (!key) throw new Error('微云创建目录未返回 dir_key'); return String(key); }, /** 下载直链(DiskFileBatchDownload) */ async getDownloadUrl(pdirKey, fileId) { const p = await WEIYUN.getDirPath(await WEIYUN.resolveDirKey(pdirKey)); const body = await WEIYUN.request('weiyunQdiskClient', 'DiskFileBatchDownload', 2402, { file_list: [{ ppdir_key: p.pdirKey || '', pdir_key: await WEIYUN.resolveDirKey(pdirKey), file_id: String(fileId) }], download_type: 0, }); const list = body && Array.isArray(body.file_list) ? body.file_list : []; const it = list[0] || {}; return { url: String(it.https_download_url || it.download_url || ''), cookieName: String(it.cookie_name || ''), cookieValue: String(it.cookie_value || ''), }; }, /** Range 下载片段(直链跨域 → GM_xmlhttpRequest;带 FTN5K cookie) */ downloadRange(dl, offset, length) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('无 GM_xmlhttpRequest,无法下载')); return; } const headers = { 'Range': `bytes=${offset}-${offset + length - 1}`, 'User-Agent': navigator.userAgent, 'Referer': 'https://www.weiyun.com/' }; if (dl.cookieName && dl.cookieValue) headers['Cookie'] = dl.cookieName + '=' + dl.cookieValue; GM_xmlhttpRequest({ method: 'GET', url: dl.url, headers, responseType: 'arraybuffer', timeout: 120000, onload: (resp) => { if (resp.status >= 200 && resp.status < 300) { try { resolve(new Uint8Array(resp.response)); } catch (e) { resolve(new Uint8Array(resp.responseText.split('').map(c => c.charCodeAt(0) & 0xFF))); } } else { reject(new Error('下载片段失败 HTTP ' + resp.status)); } }, onerror: () => reject(new Error('下载片段网络错误')), ontimeout: () => reject(new Error('下载片段超时')), }); }); }, /** * 计算微云秒传分块信息(下载整个文件流式计算,内存恒定,适合大文件): * @param {string} pdirKey 文件所在目录(可为当前目录/根) * @param {object} file {fileId, size} * @param {(pct:number)=>void} onProgress * @returns {{blockInfoList, checkSha, checkData, fileHash, size}} 或 null(失败) */ async computeBlockInfo(pdirKey, file, onProgress) { try { const size = Number(file.size || 0); if (size <= 0) return null; const dl = await WEIYUN.getDownloadUrl(pdirKey, file.fileId); if (!dl.url) return null; const blockSize = 1048576; const lastBlockSize = (size % blockSize) || blockSize; let checkBlockSize = lastBlockSize % 128; if (checkBlockSize === 0) checkBlockSize = 128; const beforeBlockSize = size - lastBlockSize; const sha = new Sha1Stream(); const stateHex = () => sha.stateHex ? sha.stateHex() : ''; const blockInfoList = []; let offset = 0; // before 块:每 1MB 取中间状态 while (offset < beforeBlockSize) { const len = Math.min(blockSize, beforeBlockSize - offset); const buf = await WEIYUN.downloadRange(dl, offset, len); if (!buf || !buf.length) return null; sha.update(buf); blockInfoList.push({ sha: stateHex(), offset, size: len }); offset += len; if (onProgress) onProgress(Math.min(99, Math.round(offset / size * 100))); } // between:读到最后一块-checkBlockSize 处 const midLen = lastBlockSize - checkBlockSize; if (midLen > 0) { const buf = await WEIYUN.downloadRange(dl, offset, midLen); if (!buf || !buf.length) return null; sha.update(buf); offset += midLen; } const checkSha = stateHex(); // after:末尾 ≤128 字节(TeeReader 同时喂给 hash → 最后一块 sha=完整 SHA1) const tailBuf = await WEIYUN.downloadRange(dl, offset, checkBlockSize); if (!tailBuf || !tailBuf.length) return null; sha.update(tailBuf); const fileHash = sha.hex(); let bin = ''; for (let i = 0; i < tailBuf.length; i += 0x8000) bin += String.fromCharCode.apply(null, tailBuf.subarray(i, i + 0x8000)); const checkData = btoa(bin); blockInfoList.push({ sha: fileHash, offset: beforeBlockSize, size: lastBlockSize }); return { blockInfoList, checkSha, checkData, fileHash, size }; } catch (e) { mcLog('weiyun', '分块计算失败: ' + e.message); return null; } }, /** * 秒传预检(PreUpload):给定分块信息提交,命中返回 {hit:true, fileId},未命中返回 {hit:false, fileId(空壳)}。 * file_exist_option: 1=覆盖 2=重命名 */ async preUpload(pair, name, size, blockSize, checkSha, checkData, blockInfoList, fileExistOption = 2) { const gt = WEIYUN.cookie('wyctoken'); if (!gt) throw new Error('未获取到微云 wyctoken,请刷新页面'); const body = { req_header: { cmd: 247120, appid: 30013, major_version: 3, minor_version: 0, fix_version: 0, version: 3, user_flag: 0 }, req_body: { ReqMsg_body: { 'weiyun.PreUploadMsgReq_body': { common_upload_req: { ppdir_key: pair.ppdir_key, pdir_key: pair.pdir_key, file_size: Number(size || 0), filename: String(name), file_exist_option: Number(fileExistOption), use_mutil_channel: true, }, upload_scr: 0, channel_count: 4, block_size: Number(blockSize || 1048576), check_sha: String(checkSha || ''), check_data: String(checkData || ''), block_info_list: Array.isArray(blockInfoList) ? blockInfoList : [], }, }, }, }; let resp; try { resp = await fetch(WEIYUN.API_BASE + '/api/v3/ftn_pre_upload?g_tk=' + encodeURIComponent(gt) + '&cmd=247120', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', 'Referer': location.origin + '/' }, body: JSON.stringify(body), }); } catch (e) { throw new Error('微云秒传网络失败: ' + e.message); } if (resp.status < 200 || resp.status >= 300) { throw new Error('微云秒传 HTTP ' + resp.status); } let j; try { j = await resp.json(); } catch (e) { throw new Error('微云秒传响应解析失败 HTTP ' + resp.status); } const core = j && (j.result || j.data); const rh = core && core.rsp_header || {}; const pre = core && core.rsp_body && core.rsp_body.RspMsg_body && core.rsp_body.RspMsg_body.weiyunPreUploadMsgRsp_body || {}; if (rh.retcode !== undefined && rh.retcode !== 0) { throw new Error('微云秒传预检失败 ' + rh.retcode + '(' + (rh.retmsg || '') + ')'); } const crsp = pre.common_upload_rsp || {}; return { hit: pre.file_exist === true, fileId: String(crsp.file_id || ''), filename: String(crsp.filename || name), }; }, /** 删除文件/目录(导入未命中时清理空壳条目) */ async deleteFiles(files, dirs) { files = (files || []).filter(f => f && f.file_id); dirs = (dirs || []).filter(d => d && d.dir_key); if (!files.length && !dirs.length) return; try { await WEIYUN.request('weiyunQdiskClient', 'DiskDirFileBatchDeleteEx', 2509, { file_list: files.map(f => ({ ppdir_key: f.ppdir_key || '', pdir_key: f.pdir_key || '', file_id: f.file_id, filename: f.filename || '' })), dir_list: dirs.map(d => ({ ppdir_key: d.ppdir_key || '', pdir_key: d.pdir_key || '', dir_key: d.dir_key, dir_name: d.dir_name || '' })), }); } catch (e) { mcLog('weiyun', '清理秒传空壳失败: ' + e.message); } }, // ============ 页面勾选读取(Vue:fileNode._selected 是真实勾选状态) ============ // 微云前端是 Vue2(disk.js 实测):li 的 staticClass 恒含 "checked"(假状态!), // 真正勾选状态是行实例 __vue__.$props.fileNode._selected(DOM 上映射为 li.act)。 // fileNode 直接携带 _id(file_id)/_name/_file_sha/_file_size/_is_dir/_pdir_key 等全部元数据。 /** 读取勾选的名字集合(兼容 DOM act class + Vue _selected) */ getCheckedNames() { const lis = Array.from(document.querySelectorAll('li.list-group-item')); const selected = lis.filter(li => li.classList.contains('act')); const names = selected.map(li => { const a = li.querySelector('.item-tit a, a.tit'); return a ? a.textContent.replace(/\s+/g, ' ').trim() : ''; }).filter(Boolean); return { names, all: lis.length > 0 && selected.length === lis.length }; }, /** * 勾选条目:优先读 li 行 Vue fileNode(_selected=true 的项,直接带全部元数据); * 兜底用 getCheckedNames 的名字去当前目录 API 列表匹配。 * 返回 { all, items:[{name, kind, fileId, dirKey, size, sha1, md5, pdirKey}] } */ async getCheckedItems() { // 方式 1:Vue fileNode(推荐,零 API 依赖、含 pdir_key) const vueItems = []; const lis = Array.from(document.querySelectorAll('li.list-group-item')); let vueSelCount = 0; let vueTotal = 0; for (const li of lis) { const v = li.__vue__; const fn = v && v.$props && v.$props.fileNode; if (!fn) continue; vueTotal++; if (fn._selected === true) { vueSelCount++; const isDir = String(fn._is_dir) === 'true'; vueItems.push(isDir ? { name: String(fn._name || ''), kind: 'dir', fileId: '', dirKey: String(fn._id || ''), size: 0, sha1: '', md5: '', pdirKey: fn._pdir_key ? String(fn._pdir_key) : '' } : { name: String(fn._name || ''), kind: 'file', fileId: String(fn._id || ''), dirKey: '', size: Number(fn._file_size || 0), sha1: String(fn._file_sha || '').toLowerCase(), md5: String(fn._file_md5 || '').toLowerCase(), pdirKey: fn._pdir_key ? String(fn._pdir_key) : '' }); } } if (vueTotal > 0) { const all = vueSelCount === vueTotal; if (all) { // 全选:页面可能只渲染可见行(虚拟列表),必须拉 API 全量列表 const dk = WEIYUN.getCurrentDirKey(); try { const full = await WEIYUN.getFileList(dk, 0, 500); if (full.files.length || full.dirs.length) { const items = []; for (const d of full.dirs) items.push({ name: d.name, kind: 'dir', fileId: '', dirKey: d.dirKey, size: 0, sha1: '', md5: '', pdirKey: dk }); for (const f of full.files) items.push({ name: f.name, kind: 'file', fileId: f.fileId, dirKey: '', size: f.size, sha1: f.sha1, md5: f.md5, pdirKey: dk }); return { all: true, items }; } } catch (e) { /* 拉取失败则退回可见行 */ } return { all: true, items: vueItems }; } return { all: false, items: vueItems }; } // 方式 2(兜底):DOM act class 名字 → API 匹配 const { names, all } = WEIYUN.getCheckedNames(); if (!names.length && !all) return { all: false, items: [] }; const dk = WEIYUN.getCurrentDirKey(); let list = null; try { list = await WEIYUN.getFileList(dk, 0, 500); } catch (e) { /* ignore */ } const items = []; const seen = new Set(); const push = (it) => { const key = String(it.fileId || it.dirKey || it.name); if (!key || seen.has(key)) return; seen.add(key); items.push(it); }; const pick = (name) => { if (!name) return; if (list) { const f = list.files.find(x => x.name === name); if (f) { push({ name: f.name, kind: 'file', fileId: f.fileId, dirKey: '', size: f.size, sha1: f.sha1, md5: f.md5, pdirKey: dk }); return; } const d = list.dirs.find(x => x.name === name); if (d) { push({ name: d.name, kind: 'dir', fileId: '', dirKey: d.dirKey, size: 0, sha1: '', pdirKey: dk }); return; } } push({ name, kind: 'file', fileId: '', dirKey: '', size: 0, sha1: '', pdirKey: dk }); }; if (all && list) { for (const d of list.dirs) push({ name: d.name, kind: 'dir', fileId: '', dirKey: d.dirKey, size: 0, sha1: '', pdirKey: dk }); for (const f of list.files) push({ name: f.name, kind: 'file', fileId: f.fileId, dirKey: '', size: f.size, sha1: f.sha1, md5: f.md5, pdirKey: dk }); return { all: true, items }; } for (const n of names) pick(n); return { all: false, items }; }, /** 当前目录 parent(统一接口返回 getLastParentId) */ async getLastParentId() { const dk = WEIYUN.getCurrentDirKey(); return await WEIYUN.resolveDirKey(dk); }, }; // ============================== 小飞机网盘平台(v1.5:feejii.com,元数据拉 md5 + getUpToken 秒传) ============================== // 实测逆向(2026-10,feejii.com 前端 bundle 逐行分析 + API 真机验证): // - API 基址 https://api.feijipan.com(feejii.com 控制台也调它);公共参数 uuid/devType=6/devCode/devModel=chrome/ // devVersion=156/appVersion=''/timestamp=AES加密(毫秒)/extra=2/appToken // - AES:key='dingHao-disk-app',AES-128-ECB/PKCS7 输出 hex ciphertext(webpack 模块 86686;已用抓包 timestamp 密文验证) // - 登录态:localStorage['disk-pc-vuex'] → common{appToken, uuid, accountInfo.userId, browserInfo} // - 列表 GET /app/record/file/list {folderId, offset, limit, type:0} → list[](fileType 1=文件 2=目录;不含哈希) // - 建目录 POST /app/file/folder/save {folderId(父), folderName, folderDesc} → list[0].id // - 秒传 POST /app/vod/getUpToken {fileName, fileType:1, userId, md5, pathKey:'', fileSize(KB整数), folderId, fileId:''} // upToken === '-1'(字符串)→ 命中:文件已秒传入库(map.fileId);否则返回 COS 上传凭证(未命中) // - 删除 POST /app/file/delete {fileIds, folderIds, status:-1}(-1=彻底删除) // - 下载 GET /ws/file/redirect {enable:1, downloadId=AES(fileId|userId), auth=AES(fileId|毫秒)},**必须带 Referer** // 页面 URL(否则返回"参数有误")→ 302 → CDN(Tencent COS,dl-*.feejii.com,支持 Range 206) // - 服务器 md5:CDN 响应头 x-amz-meta-callback = base64(JSON{callbackMode, callbackBody: JSON{...md5...}}) // 即文件上传时服务器记录的 md5(=小飞机去重池的键,实测用它能秒传命中)。导出零下载: // Range bytes=0-1 极小请求读响应头即可(浏览器 fetch 读不到该自定义头,须用 GM_xmlhttpRequest) // - 勾选:.files 组件 __vue__.$data {checkList(勾选数组), checkAll, list(当前目录完整条目)} // - 存储键为 *.gz 命名但内容为原始字节(实测下载内容与原名一致) const FEEJII = { API_BASE: 'https://api.feijipan.com', AES_KEY: 'dingHao-disk-app', PAGE_STATE_KEY: '__FJMS_PAGE_STATE__', // ---- AES-128-ECB/PKCS7 → hex(紧凑实现,已与抓包密文逐一比对通过)---- aesEcbHex(plain) { const SBOX = [0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16]; const RCON = [0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36]; const mul2 = x => ((x << 1) ^ (((x >> 7) & 1) * 0x1b)) & 0xff; const encBlock = (input, w) => { let s = input.slice(), r; for (let x = 0; x < 16; x++) s[x] ^= w[x]; for (r = 1; r < 10; r++) { for (let x = 0; x < 16; x++) s[x] = SBOX[s[x]]; const t = s.slice(); s[0]=t[0]; s[4]=t[4]; s[8]=t[8]; s[12]=t[12]; s[1]=t[5]; s[5]=t[9]; s[9]=t[13]; s[13]=t[1]; s[2]=t[10]; s[6]=t[14]; s[10]=t[2]; s[14]=t[6]; s[3]=t[15]; s[7]=t[3]; s[11]=t[7]; s[15]=t[11]; for (let c = 0; c < 4; c++) { const x = c * 4, a0 = s[x], a1 = s[x+1], a2 = s[x+2], a3 = s[x+3]; s[x] = mul2(a0) ^ mul2(a1) ^ a1 ^ a2 ^ a3; s[x+1] = a0 ^ mul2(a1) ^ mul2(a2) ^ a2 ^ a3; s[x+2] = a0 ^ a1 ^ mul2(a2) ^ mul2(a3) ^ a3; s[x+3] = mul2(a0) ^ a0 ^ a1 ^ a2 ^ mul2(a3); } for (let x = 0; x < 16; x++) s[x] ^= w[r * 16 + x]; } for (let x = 0; x < 16; x++) s[x] = SBOX[s[x]]; const t = s.slice(); s[0]=t[0]; s[4]=t[4]; s[8]=t[8]; s[12]=t[12]; s[1]=t[5]; s[5]=t[9]; s[9]=t[13]; s[13]=t[1]; s[2]=t[10]; s[6]=t[14]; s[10]=t[2]; s[14]=t[6]; s[3]=t[15]; s[7]=t[3]; s[11]=t[7]; s[15]=t[11]; for (let x = 0; x < 16; x++) s[x] ^= w[160 + x]; return s; }; const expandKey = (k) => { const w = new Array(44); for (let i = 0; i < 16; i++) w[i] = k[i]; for (let i = 4; i < 44; i++) { let t = w.slice((i - 1) * 4, i * 4); if (i % 4 === 0) { t = [t[1], t[2], t[3], t[0]]; for (let j = 0; j < 4; j++) t[j] = SBOX[t[j]]; t[0] ^= RCON[(i / 4) - 1]; } for (let j = 0; j < 4; j++) w[i * 4 + j] = w[(i - 4) * 4 + j] ^ t[j]; } return w; }; const p = String(plain == null ? '' : plain); const key = []; for (let i = 0; i < 16; i++) key.push(this.AES_KEY.charCodeAt(i) & 0xff); const w = expandKey(key); const data = []; for (let i = 0; i < p.length; i++) data.push(p.charCodeAt(i) & 0xff); let pad = 16 - (data.length % 16); for (let i = 0; i < pad; i++) data.push(pad); let out = ''; for (let off = 0; off < data.length; off += 16) { const e = encBlock(data.slice(off, off + 16), w); for (let i = 0; i < 16; i++) out += ('0' + e[i].toString(16)).slice(-2); } return out; }, /** 从 localStorage 读取登录态(disk-pc-vuex JSON) */ readCommon() { try { const raw = localStorage.getItem('disk-pc-vuex'); if (!raw) return null; const j = JSON.parse(raw); const c = j && j.common; if (!c || !c.appToken || !c.uuid) return null; return { appToken: String(c.appToken), uuid: String(c.uuid), userId: Number(c.accountInfo && c.accountInfo.userId) || 0, account: String(c.accountInfo && c.accountInfo.account || ''), browserInfo: c.browserInfo || null, }; } catch (e) { return null; } }, /** 组装公共参数 query string(API 调用带 extra=2;下载 redirect 不带) */ qs(extra, withExtra) { const c = this.readCommon(); const p = { uuid: c.uuid, devType: '6', devCode: c.uuid, devModel: 'chrome', devVersion: '156', appVersion: '', timestamp: this.aesEcbHex(String(Date.now())), appToken: c.appToken, }; if (withExtra !== false) p.extra = 2; if (extra) Object.assign(p, extra); return new URLSearchParams(p).toString(); }, /** API 请求(页面同源 fetch,CORS 已放行);返回 {code,msg,...} 或抛错 */ async api(path, method, body, extra, opts = {}) { const c = this.readCommon(); if (!c) throw new Error('未检测到小飞机网盘登录态,请刷新页面(localStorage disk-pc-vuex)'); const q = this.qs(extra, opts.withExtra !== false); const url = this.API_BASE + path + (path.includes('?') ? '&' : '?') + q; // 注意:不携带凭证(credentials/withCredentials)——api.feijipan.com 的 CORS 为 allow-origin:* 型, // 带凭证的跨域请求会被浏览器直接拦截报 Failed to fetch;鉴权全部走 appToken 查询参数(与页面自身一致)。 // GET 不带 Content-Type(保持简单请求避免 preflight);POST JSON 才带(页面 axios 同样如此且服务端已放行)。 const headers = {}; if (body != null) headers['Content-Type'] = 'application/json'; let resp; try { resp = await fetch(url, { method: method || 'GET', headers, body: body != null ? JSON.stringify(body) : undefined }); } catch (e) { throw new Error(`小飞机网络请求失败: ${e.message}`); } let j; try { j = await resp.json(); } catch (e) { throw new Error(`小飞机 HTTP ${resp.status} 响应解析失败`); } if (j.code !== undefined && j.code !== 200) { const err = new Error(j.msg || `小飞机接口错误 code=${j.code}`); err.code = j.code; throw err; } return j; }, /** 文件列表(分页:offset 从 1 起,limit 110) */ async getFileList(folderId, offset) { const j = await this.api('/app/record/file/list', 'GET', null, { offset: Number(offset || 1), limit: CONFIG.fileListPageSize || 110, folderId: String(folderId == null || folderId === '' ? '0' : folderId), type: '0' }); const list = Array.isArray(j.list) ? j.list : []; const totalPage = Number(j.totalPage || 1); const cur = Number(j.offset || offset || 1); return { list, totalPage, hasMore: cur < totalPage }; }, /** 创建目录;响应 list[0].id 为新目录 id */ async createDir(parentId, name) { const j = await this.api('/app/file/folder/save', 'POST', { folderId: String(parentId == null || parentId === '' ? '0' : parentId), folderName: String(name), folderDesc: '' }); const id = j.list && j.list[0] && j.list[0].id; if (!id) throw new Error('小飞机创建目录未返回 id'); return String(id); }, /** 秒传(getUpToken):upToken==='-1' 命中;返回 {hit, fileId} */ async rapidUpload(fileName, md5, size, folderId) { const c = this.readCommon(); const kb = String(Math.max(1, Math.round(Number(size || 0) / 1024))); const j = await this.api('/app/vod/getUpToken', 'POST', { fileName: String(fileName || ''), fileType: 1, userId: c.userId, md5: String(md5 || '').toLowerCase(), pathKey: '', fileSize: kb, folderId: String(folderId == null || folderId === '' ? '0' : folderId), fileId: '', }); const hit = j.upToken === '-1' || j.upToken === -1; return { hit, fileId: hit ? String(j.map && j.map.fileId || '') : '', raw: j }; }, /** 彻底删除(临时项清理用) */ async deleteItems(folderIds, fileIds, status) { return this.api('/app/file/delete', 'POST', { fileIds: (fileIds || []).join(','), folderIds: (folderIds || []).join(','), status: status == null ? -1 : Number(status) }); }, /** 文件下载直链 URL(redirect;不带 extra,且必须带 Referer 才能 302) */ redirectUrl(fileId) { const c = this.readCommon(); const ts = String(Date.now()); const q = this.qs({ enable: '1', downloadId: this.aesEcbHex(String(fileId) + '|' + c.userId), auth: this.aesEcbHex(String(fileId) + '|' + ts), }, false); return this.API_BASE + '/ws/file/redirect?' + q; }, /** GM_xmlhttpRequest 封装(跨域、可读全部响应头,用于取元数据/下载) */ gmRequest(opts) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('当前环境无 GM_xmlhttpRequest')); return; } GM_xmlhttpRequest(Object.assign({ method: 'GET', timeout: 120000, headers: { 'Referer': location.href }, onload: (r) => resolve(r), onerror: () => reject(new Error('小飞机 GM 请求网络错误')), ontimeout: () => reject(new Error('小飞机 GM 请求超时')), }, opts)); }); }, /** 解析 responseHeaders 字符串中的 x-amz-meta-callback → 上传时记录的 md5 */ parseMetaCallbackMd5(responseHeaders) { try { // 注意:不能整体 toLowerCase(base64 值大小写敏感),key 匹配用 /i const h = String(responseHeaders || ''); const m = h.match(/^x-amz-meta-callback\s*:\s*(.+)$/im); if (!m) return ''; const b64 = String(m[1]).trim().replace(/["';]/g, ''); const outer = JSON.parse(atob(b64)); const inner = JSON.parse(outer.callbackBody || '{}'); const md5 = String(inner.md5 || '').toLowerCase(); return /^[0-9a-f]{32}$/.test(md5) ? md5 : ''; } catch (e) { return ''; } }, /** 从服务器取文件 md5(零下载:Range bytes=0-1 读 COS 元数据头;CDN 节点偶发不带 meta 头,重试 3 次) */ async getServerMd5(fileId) { let lastErr = null; for (let attempt = 0; attempt < 3; attempt++) { try { const url = this.redirectUrl(fileId); const resp = await this.gmRequest({ url, headers: { 'Referer': location.href, 'Range': attempt === 0 ? 'bytes=0-1' : 'bytes=0-4095' } }); if (resp.status >= 200 && resp.status < 300) { const md5 = this.parseMetaCallbackMd5(resp.responseHeaders); if (md5) return md5; } } catch (e) { lastErr = e; } await sleep(300 + attempt * 200); } return ''; }, /** Range 下载返回 Uint8Array(GM 自动跟随 302;用于 sha1/微云分块计算) */ async downloadRange(fileId, offset, length) { const url = this.redirectUrl(fileId); const resp = await this.gmRequest({ url, headers: { 'Referer': location.href, 'Range': `bytes=${offset}-${offset + length - 1}` }, responseType: 'arraybuffer', }); if (resp.status < 200 || resp.status >= 300) throw new Error('小飞机下载片段失败 HTTP ' + resp.status); try { return new Uint8Array(resp.response); } catch (e) { return new Uint8Array(resp.responseText.split('').map(ch => ch.charCodeAt(0) & 0xFF)); } }, /** 大文件下载计算 md5(CDN 无服务器哈希头时的回退;内存恒定) */ async computeMd5ByDownload(fileId, size, onProgress) { const n = Number(size || 0); if (n <= 0) return ''; return computeMd5ByRange((o, l) => this.downloadRange(fileId, o, l), n, onProgress); }, /** * 下载文件流式计算 SHA1 与微云分块信息(一次下载完成;wantBlocks 才产出分块)。 * 算法与腾讯微云一致(1MB 分段中间状态 + 末尾校验块),只是下载源换成小飞机。 * @returns {{sha1, wyBlocks, wyCheckSha, wyCheckData}} 或 null(失败) */ async computeHashes(fileId, size, wantBlocks, onProgress) { try { const n = Number(size || 0); if (n <= 0) return null; const blockSize = 1048576; const lastBlockSize = (n % blockSize) || blockSize; let checkBlockSize = lastBlockSize % 128; if (checkBlockSize === 0) checkBlockSize = 128; const beforeBlockSize = n - lastBlockSize; const sha = new Sha1Stream(); const stateHex = () => sha.stateHex ? sha.stateHex() : ''; const blockInfoList = []; let offset = 0; while (offset < beforeBlockSize) { const len = Math.min(blockSize, beforeBlockSize - offset); const buf = await this.downloadRange(fileId, offset, len); if (!buf || !buf.length) return null; sha.update(buf); blockInfoList.push({ sha: stateHex(), offset, size: len }); offset += len; if (onProgress) onProgress(Math.min(99, Math.round(offset / n * 100))); } const midLen = lastBlockSize - checkBlockSize; if (midLen > 0) { const buf = await this.downloadRange(fileId, offset, midLen); if (!buf || !buf.length) return null; sha.update(buf); offset += midLen; } const checkSha = stateHex(); const tailBuf = await this.downloadRange(fileId, offset, checkBlockSize); if (!tailBuf || !tailBuf.length) return null; sha.update(tailBuf); const fileSha = sha.hex(); blockInfoList.push({ sha: fileSha, offset: beforeBlockSize, size: lastBlockSize }); if (onProgress) onProgress(100); if (!wantBlocks) return { sha1: fileSha, wyBlocks: null, wyCheckSha: '', wyCheckData: '' }; let bin = ''; for (let i = 0; i < tailBuf.length; i += 0x8000) bin += String.fromCharCode.apply(null, tailBuf.subarray(i, i + 0x8000)); return { sha1: fileSha, wyBlocks: blockInfoList, wyCheckSha: checkSha, wyCheckData: btoa(bin) }; } catch (e) { mcLog('feejii', '哈希计算失败: ' + e.message); return null; } }, /** 当前目录 folderId:URL /console/files/{id},缺省 0=根 */ getCurrentParentId() { try { const m = location.pathname.match(/\/console\/files\/([^/?#]+)/); if (m && m[1] && String(m[1]) !== '0') return String(m[1]); } catch (e) { /* ignore */ } return '0'; }, /** 勾选读取:.files 组件 __vue__.$data {checkList, checkAll, list};兜底 DOM 勾选 + API 匹配 */ getCheckedItems() { const items = []; const seen = new Set(); const push = (it) => { const key = String(it.fileId || it.fileName || ''); if (!key || seen.has(key)) return; seen.add(key); items.push(it); }; // 1) Vue 组件状态(最可靠) try { const el = document.querySelector('.files, [class*="file-list"]'); const vm = el && (el.__vue__ || (el.firstElementChild && el.firstElementChild.__vue__)); if (vm && vm.$data) { const d = vm.$data; const list = Array.isArray(d.list) ? d.list : []; const checkList = Array.isArray(d.checkList) ? d.checkList : []; if (d.checkAll === true && list.length) { // 全选:用当前目录完整列表(避免虚拟滚动只渲染可见行) for (const it of list) push({ fileId: String(it.fileId || ''), fileName: String(it.fileName || ''), fileType: Number(it.fileType), folderId: String(it.folderId || ''), fileSize: Number(it.fileSize || 0), status: it.status }); return { all: true, items }; } if (checkList.length) { for (const it of checkList) push({ fileId: String(it.fileId || ''), fileName: String(it.fileName || ''), fileType: Number(it.fileType), folderId: String(it.folderId || ''), fileSize: Number(it.fileSize || 0), status: it.status }); return { all: false, items }; } } } catch (e) { /* ignore */ } // 2) DOM 勾选(行内 checkbox) const names = new Set(); document.querySelectorAll('.files input[type="checkbox"]:checked, .files [class*="list-item"] input[type="checkbox"]:checked, .files [class*="list-item"].act input[type="checkbox"]').forEach(cb => { const row = cb.closest('[class*="list-item"]'); if (row) { const nameEl = row.querySelector('[class*="file-name"], [class*="item-title"], [class*="name"]'); const nm = nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : ''; if (nm) names.add(nm); } }); if (names.size) { // 兜底:仅返回名字条目(fileId/大小由调用方 getListPlatform 按需补全) for (const nm of names) push({ fileId: '', fileName: nm, fileType: 1, folderId: this.getCurrentParentId(), fileSize: 0 }); return { all: false, items }; } return { all: false, items }; }, }; // ============================== 蓝奏云优享版平台(v1.6:ilanzou.com,与小飞机同 openware 框架) ============================== // 实测逆向(2026-10,www.ilanzou.com 控制台抓包 + API 真机验证): // - 与小飞机(feejii)同框架:API 基址 https://apis.ilanzou.com,proved/unproved 前缀,AES key='lanZouY-disk-app' // - 登录态同 key:localStorage['disk-pc-vuex'] → common{appToken, uuid, accountInfo.userId, browserInfo} // - 列表 GET /proved/record/file/list {offset(1起),limit,folderId,type:0} → list[](fileType 1=文件 2=目录;fileSize 单位 KB;不含 md5) // - 建目录 POST /proved/file/folder/save {folderId(父),folderName,folderDesc:''} → list[0].id // - 秒传 POST /proved/7n/getUpToken {fileName,type:1,md5,fileSize(KB整数),folderId,fileId:''} // upToken==='-1' → 创建文件记录(map.fileId)。实测任意 md5 都返回 -1:md5 不在内容池时 // 记录指向无关对象(幽灵文件)→ 导入后须经 CDN Content-Md5 零下载校验,不一致则删除并报未命中。 // - 删除 POST /proved/file/delete {fileIds,folderIds,status:0} // - 下载 GET /unproved/file/redirect {enable:1,downloadId=AES(fileId|userId),auth=AES(fileId|毫秒),appToken...} // 必须带 Referer(https://www.ilanzou.com/)→ 302 → CDN(small*.bakstotre.com)→ 响应头 // Content-Md5 = base64(文件真实 md5)。导出零下载:Range bytes=0-1 极小请求读该头即可 // (浏览器 fetch 读不到自定义头,须用 GM_xmlhttpRequest) // - 勾选:.files 组件 __vue__.$data {checkList, checkAll, list(当前目录完整条目)} /** openware 框架 AES-128-ECB/PKCS7 → hex(key 参数化;小飞机/优享版共用,已与抓包 timestamp 密文验证) */ function openwareAesEcbHex(plain, keyStr) { const SBOX = [0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16]; const RCON = [0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36]; const mul2 = x => ((x << 1) ^ (((x >> 7) & 1) * 0x1b)) & 0xff; const encBlock = (input, w) => { let s = input.slice(), r; for (let x = 0; x < 16; x++) s[x] ^= w[x]; for (r = 1; r < 10; r++) { for (let x = 0; x < 16; x++) s[x] = SBOX[s[x]]; const t = s.slice(); s[0]=t[0]; s[4]=t[4]; s[8]=t[8]; s[12]=t[12]; s[1]=t[5]; s[5]=t[9]; s[9]=t[13]; s[13]=t[1]; s[2]=t[10]; s[6]=t[14]; s[10]=t[2]; s[14]=t[6]; s[3]=t[15]; s[7]=t[3]; s[11]=t[7]; s[15]=t[11]; for (let c = 0; c < 4; c++) { const x = c * 4, a0 = s[x], a1 = s[x+1], a2 = s[x+2], a3 = s[x+3]; s[x] = mul2(a0) ^ mul2(a1) ^ a1 ^ a2 ^ a3; s[x+1] = a0 ^ mul2(a1) ^ mul2(a2) ^ a2 ^ a3; s[x+2] = a0 ^ a1 ^ mul2(a2) ^ mul2(a3) ^ a3; s[x+3] = mul2(a0) ^ a0 ^ a1 ^ a2 ^ mul2(a3); } for (let x = 0; x < 16; x++) s[x] ^= w[r * 16 + x]; } for (let x = 0; x < 16; x++) s[x] = SBOX[s[x]]; const t = s.slice(); s[0]=t[0]; s[4]=t[4]; s[8]=t[8]; s[12]=t[12]; s[1]=t[5]; s[5]=t[9]; s[9]=t[13]; s[13]=t[1]; s[2]=t[10]; s[6]=t[14]; s[10]=t[2]; s[14]=t[6]; s[3]=t[15]; s[7]=t[3]; s[11]=t[7]; s[15]=t[11]; for (let x = 0; x < 16; x++) s[x] ^= w[160 + x]; return s; }; const expandKey = (k) => { const w = new Array(44); for (let i = 0; i < 16; i++) w[i] = k[i]; for (let i = 4; i < 44; i++) { let t = w.slice((i - 1) * 4, i * 4); if (i % 4 === 0) { t = [t[1], t[2], t[3], t[0]]; for (let j = 0; j < 4; j++) t[j] = SBOX[t[j]]; t[0] ^= RCON[(i / 4) - 1]; } for (let j = 0; j < 4; j++) w[i * 4 + j] = w[(i - 4) * 4 + j] ^ t[j]; } return w; }; const p = String(plain == null ? '' : plain); const key = []; for (let i = 0; i < 16; i++) key.push(String(keyStr || '').charCodeAt(i) & 0xff); const w = expandKey(key); const data = []; for (let i = 0; i < p.length; i++) data.push(p.charCodeAt(i) & 0xff); let pad = 16 - (data.length % 16); for (let i = 0; i < pad; i++) data.push(pad); let out = ''; for (let off = 0; off < data.length; off += 16) { const e = encBlock(data.slice(off, off + 16), w); for (let i = 0; i < 16; i++) out += ('0' + e[i].toString(16)).slice(-2); } return out; } /** 蓝奏云 acw_sc__v2 人机校验 Cookie 计算(yunx-repo LanzouApi 移植,arg1 40hex → cookie 值) */ function lanzouAcwScV2(arg1) { const pos = [15, 35, 29, 24, 33, 16, 1, 38, 10, 9, 19, 31, 40, 27, 22, 23, 25, 13, 6, 11, 39, 18, 20, 8, 14, 21, 32, 26, 2, 30, 7, 4, 17, 5, 3, 28, 34, 37, 12, 36]; const mask = '3000176000856006061501533003690027800375'; const q = new Array(pos.length); for (let x = 0; x < arg1.length; x++) { for (let z = 0; z < pos.length; z++) { if (pos[z] === x + 1) q[z] = arg1[x]; } } const u = q.join(''); let out = ''; let i = 0; while (i + 2 <= u.length && i + 2 <= mask.length) { const a = parseInt(u.substring(i, i + 2), 16); const b = parseInt(mask.substring(i, i + 2), 16); out += (a ^ b).toString(16).padStart(2, '0'); i += 2; } return out; } /** 解析 GM 响应头字符串中的 Content-Md5(base64 → 32hex) */ function parseContentMd5Header(responseHeaders) { try { const h = String(responseHeaders || ''); const m = h.match(/^content-md5\s*:\s*(.+)$/im); if (!m) return ''; const b64 = String(m[1]).trim().replace(/["';]/g, ''); const hex = normalizeMd5(b64); return /^[0-9a-f]{32}$/.test(hex) ? hex : ''; } catch (e) { return ''; } } const ILANZOU = { API_BASE: 'https://apis.ilanzou.com', AES_KEY: 'lanZouY-disk-app', PAGE_STATE_KEY: '__ILZMS_PAGE_STATE__', aesEcbHex(plain) { return openwareAesEcbHex(plain, this.AES_KEY); }, /** 从 localStorage 读取登录态(disk-pc-vuex JSON,与小飞机同框架) */ readCommon() { try { const raw = localStorage.getItem('disk-pc-vuex'); if (!raw) return null; const j = JSON.parse(raw); const c = j && j.common; if (!c || !c.appToken || !c.uuid) return null; return { appToken: String(c.appToken), uuid: String(c.uuid), userId: Number(c.accountInfo && c.accountInfo.userId) || 0, account: String(c.accountInfo && c.accountInfo.account || ''), browserInfo: c.browserInfo || null, }; } catch (e) { return null; } }, /** 组装公共参数 query string(API 调用带 extra=2;下载 redirect 不带) */ qs(extra, withExtra) { const c = this.readCommon(); const p = { uuid: c.uuid, devType: '6', devCode: c.uuid, devModel: 'chrome', devVersion: '156', appVersion: '', timestamp: this.aesEcbHex(String(Date.now())), appToken: c.appToken, }; if (withExtra !== false) p.extra = 2; if (extra) Object.assign(p, extra); return new URLSearchParams(p).toString(); }, /** API 请求(页面同源 fetch,CORS 已放行);返回 {code,msg,...} 或抛错 */ async api(path, method, body, extra, opts = {}) { const c = this.readCommon(); if (!c) throw new Error('未检测到蓝奏云优享版登录态,请刷新页面(localStorage disk-pc-vuex)'); const q = this.qs(extra, opts.withExtra !== false); const url = this.API_BASE + path + (path.includes('?') ? '&' : '?') + q; const headers = {}; if (body != null) headers['Content-Type'] = 'application/json'; let resp; try { resp = await fetch(url, { method: method || 'GET', headers, body: body != null ? JSON.stringify(body) : undefined }); } catch (e) { throw new Error(`优享版网络请求失败: ${e.message}`); } let j; try { j = await resp.json(); } catch (e) { throw new Error(`优享版 HTTP ${resp.status} 响应解析失败`); } if (j.code !== undefined && j.code !== 200) { const err = new Error(j.msg || `优享版接口错误 code=${j.code}`); err.code = j.code; throw err; } return j; }, /** 文件列表(分页:offset 从 1 起) */ async getFileList(folderId, offset) { const j = await this.api('/proved/record/file/list', 'GET', null, { offset: Number(offset || 1), limit: CONFIG.fileListPageSize || 110, folderId: String(folderId == null || folderId === '' ? '0' : folderId), type: '0' }); const list = Array.isArray(j.list) ? j.list : []; const totalPage = Number(j.totalPage || 1); const cur = Number(j.offset || offset || 1); return { list, totalPage, hasMore: cur < totalPage }; }, /** 创建目录;响应 list[0].id 为新目录 id */ async createDir(parentId, name) { const j = await this.api('/proved/file/folder/save', 'POST', { folderId: String(parentId == null || parentId === '' ? '0' : parentId), folderName: String(name), folderDesc: '' }); const id = j.list && j.list[0] && j.list[0].id; if (!id) throw new Error('优享版创建目录未返回 id'); return String(id); }, /** 秒传(getUpToken):upToken==='-1' 创建记录(map.fileId);只认 md5 */ async rapidUpload(fileName, md5, size, folderId) { const c = this.readCommon(); const kb = String(Math.max(1, Math.round(Number(size || 0) / 1024))); const j = await this.api('/proved/7n/getUpToken', 'POST', { fileName: String(fileName || ''), type: 1, md5: String(md5 || '').toLowerCase(), fileSize: kb, folderId: String(folderId == null || folderId === '' ? '0' : folderId), fileId: '', }); const hit = j.upToken === '-1' || j.upToken === -1; return { hit, fileId: hit ? String(j.map && j.map.fileId || '') : '', raw: j }; }, /** 删除(导入幽灵清理用;status 0=进回收站) */ async deleteItems(folderIds, fileIds, status) { return this.api('/proved/file/delete', 'POST', { fileIds: (fileIds || []).join(','), folderIds: (folderIds || []).join(','), status: status == null ? 0 : Number(status) }); }, /** 文件下载直链 URL(redirect;不带 extra,且必须带 Referer 才能 302) */ redirectUrl(fileId) { const c = this.readCommon(); const ts = String(Date.now()); const q = this.qs({ enable: '1', downloadId: this.aesEcbHex(String(fileId) + '|' + c.userId), auth: this.aesEcbHex(String(fileId) + '|' + ts), }, false); return this.API_BASE + '/unproved/file/redirect?' + q; }, /** GM_xmlhttpRequest 封装(跨域、可读全部响应头) */ gmRequest(opts) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('当前环境无 GM_xmlhttpRequest')); return; } GM_xmlhttpRequest(Object.assign({ method: 'GET', timeout: 120000, headers: { 'Referer': 'https://www.ilanzou.com/' }, onload: (r) => resolve(r), onerror: () => reject(new Error('优享版 GM 请求网络错误')), ontimeout: () => reject(new Error('优享版 GM 请求超时')), }, opts)); }); }, /** 从服务器取文件 md5(零下载:Range bytes=0-1 读 CDN 响应头 Content-Md5;CDN 节点偶发不带,重试 3 次) */ async getServerMd5(fileId) { let lastErr = null; for (let attempt = 0; attempt < 3; attempt++) { try { const url = this.redirectUrl(fileId); const resp = await this.gmRequest({ url, headers: { 'Referer': 'https://www.ilanzou.com/', 'Range': attempt === 0 ? 'bytes=0-1' : 'bytes=0-4095' } }); if (resp.status >= 200 && resp.status < 300) { const md5 = parseContentMd5Header(resp.responseHeaders); if (md5) return md5; } } catch (e) { lastErr = e; } await sleep(300 + attempt * 200); } return ''; }, /** Range 下载返回 Uint8Array(GM 自动跟随 302;用于大文件下载计算 md5) */ async downloadRange(fileId, offset, length) { const url = this.redirectUrl(fileId); const resp = await this.gmRequest({ url, headers: { 'Referer': 'https://www.ilanzou.com/', 'Range': `bytes=${offset}-${offset + length - 1}` }, responseType: 'arraybuffer', }); if (resp.status < 200 || resp.status >= 300) throw new Error('优享版下载片段失败 HTTP ' + resp.status); try { return new Uint8Array(resp.response); } catch (e) { return new Uint8Array(resp.responseText.split('').map(ch => ch.charCodeAt(0) & 0xFF)); } }, /** 大文件下载计算 md5(CDN 无 content-md5 头时的回退;内存恒定) */ async computeMd5ByDownload(fileId, size, onProgress) { const n = Number(size || 0); if (n <= 0) return ''; return computeMd5ByRange((o, l) => this.downloadRange(fileId, o, l), n, onProgress); }, /** 当前目录 folderId:URL /console/files/{id},缺省 0=根 */ getCurrentParentId() { try { const m = location.pathname.match(/\/console\/files\/([^/?#]+)/); if (m && m[1] && String(m[1]) !== '0') return String(m[1]); } catch (e) { /* ignore */ } return '0'; }, /** 勾选读取:.files 组件 __vue__.$data {checkList, checkAll, list};兜底 DOM 勾选 + API 匹配 */ getCheckedItems() { const items = []; const seen = new Set(); const push = (it) => { const key = String(it.fileId || it.fileName || ''); if (!key || seen.has(key)) return; seen.add(key); items.push(it); }; try { const el = document.querySelector('.files, [class*="file-list"]'); const vm = el && (el.__vue__ || (el.firstElementChild && el.firstElementChild.__vue__)); if (vm && vm.$data) { const d = vm.$data; const list = Array.isArray(d.list) ? d.list : []; const checkList = Array.isArray(d.checkList) ? d.checkList : []; if (d.checkAll === true && list.length) { for (const it of list) push({ fileId: String(it.fileId || ''), fileName: String(it.fileName || ''), fileType: Number(it.fileType), folderId: String(it.folderId || ''), fileSize: Number(it.fileSize || 0), status: it.status }); return { all: true, items }; } if (checkList.length) { for (const it of checkList) push({ fileId: String(it.fileId || ''), fileName: String(it.fileName || ''), fileType: Number(it.fileType), folderId: String(it.folderId || ''), fileSize: Number(it.fileSize || 0), status: it.status }); return { all: false, items }; } } } catch (e) { /* ignore */ } return { all: false, items }; }, }; // ============================== 蓝奏云平台(v1.6:woozooo.com,分享链 + CDN Content-Md5 零下载导出) ============================== // 实测逆向(2026-10,up.woozooo.com 个人盘抓包 + 分享下载链路验证): // - 鉴权:Cookie(ylogin/phpdisk_info/PHPSESSID 随页面请求)+ vei(文件页 HTML 内变量,绑定 PHPSESSID)+ uid(iframe URL u=) // - 列表 POST /doupload.php?uid=:task=5(文件 {folder_id,pg}) / task=47(文件夹 {folder_id}),vei 参数必带 // 文件条目 {id, name_all, size(字符串"526.0 B"), time, icon, downs, onof};**不含 md5**;zt=1 成功/2 空 // - 建目录 task=2 {parent_id, folder_name, folder_description} → text=新目录 id;删除 task=6 {file_id} // - 分享信息 task=22 {file_id} → info {f_id(分享ID), pwd, onof(1=有密码), is_newd(分享域名)} // - 分享页 GET https://{is_newd}/{f_id} → 解析 sign(isngis 变量)+ ajaxfile.php?file=<个人盘file_id> + kdns; // 可能遇 acw_sc__v2 校验页(arg1 40hex → 计算 Cookie 重试) // - 取直链 POST https://apifile.woozooo.com/ajaxfile.php?file= {action:downprocess, sign, kd, p} // → {dom, url, inf} → 下载节点 GET https://{dom}/file/?{url}(Referer=分享页 + Cookie:down_ip=1) // → 302 → CDN(zip*.webgetstore.com)→ 响应头 Content-Md5 = base64(文件真实 md5) → 零下载取 md5 // - **无哈希秒传接口**(html5up.php 必须携带文件本体、md5-only 被拒、服务器不做内容去重)→ 导入不支持 // - 页面为 iframe(mainframe)结构,脚本须跨 iframe 读取 const LANZOU = { PAGE_STATE_KEY: '__LZMS_PAGE_STATE__', /** 访问个人盘文件管理 iframe(mydisk.php?item=files&action=index&u=xxx,同源) */ frameDoc() { try { const f = document.querySelector('iframe[name="mainframe"], iframe[src*="item=files"], iframe'); if (f && f.contentDocument) return f.contentDocument; } catch (e) { /* ignore */ } return null; }, /** 从 iframe 页面提取 vei(服务端渲染变量,绑定 PHPSESSID;页面形式为 'vei':'xxx') */ extractVei(doc) { try { const d = doc || this.frameDoc() || document; const html = d.documentElement ? d.documentElement.innerHTML : ''; const m = html.match(/vei['"]?\s*[:=]\s*['"]([^'",}]+)['"]?/); if (m && m[1]) return m[1]; } catch (e) { /* ignore */ } return ''; }, /** 从 iframe URL 提取 uid */ extractUid(doc) { try { const d = doc || this.frameDoc(); const url = d && d.location ? d.location.href : location.href; const m = url.match(/[?&]u=(\d+)/); if (m && m[1]) return m[1]; const m2 = url.match(/[?&]id=(\d+)/); if (m2 && m2[1]) return m2[1]; } catch (e) { /* ignore */ } return ''; }, /** 当前目录 folder_id(iframe 全局变量;根目录 -1) */ getCurrentFolderId() { try { const d = this.frameDoc(); if (d) { const v = d.defaultView && d.defaultView.folder_id; if (v !== undefined && v !== null && String(v) !== 'undefined') return String(v); } } catch (e) { /* ignore */ } return '-1'; }, /** 页面侧 doupload 请求(iframe 内 fetch,同源带 cookie) */ doupload(uid, vei, data) { const d = this.frameDoc(); const win = (d && d.defaultView) || window; return win.fetch('/doupload.php?uid=' + encodeURIComponent(uid), { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(Object.assign({ vei: vei || '' }, data)).toString(), }).then(r => r.json()); }, /** 文件列表:task=47 文件夹 + task=5 文件(分页) */ async getFileList(folderId) { const uid = this.extractUid(); const vei = this.extractVei(); if (!uid || !vei) throw new Error('蓝奏云未检测到会话参数(uid/vei),请刷新页面'); const fid = String(folderId == null || folderId === '' ? '-1' : folderId); const folders = await this.doupload(uid, vei, { task: '47', folder_id: fid }); const files = []; for (let pg = 1; pg <= 200; pg++) { const r = await this.doupload(uid, vei, { task: '5', folder_id: fid, pg: String(pg) }); const arr = (r && Array.isArray(r.text)) ? r.text : []; files.push(...arr); if (!arr.length) break; await sleep(CONFIG.listPageDelay); } const folderItems = (folders && Array.isArray(folders.text) ? folders.text : []).map(x => ({ id: String(x.fol_id || x.id || ''), name: String(x.name || x.name_all || ''), isDir: true, size: 0, time: String(x.time || ''), })); const fileItems = files.map(x => ({ id: String(x.id || ''), name: String(x.name_all || x.name || ''), isDir: false, size: String(x.size || ''), sizeBytes: 0, time: String(x.time || ''), icon: String(x.icon || ''), })); return { folders: folderItems, files: fileItems }; }, /** 建目录:task=2 → text 为新目录 id */ async createDir(parentId, dirName) { const uid = this.extractUid(); const vei = this.extractVei(); const r = await this.doupload(uid, vei, { task: '2', parent_id: String(parentId == null || parentId === '' ? '-1' : parentId), folder_name: String(dirName), folder_description: '' }); if (Number(r.zt) !== 1 || !r.text) throw new Error('蓝奏云创建目录失败:' + (r.inf || r.info || JSON.stringify(r).slice(0, 120))); return String(r.text); }, /** 删除文件:task=6 */ async deleteFile(fileId) { const uid = this.extractUid(); const vei = this.extractVei(); const r = await this.doupload(uid, vei, { task: '6', file_id: String(fileId) }); return Number(r.zt) === 1; }, /** 分享信息:task=22 → {f_id, pwd, onof, is_newd} */ async getShareInfo(fileId) { const uid = this.extractUid(); const vei = this.extractVei(); const r = await this.doupload(uid, vei, { task: '22', file_id: String(fileId) }); const info = r && r.info; if (Number(r.zt) !== 1 || !info) throw new Error('蓝奏云获取分享信息失败(fileId=' + fileId + ')'); return { fId: String(info.f_id || ''), pwd: String(info.pwd || ''), onof: String(info.onof || '0'), isNewd: String(info.is_newd || ''), taoc: String(info.taoc || '') }; }, /** 分享页 HTML(GM 跨域访问;处理 acw_sc__v2 校验重试) */ async fetchSharePage(shareUrl) { const shareOrigin = (() => { try { return new URL(shareUrl).origin; } catch (e) { return ''; } })(); let lastErr = null; for (let attempt = 0; attempt < 3; attempt++) { try { const headers = { 'Referer': shareOrigin + '/', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36 Edg/156.0.0.0' }; if (attempt > 0 && this._acwCookie) headers['Cookie'] = 'acw_sc__v2=' + this._acwCookie; const resp = await this.gmRequest({ url: shareUrl, headers, responseType: 'text' }); if (resp.status < 200 || resp.status >= 300) { lastErr = new Error('分享页 HTTP ' + resp.status); continue; } const text = String(resp.responseText || resp.response || ''); const arg1m = text.match(/\barg1\s*=\s*'([0-9A-Fa-f]{40})'/); if (arg1m) { this._acwCookie = lanzouAcwScV2(arg1m[1]); continue; // 重放 } return text; } catch (e) { lastErr = e; } await sleep(300 + attempt * 250); } throw new Error('蓝奏云分享页访问失败:' + (lastErr ? lastErr.message : '未知错误')); }, /** GM_xmlhttpRequest 封装 */ gmRequest(opts) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('当前环境无 GM_xmlhttpRequest')); return; } GM_xmlhttpRequest(Object.assign({ method: 'GET', timeout: 120000, onload: (r) => resolve(r), onerror: () => reject(new Error('蓝奏云 GM 请求网络错误')), ontimeout: () => reject(new Error('蓝奏云 GM 请求超时')), }, opts)); }); }, /** 从分享页 HTML 解析下载参数:{ajaxUrl, sign, kd, withPwd} */ parseSharePage(html, shareOrigin, fileId, shareInfo) { // ajaxfile 地址:优先页面内 url:'https://apifile.woozooo.com/ajaxfile.php?file=xxx' const ajaxUrl = (html.match(/https?:\/\/[^'"\s]+\/(?:ajaxm|ajaxfile)\.php\?file=\d+/) || [])[0] || (html.match(/url\s*:\s*'(https?:\/\/[^']+\/(?:ajaxm|ajaxfile)\.php\?file=\d+)'/) || [])[1] || (shareOrigin + '/ajaxfile.php?file=' + fileId); // 签名变量:新版 wp_sign / isngis / sign let sign = (html.match(/\bwp_sign\s*=\s*'([^']+)'/) || [])[1] || (html.match(/\bisngis\s*=\s*'([^']+)'/) || [])[1] || (html.match(/\bsign\s*=\s*'([^']+)'/) || [])[1] || ''; // 无密码新版:signs/websignkey/websign 协议 const signs = (html.match(/\bsigns\s*=\s*'([^']+)'/) || [])[1] || (html.match(/\bwebsignkey\s*=\s*'([^']+)'/) || [])[1] || ''; const websignkey = (html.match(/\bwebsignkey\s*=\s*'([^']+)'/) || [])[1] || ''; const websign = (html.match(/\bwebsign\s*=\s*'([^']+)'/) || [])[1] || ''; const kd = (html.match(/\bkdns\s*=\s*'?(\d+)'?/) || [])[1] || '1'; const withPwd = Number(shareInfo.onof) === 1; return { ajaxUrl, sign, signs, websignkey, websign, kd, withPwd }; }, /** 调 ajaxfile.php 取下载节点直链 {dom, url} */ async getDownloadNode(fileId, shareInfo) { const shareUrl = shareInfo.isNewd + '/' + shareInfo.fId; const shareOrigin = (() => { try { return new URL(shareUrl).origin; } catch (e) { return ''; } })(); const html = await this.fetchSharePage(shareUrl); const p = this.parseSharePage(html, shareOrigin, fileId, shareInfo); if (!p.sign && !p.signs) throw new Error('蓝奏云分享页缺少下载参数(sign),请手动打开分享页一次后重试'); const form = { action: 'downprocess', kd: p.kd }; if (p.withPwd) { form.sign = p.sign; form.p = shareInfo.pwd; } else { form.sign = p.sign; form.signs = p.signs || p.sign; form.websignkey = p.websignkey; form.websign = p.websign; form.ves = '1'; } const resp = await this.gmRequest({ url: p.ajaxUrl, method: 'POST', headers: { 'Referer': shareUrl, 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36 Edg/156.0.0.0', }, data: new URLSearchParams(form).toString(), }); let j; try { j = JSON.parse(resp.responseText || '{}'); } catch (e) { throw new Error('蓝奏云取直链响应解析失败:' + String(resp.responseText || '').slice(0, 150)); } if (Number(j.zt) !== 1 || !j.dom || !j.url) { const msg = String(j.inf || j.info || j.msg || '') || ('zt=' + j.zt); throw new Error('蓝奏云取直链失败:' + msg); } return { dom: String(j.dom), url: String(j.url) }; }, /** 下载节点缓存(签名 URL 60s 内复用,避免每个 Range 分块重复走分享页) */ async getNodeCached(fileId) { const shareInfo = await this.getShareInfo(fileId); const shareUrl = shareInfo.isNewd + '/' + shareInfo.fId; const shareOrigin = (() => { try { return new URL(shareUrl).origin; } catch (e) { return ''; } })(); const now = Date.now(); if (this._nodeCache && this._nodeCache.fileId === String(fileId) && now - this._nodeCache.at < 60000) { return this._nodeCache; } const node = await this.getDownloadNode(fileId, shareInfo); const cache = { fileId: String(fileId), dom: node.dom, url: node.url, shareOrigin, at: now }; this._nodeCache = cache; return cache; }, /** * lanrar 下载节点 acw_sc__v2 人机校验:首访(无 Range)拿校验页 arg1 → 计算 cookie → 缓存。 * 实测:带 Range 的请求在无 acw cookie 时被直接断连(网络错误),必须先过校验再重放。 * 返回 acw cookie 值('' 表示无需校验/直接可用)。 */ async ensureLanrarAcw(node) { if (this._lanrarAcw && Date.now() - this._lanrarAcwAt < 55 * 60 * 1000) return this._lanrarAcw; try { const jump = node.dom + '/file/?' + node.url.replace(/^\?/, ''); for (let attempt = 0; attempt < 3; attempt++) { const resp = await this.gmRequest({ url: jump, headers: { 'Referer': node.shareOrigin + '/', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36 Edg/156.0.0.0', 'Cookie': 'down_ip=1', }, }); const text = String(resp.responseText || ''); const m = text.match(/\barg1\s*=\s*'([0-9A-Fa-f]{40})'/); if (m) { this._lanrarAcw = lanzouAcwScV2(m[1]); this._lanrarAcwAt = Date.now(); return this._lanrarAcw; } // 无校验页(可能已直接 302/200)→ 无需 acw this._lanrarAcw = ''; this._lanrarAcwAt = Date.now(); return ''; } } catch (e) { /* 重试外层 */ } return this._lanrarAcw || ''; }, /** 下载节点 → CDN 响应头取 Content-Md5(零下载取 md5);先过 acw 校验再带 Range 重放;重试 3 次 */ async getServerMd5(fileId) { let node; try { node = await this.getNodeCached(fileId); } catch (e) { return ''; } const jump = node.dom + '/file/?' + node.url.replace(/^\?/, ''); let lastErr = null; for (let attempt = 0; attempt < 3; attempt++) { try { // 先确保 lanrar acw 校验(首次无 Range 拿 arg1;带 Range 请求无 acw 会被断连) const acw = await this.ensureLanrarAcw(node); const cookies = 'down_ip=1' + (acw ? '; acw_sc__v2=' + acw : ''); const resp = await this.gmRequest({ url: jump, headers: { 'Referer': node.shareOrigin + '/', 'Cookie': cookies, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36 Edg/156.0.0.0', 'Range': attempt === 0 ? 'bytes=0-1' : 'bytes=0-4095', }, }); if (resp.status >= 200 && resp.status < 300) { const md5 = parseContentMd5Header(resp.responseHeaders); if (md5) return md5; // 若返回校验页则清除缓存重算 if (/arg1\s*=\s*'[0-9A-Fa-f]{40}'/.test(String(resp.responseText || ''))) { this._lanrarAcw = ''; } } } catch (e) { lastErr = e; } await sleep(300 + attempt * 250); } return ''; }, /** Range 下载返回 Uint8Array(复用下载节点签名 URL;先过 acw 校验;用于大文件下载计算 md5) */ async downloadRange(fileId, offset, length) { const node = await this.getNodeCached(fileId); const jump = node.dom + '/file/?' + node.url.replace(/^\?/, ''); const acw = await this.ensureLanrarAcw(node); const cookies = 'down_ip=1' + (acw ? '; acw_sc__v2=' + acw : ''); const resp = await this.gmRequest({ url: jump, headers: { 'Referer': node.shareOrigin + '/', 'Cookie': cookies, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36 Edg/156.0.0.0', 'Range': `bytes=${offset}-${offset + length - 1}`, }, responseType: 'arraybuffer', }); if (resp.status < 200 || resp.status >= 300) throw new Error('蓝奏云下载片段失败 HTTP ' + resp.status); try { return new Uint8Array(resp.response); } catch (e) { return new Uint8Array(resp.responseText.split('').map(ch => ch.charCodeAt(0) & 0xFF)); } }, /** 大文件下载计算 md5(CDN 无 content-md5 头时的回退;内存恒定) */ async computeMd5ByDownload(fileId, size, onProgress) { const n = Number(size || 0); if (n <= 0) return ''; return computeMd5ByRange((o, l) => this.downloadRange(fileId, o, l), n, onProgress); }, /** 勾选/范围读取:蓝奏云无网页多选,返回 null 表示需用弹窗选择器 */ getCheckedItems() { return null; }, }; /** 微云导出分块计算的状态 hex(需先给 Sha1Stream 增加 stateHex 能力,见下) */ if (!Sha1Stream.prototype || !('stateHex' in (Sha1Stream.prototype || {}))) { /* 由下方扩展实现 */ } /** * 流式 SHA1(RFC 3174;分块增量 update,大文件不占满内存)。 * 用于夸克/UC 导出时下载文件流式计算真实 SHA1(跨账号秒传的全局键)。 * 已验证:empty/abc/Hello World/本地文件 全部与标准 SHA1 一致。 */ function Sha1Stream() { let h0 = 0x67452301, h1 = 0xEFCDAB89, h2 = 0x98BADCFE, h3 = 0x10325476, h4 = 0xC3D2E1F0; let buffer = [], bufLen = 0; let msgLenBits = 0; function rotl(x, n) { return ((x << n) | (x >>> (32 - n))) >>> 0; } function processChunk(words) { const w = new Array(80); for (let i = 0; i < 16; i++) w[i] = words[i]; for (let i = 16; i < 80; i++) { const t = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]; w[i] = rotl(t, 1); } let a = h0, b = h1, c = h2, d = h3, e = h4; for (let i = 0; i < 80; i++) { let f, k; if (i < 20) { f = (b & c) | ((~b) & d); k = 0x5A827999; } else if (i < 40) { f = b ^ c ^ d; k = 0x6ED9EBA1; } else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDC; } else { f = b ^ c ^ d; k = 0xCA62C1D6; } const temp = (rotl(a, 5) + f + e + k + w[i]) >>> 0; e = d; d = c; c = rotl(b, 30); b = a; a = temp; } h0 = (h0 + a) >>> 0; h1 = (h1 + b) >>> 0; h2 = (h2 + c) >>> 0; h3 = (h3 + d) >>> 0; h4 = (h4 + e) >>> 0; } this.update = function (bytes) { if (!bytes || !bytes.length) return; msgLenBits += bytes.length * 8; for (let i = 0; i < bytes.length; i++) { buffer.push(bytes[i] & 0xFF); bufLen++; if (bufLen === 64) { const words = new Array(16); for (let j = 0; j < 16; j++) { words[j] = ((buffer[j * 4] << 24) | (buffer[j * 4 + 1] << 16) | (buffer[j * 4 + 2] << 8) | buffer[j * 4 + 3]) >>> 0; } processChunk(words); buffer = []; bufLen = 0; } } }; /** * 导出当前 SHA1 中间状态(Go crypto/sha1 MarshalBinary 前 20 字节,每 4 字节小端), * 用于腾讯微云秒传 block_info_list.sha / check_sha(weiyun-sdk-go GetSha1State 同款,已实测验证)。 */ this.stateHex = function () { return [h0, h1, h2, h3, h4].map(v => { const b = [v & 0xff, (v >>> 8) & 0xff, (v >>> 16) & 0xff, (v >>> 24) & 0xff]; return b.map(x => ('0' + x.toString(16)).slice(-2)).join(''); }).join(''); }; this.hex = function () { buffer.push(0x80); bufLen++; while (bufLen % 64 !== 56) { buffer.push(0x00); bufLen++; } const hi = Math.floor(msgLenBits / 0x100000000), lo = msgLenBits >>> 0; const lenBytes = [ (hi >>> 24) & 0xFF, (hi >>> 16) & 0xFF, (hi >>> 8) & 0xFF, hi & 0xFF, (lo >>> 24) & 0xFF, (lo >>> 16) & 0xFF, (lo >>> 8) & 0xFF, lo & 0xFF, ]; buffer.push(...lenBytes); while (buffer.length >= 64) { const words = new Array(16); for (let j = 0; j < 16; j++) { words[j] = ((buffer[j * 4] << 24) | (buffer[j * 4 + 1] << 16) | (buffer[j * 4 + 2] << 8) | buffer[j * 4 + 3]) >>> 0; } processChunk(words); buffer.splice(0, 64); } const parts = [h0, h1, h2, h3, h4].map(v => ('00000000' + v.toString(16)).slice(-8)); return parts.join('').toLowerCase(); }; } /** 增量 MD5(RFC 1321;分块 update,大文件不占内存;用于大文件无服务器哈希时的下载计算) */ function Md5Stream() { let a0 = 1732584193, b0 = -271733879, c0 = -1732584194, d0 = 271733878; const buf = new Uint8Array(128); let bufLen = 0; let byteLen = 0; const safeAdd = (x, y) => { const lsw = (x & 0xffff) + (y & 0xffff); const msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xffff); }; const bitRotateLeft = (num, cnt) => (num << cnt) | (num >>> (32 - cnt)); const md5cmn = (q, a, b, x, s, t) => safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b); const md5ff = (a, b, c, d, x, s, t) => md5cmn((b & c) | (~b & d), a, b, x, s, t); const md5gg = (a, b, c, d, x, s, t) => md5cmn((b & d) | (c & ~d), a, b, x, s, t); const md5hh = (a, b, c, d, x, s, t) => md5cmn(b ^ c ^ d, a, b, x, s, t); const md5ii = (a, b, c, d, x, s, t) => md5cmn(c ^ (b | ~d), a, b, x, s, t); function round(x) { let a = a0, b = b0, c = c0, d = d0; a = md5ff(a, b, c, d, x[0], 7, -680876936); d = md5ff(d, a, b, c, x[1], 12, -389564586); c = md5ff(c, d, a, b, x[2], 17, 606105819); b = md5ff(b, c, d, a, x[3], 22, -1044525330); a = md5ff(a, b, c, d, x[4], 7, -176418897); d = md5ff(d, a, b, c, x[5], 12, 1200080426); c = md5ff(c, d, a, b, x[6], 17, -1473231341); b = md5ff(b, c, d, a, x[7], 22, -45705983); a = md5ff(a, b, c, d, x[8], 7, 1770035416); d = md5ff(d, a, b, c, x[9], 12, -1958414417); c = md5ff(c, d, a, b, x[10], 17, -42063); b = md5ff(b, c, d, a, x[11], 22, -1990404162); a = md5ff(a, b, c, d, x[12], 7, 1804603682); d = md5ff(d, a, b, c, x[13], 12, -40341101); c = md5ff(c, d, a, b, x[14], 17, -1502002290); b = md5ff(b, c, d, a, x[15], 22, 1236535329); a = md5gg(a, b, c, d, x[1], 5, -165796510); d = md5gg(d, a, b, c, x[6], 9, -1069501632); c = md5gg(c, d, a, b, x[11], 14, 643717713); b = md5gg(b, c, d, a, x[0], 20, -373897302); a = md5gg(a, b, c, d, x[5], 5, -701558691); d = md5gg(d, a, b, c, x[10], 9, 38016083); c = md5gg(c, d, a, b, x[15], 14, -660478335); b = md5gg(b, c, d, a, x[4], 20, -405537848); a = md5gg(a, b, c, d, x[9], 5, 568446438); d = md5gg(d, a, b, c, x[14], 9, -1019803690); c = md5gg(c, d, a, b, x[3], 14, -187363961); b = md5gg(b, c, d, a, x[8], 20, 1163531501); a = md5gg(a, b, c, d, x[13], 5, -1444681467); d = md5gg(d, a, b, c, x[2], 9, -51403784); c = md5gg(c, d, a, b, x[7], 14, 1735328473); b = md5gg(b, c, d, a, x[12], 20, -1926607734); a = md5hh(a, b, c, d, x[5], 4, -378558); d = md5hh(d, a, b, c, x[8], 11, -2022574463); c = md5hh(c, d, a, b, x[11], 16, 1839030562); b = md5hh(b, c, d, a, x[14], 23, -35309556); a = md5hh(a, b, c, d, x[1], 4, -1530992060); d = md5hh(d, a, b, c, x[4], 11, 1272893353); c = md5hh(c, d, a, b, x[7], 16, -155497632); b = md5hh(b, c, d, a, x[10], 23, -1094730640); a = md5hh(a, b, c, d, x[13], 4, 681279174); d = md5hh(d, a, b, c, x[0], 11, -358537222); c = md5hh(c, d, a, b, x[3], 16, -722521979); b = md5hh(b, c, d, a, x[6], 23, 76029189); a = md5hh(a, b, c, d, x[9], 4, -640364487); d = md5hh(d, a, b, c, x[12], 11, -421815835); c = md5hh(c, d, a, b, x[15], 16, 530742520); b = md5hh(b, c, d, a, x[2], 23, -995338651); a = md5ii(a, b, c, d, x[0], 6, -198630844); d = md5ii(d, a, b, c, x[7], 10, 1126891415); c = md5ii(c, d, a, b, x[14], 15, -1416354905); b = md5ii(b, c, d, a, x[5], 21, -57434055); a = md5ii(a, b, c, d, x[12], 6, 1700485571); d = md5ii(d, a, b, c, x[3], 10, -1894986606); c = md5ii(c, d, a, b, x[10], 15, -1051523); b = md5ii(b, c, d, a, x[1], 21, -2054922799); a = md5ii(a, b, c, d, x[8], 6, 1873313359); d = md5ii(d, a, b, c, x[15], 10, -30611744); c = md5ii(c, d, a, b, x[6], 15, -1560198380); b = md5ii(b, c, d, a, x[13], 21, 1309151649); a = md5ii(a, b, c, d, x[4], 6, -145523070); d = md5ii(d, a, b, c, x[11], 10, -1120210379); c = md5ii(c, d, a, b, x[2], 15, 718787259); b = md5ii(b, c, d, a, x[9], 21, -343485551); a0 = safeAdd(a0, a) >>> 0; b0 = safeAdd(b0, b) >>> 0; c0 = safeAdd(c0, c) >>> 0; d0 = safeAdd(d0, d) >>> 0; } this.update = function (bytes) { if (!bytes || !bytes.length) return; byteLen += bytes.length; for (let i = 0; i < bytes.length; i++) { buf[bufLen++] = bytes[i] & 0xff; if (bufLen === 64) { const w = new Array(16); for (let j = 0; j < 16; j++) w[j] = (buf[j * 4] | (buf[j * 4 + 1] << 8) | (buf[j * 4 + 2] << 16) | (buf[j * 4 + 3] << 24)) >>> 0; // MD5 小端字节序 round(w); bufLen = 0; } } }; this.hex = function () { const totalBits = byteLen * 8; // MD5 64 位长度(little-endian):注意 JS 位移量按 mod 32 处理,高 32 位必须显式取 const lenLo = totalBits >>> 0; const lenHi = Math.floor(totalBits / 0x100000000) >>> 0; buf[bufLen++] = 0x80; while (bufLen % 64 !== 56) buf[bufLen++] = 0; for (let i = 0; i < 4; i++) buf[bufLen++] = (lenLo >>> (i * 8)) & 0xff; for (let i = 0; i < 4; i++) buf[bufLen++] = (lenHi >>> (i * 8)) & 0xff; for (let off = 0; off < bufLen; off += 64) { const w = new Array(16); for (let j = 0; j < 16; j++) w[j] = (buf[off + j * 4] | (buf[off + j * 4 + 1] << 8) | (buf[off + j * 4 + 2] << 16) | (buf[off + j * 4 + 3] << 24)) >>> 0; // MD5 小端字节序 round(w); } // MD5 输出:每个 word 低字节在前(little-endian hex) return [a0, b0, c0, d0].map(v => { let s = ''; for (let i = 0; i < 4; i++) s += ('0' + (((v >>> (i * 8)) & 0xff)).toString(16)).slice(-2); return s; }).join(''); }; } /** * 通过 Range 分块下载流式计算 md5(大文件 CDN 无 content-md5 头时的回退;内存恒定)。 * downloadFn(offset, length) → Uint8Array;返回 md5 hex 或 ''(失败)。 */ async function computeMd5ByRange(downloadFn, byteSize, onProgress) { const n = Number(byteSize) || 0; if (n <= 0) return ''; const md5 = new Md5Stream(); const CHUNK = 512 * 1024; let offset = 0; try { while (offset < n) { const len = Math.min(CHUNK, n - offset); const buf = await downloadFn(offset, len); if (!buf || !buf.length) return ''; md5.update(buf); offset += len; if (onProgress) onProgress(Math.min(99, Math.round(offset / n * 100))); } } catch (e) { return ''; } if (onProgress) onProgress(100); return md5.hex(); } /** 导出模态框内确认:是否下载计算无服务器哈希的文件(返回 Promise) */ function confirmBigDownloadModal(modal, statusEl, count, totalBytes) { return new Promise(resolve => { try { setStatus(statusEl, `${count} 个文件服务器未提供 md5(多为超过分片阈值的大文件,CDN 无哈希头),可下载计算(共 ${formatSize(totalBytes)},需完整下载全部内容,较慢)。`, 'warn'); modal.footerEl.innerHTML = ` `; modal.footerEl.querySelector('[data-act="skip"]').onclick = () => resolve(false); modal.footerEl.querySelector('[data-act="dl"]').onclick = () => resolve(true); } catch (e) { resolve(false); } }); } /** 字节数组 → md5 hex(原始字节串) */ function baiduBytesToMd5Hex(bytes) { let bin = ''; const step = 0x8000; for (let i = 0; i < bytes.length; i += step) { bin += String.fromCharCode.apply(null, bytes.subarray(i, i + step)); } return BAIDU.md5Hex(bin); } // ============================== UI(参照 123FastLink 风格)============================== const STYLES = ` :root{ --gy-primary:#10b981;--gy-primary-hover:#059669; --gy-danger:#ef4444;--gy-danger-hover:#dc2626; --gy-warning:#f59e0b;--gy-warning-hover:#d97706; --gy-info:#3b82f6;--gy-info-hover:#2563eb; --gy-bg:#ffffff;--gy-surface:#f8fafc;--gy-border:#e2e8f0; --gy-text:#1e293b;--gy-text2:#64748b;--gy-text3:#94a3b8; --gy-radius:12px;--gy-shadow:0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -2px rgba(0,0,0,0.05); } .gy-ms-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:2147483646;animation:gy-ms-fade .18s ease-out} .gy-ms-modal{background:var(--gy-bg);border-radius:var(--gy-radius);box-shadow:var(--gy-shadow);width:90%;max-width:560px;max-height:88vh;overflow:hidden;border:1px solid var(--gy-border);animation:gy-ms-slide .22s cubic-bezier(0.4,0,0.2,1)} .gy-ms-header{padding:18px 22px 14px;border-bottom:1px solid var(--gy-border);display:flex;align-items:center;justify-content:space-between} .gy-ms-title{font-size:17px;font-weight:600;color:var(--gy-text);display:flex;align-items:center;gap:8px} .gy-ms-title svg{width:18px;height:18px;color:var(--gy-primary)} .gy-ms-close{background:none;border:none;height:30px;width:30px;display:flex;align-items:center;justify-content:center;color:var(--gy-text2);cursor:pointer;border-radius:6px;font-size:18px;line-height:1} .gy-ms-close:hover{background:var(--gy-surface);color:var(--gy-text)} .gy-ms-content{padding:18px 22px;overflow:auto;max-height:calc(88vh - 120px)} .gy-ms-footer{padding:12px 22px 18px;border-top:1px solid var(--gy-border);display:flex;gap:10px;justify-content:flex-end;align-items:center;flex-wrap:wrap} .gy-ms-btn{padding:8px 18px;border-radius:8px;border:none;cursor:pointer;font-size:13px;font-weight:500;transition:all .15s} .gy-ms-btn-primary{background:var(--gy-primary);color:#fff} .gy-ms-btn-primary:hover{background:var(--gy-primary-hover)} .gy-ms-btn-secondary{background:#e6f4ff;color:#1677ff;border:1px solid #91caff} .gy-ms-btn-outline{background:#fff;border:1px solid var(--gy-border);color:var(--gy-text)} .gy-ms-btn-outline:hover{background:var(--gy-surface)} .gy-ms-btn-danger{background:var(--gy-danger);color:#fff} .gy-ms-btn:disabled{opacity:.6;cursor:not-allowed} .gy-ms-status{font-size:12px;min-height:18px;white-space:pre-wrap;margin-top:8px} .gy-ms-status-ok{color:#16a34a} .gy-ms-status-err{color:#dc2626} .gy-ms-status-warn{color:#d97706} .gy-ms-status-info{color:#2563eb} .gy-ms-textarea{width:100%;box-sizing:border-box;min-height:150px;padding:10px;border:1px solid #d9d9d9;border-radius:8px;font-size:12px;font-family:ui-monospace,monospace;resize:vertical;color:var(--gy-text)} .gy-ms-pre{background:#f7f7f7;padding:12px;border-radius:8px;font-family:ui-monospace,monospace;font-size:12px;white-space:pre-wrap;word-break:break-all;margin:0;max-height:280px;overflow:auto} .gy-ms-toast{position:fixed;top:24px;left:50%;transform:translateX(-50%);z-index:2147483647;padding:10px 22px;border-radius:10px;color:#fff;font-size:13px;box-shadow:0 6px 20px rgba(0,0,0,.25);animation:gy-ms-fade .2s ease-out;max-width:80vw;word-break:break-all} .gy-ms-toast-success{background:#16a34a} .gy-ms-toast-error{background:#dc2626} .gy-ms-toast-warning{background:#d97706} .gy-ms-toast-info{background:#2563eb} .gy-ms-file-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:10px 0 8px;font-size:12px;color:var(--gy-text2)} .gy-ms-bar{height:6px;border-radius:3px;background:var(--gy-surface);overflow:hidden;margin-top:10px} .gy-ms-bar-fill{height:100%;background:var(--gy-primary);border-radius:3px;transition:width .2s} @keyframes gy-ms-fade{from{opacity:0}to{opacity:1}} @keyframes gy-ms-slide{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}} `; const ICONS = { bolt: ``, generate: ``, import: ``, settings: ``, }; // ---- 模态框基础 ---- function openModal({ title, icon, content, footer, width }) { closeAllModals(); const overlay = document.createElement('div'); overlay.className = 'gy-ms-overlay'; overlay.innerHTML = `
${icon || ''}${title}
`; document.body.appendChild(overlay); const contentEl = overlay.querySelector('.gy-ms-content'); const footerEl = overlay.querySelector('.gy-ms-footer'); if (typeof content === 'function') content(contentEl, overlay); else if (content) contentEl.innerHTML = content; if (typeof footer === 'function') footer(footerEl, overlay); else if (footer) footerEl.innerHTML = footer; overlay.querySelector('.gy-ms-close').onclick = () => overlay.remove(); overlay.addEventListener('mousedown', e => { if (e.target === overlay) overlay.remove(); }); return { overlay, contentEl, footerEl }; } function closeAllModals() { document.querySelectorAll('.gy-ms-overlay').forEach(o => o.remove()); } function showToast(message, type = 'success', duration = 2600) { const t = document.createElement('div'); t.className = `gy-ms-toast gy-ms-toast-${type}`; t.textContent = message; document.body.appendChild(t); setTimeout(() => t.remove(), duration); } function setStatus(el, text, type = 'info') { if (!el) return; el.textContent = text; el.className = `gy-ms-status gy-ms-status-${type}`; } function updateProgress(el, percent, text) { if (!el) return; const fill = el.querySelector('.gy-ms-bar-fill'); if (fill) fill.style.width = `${Math.max(0, Math.min(100, percent))}%`; const label = el.querySelector('.gy-ms-bar-label'); if (label) label.textContent = text || ''; } // ---- 平台适配:获取勾选项与目录遍历 ---- function getPlatformApi() { return isGuangya() ? new GuangyaApi() : null; } function getCheckedItems() { if (isGuangya()) return guangyaGetCheckedItems(); if (isXunlei()) return xunleiGetCheckedItems(); if (isBaidu()) { const s = BAIDU.getCheckedItems(); return { all: s.headState === 'checked', items: s.ids, headState: s.headState }; } if (isAliyun()) return ALIYUN.getCheckedItems(); if (isQuark()) return QUARK.getCheckedItems(); if (isUc()) return UC.getCheckedItems(); if (isWeiyun()) { // 微云为异步(DOM checked → API 匹配),同步包装为 Promise 由调用方 await return WEIYUN.getCheckedItems(); } if (isFeejii()) return FEEJII.getCheckedItems(); if (isCtfile()) { const s = ctfileGetCheckedItems(); return { all: s.all, items: s.names.map(name => ({ name })) }; } if (isY139()) return y139GetCheckedItems(); if (isP123()) return p123GetCheckedItems(); return { all: false, items: [] }; } function getLastParentId() { if (isGuangya()) return guangyaGetLastParentId(); if (isXunlei()) return xunleiGetLastParentId(); if (isBaidu()) return BAIDU.getCurrentDir(); if (isAliyun()) return ALIYUN.getCurrentParentId(); if (isQuark()) return QUARK.getCurrentParentId(); if (isUc()) return UC.getCurrentParentId(); if (isWeiyun()) return WEIYUN.getLastParentId(); if (isFeejii()) return FEEJII.getCurrentParentId(); if (isCtfile()) return CTFILE.getCurrentFolderId(); if (isY139()) return y139GetCurrentFolderId(); if (isP123()) return p123GetCurrentParentId(); return ''; } /** 统一:获取文件详情(md5 + gcid + size);迅雷条目哈希缺失时调详情接口补齐 */ async function getFileDetailPlatform(api, item) { if (isGuangya()) return api.getFileDetail(item.fileId); if (isAliyun()) { // 阿里云盘列表条目已含 content_hash(SHA1) + size,无需额外接口 const sha1 = String(item.contentHash || item.content_hash || '').toUpperCase(); return { md5: '', gcid: '', sha1: /^[0-9A-F]{40}$/.test(sha1) ? sha1 : '', fileSize: Number(item.size || 0) }; } if (isXunlei()) { const md5 = item.md5_checksum ? String(item.md5_checksum).toLowerCase() : ''; const gcid = item.hash ? normalizeGcid(item.hash) : ''; const size = Number(item.size || 0); // drive.all 条目通常不带 md5/hash(字段为空),需要调详情接口补齐 if (!md5 && !gcid) { try { const d = await xunleiGetFileDetail(item.id || item.fileId); return { md5: d.md5 || '', gcid: d.gcid || '', fileSize: d.fileSize || size, }; } catch (e) { // 详情失败时返回原始字段(可能只有 size) return { md5, gcid, fileSize: size }; } } return { md5, gcid, fileSize: size }; } if (isQuark()) { // 夸克列表条目无 hash,需批量调 download 接口(由调用方按批取,此处单条兜底) const md5 = String(item.md5 || item.md5_checksum || '').toLowerCase(); const sha1 = String(item.sha1 || item.content_hash || '').toLowerCase(); if (!md5 && !sha1 && item.fid) { try { const map = await QUARK.getFileHashes([item.fid]); const h = map[String(item.fid)] || {}; return { md5: h.md5 || '', sha1: h.sha1 || '', gcid: '', fileSize: Number(h.size || item.size || 0) }; } catch (e) { return { md5, sha1, gcid: '', fileSize: Number(item.size || 0) }; } } return { md5, sha1, gcid: '', fileSize: Number(item.size || 0) }; } if (isWeiyun()) { // 微云列表条目已含 file_sha(完整SHA1) + size;file_md5 恒为全 0(微云不存 MD5) const sha1 = String(item.sha1 || item.file_sha || '').toLowerCase(); return { md5: '', sha1: /^[0-9a-f]{40}$/.test(sha1) ? sha1 : '', gcid: '', fileSize: Number(item.size || 0) }; } if (isCtfile()) { // 城通列表条目 file_real_name="{md5(前1MB)}-{size}";≤1MB 即完整内容 MD5,>1MB 仅前1MB 哈希 const meta = CTFILE.parseItemMeta(item); const size = meta.size || Number(item.size || 0); return { md5: size <= CTFILE.CHECKSUM_SLICE ? meta.checksum : '', ctChecksum: meta.checksum, gcid: '', fileSize: size }; } return { md5: '', gcid: '', fileSize: 0 }; } /** 统一:判断条目是否为目录 */ function isDirPlatform(item) { if (isGuangya()) return Number(item.resType) === 2; // 光鸭目录标记 resType===2 if (isXunlei()) return String(item.kind) === 'drive#folder'; // 迅雷目录 kind===drive#folder if (isAliyun()) return String(item.type) === 'folder' || String(item.type) === 'backupfolder'; // 阿里目录 type===folder if (isQuark()) return item.isDir === true || item.dir === true; // 夸克目录 dir===true if (isUc()) return item.isDir === true || item.dir === true; if (isWeiyun()) return String(item.kind) === 'dir'; // 微云目录 kind==='dir' if (isFeejii()) return Number(item.fileType) === 2; // 小飞机目录 fileType===2 if (isCtfile()) return String(item.ext) === 'folder'; // 城通目录 ext==='folder' return false; } /** 统一:获取目录子项 */ async function getListPlatform(api, folderId, page) { if (isGuangya()) return { list: await api.getFileList(folderId, page), next: '' }; if (isAliyun()) { const driveId = await ALIYUN.getDriveId(); const r = await ALIYUN.getFileList(driveId, folderId, page || ''); return { list: r.list, next: r.nextMarker }; } if (isQuark()) { const r = await QUARK.getFileList(folderId, page || 1); // 规范化条目:与统一 item 形态对齐 const list = (r.list || []).map(it => ({ fileId: String(it.fid || ''), fid: String(it.fid || ''), name: String(it.file_name || ''), fileName: String(it.file_name || ''), isDir: it.dir === true, type: it.dir === true ? 'folder' : 'file', size: Number(it.size || 0), md5: String(it.md5 || ''), sha1: String(it.sha1 || ''), })); return { list, next: r.hasMore ? (Number(page || 1) + 1) : '' }; } if (isXunlei()) { const r = await xunleiGetFileList(folderId, page || ''); return { list: r.list, next: r.nextPageToken }; } if (isWeiyun()) { const { dirs, files, finishFlag } = await WEIYUN.getFileList(folderId, 0, 500); const list = [...dirs, ...files]; return { list, next: finishFlag ? '' : (Number(page || 0) + 500) }; } if (isFeejii()) { const r = await FEEJII.getFileList(folderId, page || 1); const list = (r.list || []).map(it => ({ fileId: String(it.fileId || ''), fileName: String(it.fileName || ''), name: String(it.fileName || ''), isDir: Number(it.fileType) === 2, type: Number(it.fileType) === 2 ? 'folder' : 'file', fileType: Number(it.fileType), size: Number(it.fileSize || 0), folderId: String(it.folderId || ''), })); return { list, next: r.hasMore ? (Number(page || 1) + 1) : '' }; } if (isCtfile()) { const wsId = await CTFILE.getWorkspaceId(); const r = await CTFILE.getFileList(wsId, folderId, page || 0); const list = (r.list || []).map(it => ({ fileId: String(it.fid || ''), fid: String(it.fid || ''), name: String(it.name || ''), fileName: String(it.name || ''), isDir: String(it.ext) === 'folder', type: String(it.ext) === 'folder' ? 'folder' : 'file', ext: String(it.ext || ''), size: Number(it.size || 0), file_real_name: String(it.file_real_name || ''), })); return { list, next: '' }; } return { list: [], next: '' }; } // ---- 生成秒传 JSON(导出,双哈希:MD5 + GCID)---- async function generateJson() { const { items, all } = getCheckedItems(); if (!items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; } if (isXunlei() && !(await xunleiGetCredentials()).accessToken) { showToast('未检测到迅雷云盘登录态,请先刷新页面', 'warning'); return; } const api = getPlatformApi(); const modal = openModal({ title: '生成秒传 JSON', icon: ICONS.generate, width: 620, content(el) { el.innerHTML = `
准备中...
`; }, footer() { return ''; }, }); const statusEl = modal.contentEl.querySelector('#gy-ms-gen-status'); const barEl = modal.contentEl.querySelector('.gy-ms-bar'); const files = []; const warnMissing = []; let done = 0; let total = items.length; const collectItem = async (item, prefix) => { try { const detail = await getFileDetailPlatform(api, item); const md5 = detail.md5; const gcid = detail.gcid; if (md5 || gcid) { files.push({ path: prefix + (item.fileName || item.name || ''), etag: md5 || '', gcid: gcid || '', size: detail.fileSize || Number(item.fileSize || item.size || 0), }); } else { warnMissing.push(prefix + (item.fileName || item.name || '')); } } catch (e) { warnMissing.push(`${prefix}${item.fileName || item.name || ''}(${e.message})`); } }; const walkFolder = async (folderId, prefix) => { let page = ''; for (;;) { let list, next; try { const r = await getListPlatform(api, folderId, page); list = r.list; next = r.next; } catch (e) { setStatus(statusEl, `获取目录失败:${prefix} ${e.message}`, 'err'); return; } for (const it of list) { if (isDirPlatform(it)) { await walkFolder(it.fileId || it.id, prefix + (it.fileName || it.name) + '/'); } else { await collectItem(it, prefix); done++; updateProgress(barEl, Math.round((done / Math.max(1, total)) * 100), `获取文件信息 ${done}/${total}:${it.fileName || it.name}`); await sleep(CONFIG.md5Delay); } } if (isXunlei()) { if (!next) break; page = next; } else { if (list.length < CONFIG.fileListPageSize) break; page = Number(page || 0) + 1; } await sleep(CONFIG.listPageDelay); } }; try { const platformName = isGuangya() ? '光鸭' : '迅雷'; setStatus(statusEl, all ? `已全选当前目录 ${items.length} 项,正在获取文件信息...` : `正在获取 ${items.length} 项的文件信息...`, 'info'); for (const item of items) { if (isDirPlatform(item)) { await walkFolder(item.fileId || item.id, (item.fileName || item.name) + '/'); } else { if (!(item.fileId || item.id)) { warnMissing.push(`${item.fileName || item.name}(未能匹配到文件ID)`); done++; continue; } await collectItem(item, ''); done++; updateProgress(barEl, Math.round((done / Math.max(1, total)) * 100), `获取文件信息 ${done}/${total}:${item.fileName || item.name}`); await sleep(CONFIG.md5Delay); } } if (!files.length) { setStatus(statusEl, '未获取到任何文件的哈希(MD5/GCID),无法生成 JSON。', 'err'); modal.footerEl.innerHTML = ``; return; } const totalSize = files.reduce((s, f) => s + (Number(f.size) || 0), 0); const json = { scriptVersion: SCRIPT_VERSION, platform: platformName, totalFilesCount: files.length, totalSize, formattedTotalSize: formatSize(totalSize), files, }; const jsonStr = JSON.stringify(json, null, 2); const exportName = CONFIG.defaultExportName + '.json'; const dual = files.filter(f => f.etag && f.gcid).length; modal.contentEl.innerHTML = `
已生成 ${files.length} 项秒传 JSON(${dual} 项双哈希 MD5+GCID)${warnMissing.length ? `;${warnMissing.length} 项未获取到哈希` : ''}
${escapeHtml(jsonStr)}
`; modal.footerEl.innerHTML = ` `; modal.footerEl.querySelector('[data-act="copy"]').onclick = () => { try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); } catch (e) { const ta = document.createElement('textarea'); ta.value = jsonStr; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); showToast('秒传 JSON 已复制到剪贴板'); } }; modal.footerEl.querySelector('[data-act="download"]').onclick = () => { const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = exportName; a.click(); URL.revokeObjectURL(url); }; modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); } catch (e) { setStatus(statusEl, `生成失败:${e.message}`, 'err'); modal.footerEl.innerHTML = ``; } } function escapeHtml(str) { return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } // ---- 百度导出(生成秒传 JSON,含 contentMd5 + 云服务器注册)---- async function baiduGenerateJson() { const bdstoken = BAIDU.getBdstoken(); if (!bdstoken) { showToast('未能获取百度 bdstoken,请刷新百度网盘页面后重试', 'warning'); return; } // 需要下载凭证 token(dlink 下载/嵌入字节/云注册依赖) await BAIDU.ensureAccessToken(); const sel = BAIDU.getCheckedItems(); const currentDir = BAIDU.getCurrentDir(); let fsIds = sel.ids.slice(); if (sel.headState === 'checked' && !fsIds.length) { showToast('检测到全选,正在拉取当前目录全部条目...', 'info', 2000); let page = 1; try { for (;;) { const r = await BAIDU.listDir(currentDir, page); for (const it of r.list) fsIds.push(String(it.fs_id)); if (!r.hasMore) break; page++; await sleep(CONFIG.listPageDelay); } } catch (e) { showToast(`拉取目录失败:${e.message}`, 'error'); return; } } fsIds = Array.from(new Set(fsIds)); if (!fsIds.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; } const modal = openModal({ title: '生成秒传 JSON(百度网盘)', icon: ICONS.generate, width: 620, content(el) { el.innerHTML = `
准备中...
`; }, footer() { return ''; }, }); const statusEl = modal.contentEl.querySelector('#gy-ms-bd-gen-status'); const barEl = modal.contentEl.querySelector('.gy-ms-bar'); const files = []; const warnMissing = []; try { setStatus(statusEl, `正在获取 ${fsIds.length} 项的文件信息...`, 'info'); const metas = await BAIDU.getFileMetas(fsIds); const folderMetas = []; for (const id of fsIds) { const m = metas[id]; if (!m) { warnMissing.push(`fs_id=${id}(未取到元数据)`); continue; } if (m.isdir) { folderMetas.push(m); } else { const md5 = m.md5; if (md5 && /^[0-9a-f]{32}$/.test(md5)) { const entry = { path: String(m.path || m.name).replace(/^\//, ''), etag: md5, contentMd5: BAIDU.encryptMd5(md5), size: m.size, fsId: m.fsId || '' }; // 大文件(分块上传)md5 字段可能非内容哈希:尝试从 dlink 响应头探测真实 md5 覆盖 // (注:小巧数据不内嵌进 JSON——JSON 需保持轻量可发社交平台,验证块一律走云服务器) if (Number(m.size) > 262144 && m.fsId) { try { const dl = await BAIDU.getDlink(m.fsId, m.path); if (dl) { const realMd5 = await BAIDU.probeDlinkMd5(dl); if (realMd5 && /^[0-9a-f]{32}$/.test(realMd5) && realMd5 !== entry.etag) { entry.etag = realMd5; entry.contentMd5 = BAIDU.encryptMd5(realMd5); entry.md5Fixed = true; setStatus(statusEl, `已从下载响应头修复真实 MD5:${m.name}`, 'info'); } } } catch (e) { /* 探测失败则保留 filemetas 值 */ } await sleep(CONFIG.md5Delay); } files.push(entry); } else { warnMissing.push((m.path || m.name) + ':未能取得合法 MD5'); } } setStatus(statusEl, `已处理 ${files.length} 项...`, 'info'); await sleep(CONFIG.md5Delay); } const counter = { done: 0 }; for (const f of folderMetas) { setStatus(statusEl, `正在扫描文件夹:${f.path}...`, 'info'); await baiduWalkFolder(f.path, files, warnMissing, statusEl, barEl, counter); } if (!files.length) { setStatus(statusEl, '未获取到任何文件的哈希(MD5),无法生成 JSON。', 'err'); modal.footerEl.innerHTML = ``; return; } const totalSize = files.reduce((s, f) => s + (Number(f.size) || 0), 0); const bigCount = files.filter(f => (Number(f.size) || 0) > 4 * 1024 * 1024).length; // ---- 云块服务器注册(仅「同意提供令牌」时;仅导出模式不采集令牌)---- const cloudNotes = []; let tracker = null; if (!CONFIG.cloudServer || !CONFIG.cloudRegister) { cloudNotes.push('未配置云服务器或未启用注册'); } else if (!BAIDU.cloudConsentEnabled()) { mcLog('export', '仅导出模式:不采集令牌,跳过云注册'); } else { try { await BAIDU.ensureAccessToken(); setStatus(statusEl, '正在注册云块服务器...', 'info'); const token = BAIDU.getAccessTokenCached(); const reg = await BAIDU.cloudRegister(CONFIG.cloudServer, token, files); if (reg && reg.ok) { tracker = { cloud: { server: CONFIG.cloudServer } }; mcLog('export', '云服务器注册成功,' + (reg.registered != null ? reg.registered + ' 个文件归并' : '')); } else { cloudNotes.push('云服务器注册失败(' + (reg && reg.err || '未知错误') + ')'); mcLog('export', 'ERROR 云注册失败:' + (reg && reg.err)); } } catch (e) { cloudNotes.push('云服务器注册异常(' + e.message + ')'); mcLog('export', 'ERROR 云注册异常:' + e.message); } } const json = { scriptVersion: SCRIPT_VERSION, platform: '百度', totalFilesCount: files.length, totalSize, formattedTotalSize: formatSize(totalSize), tracker, files, }; const jsonStr = JSON.stringify(json, null, 2); const exportName = CONFIG.defaultExportName + '_百度.json'; modal.contentEl.innerHTML = `
已生成 ${files.length} 项秒传 JSON${warnMissing.length ? `;${warnMissing.length} 项未获取到哈希` : ''}${cloudNotes.length ? `;${cloudNotes.length} 项云注册未成功` : ''}
${tracker ? `
云块服务器已注册,导入者可经服务器取验证块。
` : ''} ${cloudNotes.length ? `
${cloudNotes.map(n => '· ' + n).join('\n')}
` : ''}
${escapeHtml(jsonStr)}
`; modal.footerEl.innerHTML = ` `; modal.footerEl.querySelector('[data-act="copy"]').onclick = () => { try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); } catch (e) { const ta = document.createElement('textarea'); ta.value = jsonStr; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); showToast('秒传 JSON 已复制到剪贴板'); } }; modal.footerEl.querySelector('[data-act="download"]').onclick = () => { const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = exportName; a.click(); URL.revokeObjectURL(url); }; modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); // 导出日志按钮(本地,便于排查问题) try { const logBtn = document.createElement('button'); logBtn.className = 'gy-ms-btn gy-ms-btn-outline'; logBtn.textContent = '导出日志'; logBtn.style.marginLeft = '8px'; logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); }; modal.footerEl.appendChild(logBtn); } catch (e) { /* ignore */ } } catch (e) { setStatus(statusEl, `生成失败:${e.message}`, 'err'); modal.footerEl.innerHTML = ``; } } /** 递归收集目录下所有文件(path 用服务器绝对路径,etag 为解密 md5) */ async function baiduWalkFolder(dir, files, warnMissing, statusEl, barEl, counter) { let page = 1; for (;;) { let list, hasMore; try { const r = await BAIDU.listDir(dir, page); list = r.list; hasMore = r.hasMore; } catch (e) { setStatus(statusEl, `列出目录失败:${dir}(${e.message})`, 'err'); return; } for (const it of list) { if (Number(it.isdir) === 1) { await baiduWalkFolder(it.path, files, warnMissing, statusEl, barEl, counter); } else { const md5 = BAIDU.decryptMd5(it.md5 || ''); const name = String(it.server_filename || ''); if (md5 && /^[0-9a-f]{32}$/.test(md5)) { files.push({ path: String(it.path || name).replace(/^\//, ''), etag: md5, contentMd5: BAIDU.encryptMd5(md5), size: Number(it.size || 0), fsId: String(it.fs_id || '') }); } else { warnMissing.push(String(it.path || name) + ':未能取得合法 MD5'); } counter.done++; if (statusEl) setStatus(statusEl, `正在扫描:${dir}(已收集 ${files.length} 个文件)`, 'info'); if (barEl) updateProgress(barEl, 0, `已收集 ${files.length} 个文件:${name}`); await sleep(CONFIG.md5Delay); } } if (!hasMore) break; page++; await sleep(CONFIG.listPageDelay); } } /** 百度云服务器导入(协作秒传主流程) */ async function baiduCloudImportRow(row, cloudServer, uk, currentDir, onProgress) { const SIZE256 = 262144; const bdstoken = BAIDU.getBdstoken(); const dataTime = Math.floor(Date.now() / 1000); const size = row.size; const offset = size <= SIZE256 ? 0 : BAIDU.computeDataOffset(uk, row.contentMd5, dataTime, size); mcLog('import', '云分支:size=' + size + ' offset=' + offset + ' contentMd5=' + row.contentMd5); // 经云服务器取验证块(服务器按 md5 查池、轮询凭据拉块) mcLog('import', '经 ' + cloudServer + ' 取验证块'); let sliceBytes = null; if (offset !== 0 && size > SIZE256) { if (onProgress) onProgress('正在经云服务器取 slice 块...'); sliceBytes = await BAIDU.cloudBlock(cloudServer, row.contentMd5, 0, SIZE256); if (!sliceBytes || sliceBytes.length < SIZE256) { mcLog('import', 'ERROR 云服务器 slice 块获取失败'); return { ok: false, errno: -1, reason: '云服务器获取 slice 块失败(该 md5 未注册或凭据不可用)' }; } } if (onProgress) onProgress('正在经云服务器取验证块...'); let contentBytes = await BAIDU.cloudBlock(cloudServer, row.contentMd5, offset, SIZE256); if (!contentBytes && sliceBytes && offset === 0) contentBytes = sliceBytes; if (!contentBytes || contentBytes.length < SIZE256) { mcLog('import', 'ERROR 云服务器验证块获取失败(offset=' + offset + ' len=' + (contentBytes && contentBytes.length) + ')'); return { ok: false, errno: -1, reason: '云服务器获取验证块失败(该 md5 无人注册或凭据不可用)' }; } contentBytes = contentBytes.subarray(0, SIZE256); mcLog('import', '云服务器取块成功(slice=' + (sliceBytes ? sliceBytes.length : 0) + ' content=' + contentBytes.length + ')'); const sliceMd5 = size <= SIZE256 ? row.contentMd5 : BAIDU.encryptMd5(baiduBytesToMd5Hex(sliceBytes || contentBytes)); // rapidupload 秒传(目标路径 = 当前目录 + 原文件名,与同账号导入一致) const targetPath = (currentDir === '/' || !currentDir ? '/' : String(currentDir).replace(/\/+$/, '') + '/') + String(row.path || '').replace(/^\/+/, ''); const pcBody = new URLSearchParams({ path: targetPath, autoinit: '1', block_list: JSON.stringify(['00000000000000000000000000000000', '11111111111111111111111111111111']), target_path: '/', local_mtime: String(Math.floor(Date.now() / 1000)) }); const pcr = await fetch(BAIDU.API_BASE + '/api/precreate?rtype=1&bdstoken=' + encodeURIComponent(bdstoken) + '&app_id=' + BAIDU.APP_ID + '&channel=' + BAIDU.CHANNEL + '&web=1&clienttype=0&dp-logid=' + Date.now(), { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': location.origin + '/disk/main' }, body: pcBody.toString() }); const pcj = await pcr.json(); if (!pcj.uploadid) { mcLog('import', 'ERROR 云模式 precreate 失败 errno=' + pcj.errno); return { ok: false, errno: pcj.errno, reason: 'precreate 失败' }; } let bin = ''; for (let i = 0; i < contentBytes.length; i += 0x8000) bin += String.fromCharCode.apply(null, contentBytes.subarray(i, i + 0x8000)); const ruBody = new URLSearchParams({ uploadid: pcj.uploadid, path: targetPath, 'content-length': String(size), 'content-md5': row.contentMd5, 'slice-md5': sliceMd5, target_path: '/', local_mtime: String(Math.floor(Date.now() / 1000)), data_time: String(dataTime), data_offset: String(offset), data_content: btoa(bin), }); const rur = await fetch(BAIDU.API_BASE + '/api/rapidupload?rtype=1&bdstoken=' + encodeURIComponent(bdstoken) + '&app_id=' + BAIDU.APP_ID + '&channel=' + BAIDU.CHANNEL + '&web=1&clienttype=0&dp-logid=' + Date.now(), { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': location.origin + '/disk/main' }, body: ruBody.toString() }); let ruj = null; try { ruj = await rur.json(); } catch (e) { /* ignore */ } mcLog('import', '云模式 rapidupload 结果 errno=' + (ruj && ruj.errno)); // 成功 → 滚雪球:定位秒传后的文件 fsId,把当前账号凭据加入云池(后续导入者可复用) if (ruj && ruj.errno === 0) { try { const myFsId = await BAIDU.getFsidByPath(targetPath); if (myFsId) { BAIDU.cloudPool(cloudServer, BAIDU.getAccessTokenCached(), row.contentMd5, myFsId).catch(() => {}); mcLog('import', '云模式秒传成功,凭据已入池(fsId=' + myFsId + ')'); } else { mcLog('import', '云模式秒传成功,但未定位到新文件 fsId'); } } catch (e) { mcLog('import', '云模式入池异常:' + e.message); } } return { ok: ruj && ruj.errno === 0, errno: ruj && ruj.errno, reason: ruj && ruj.errno === 0 ? '' : ('errno=' + ruj.errno) }; } // ---- 百度导入(同账号 dlink / 嵌入数据 / 云服务器协作)---- async function baiduImportJson(rawJson, onProgress) { if (!BAIDU.cloudConsentEnabled()) { throw new Error('已选择「仅导出」模式:不采集令牌,导入功能已禁用(设置里可切换提供令牌恢复)'); } let obj; try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); } const rows = []; for (const f of (obj.files || [])) { const path = String(f.path || f.name || '').trim(); const md5 = normalizeMd5(f.etag || f.md5); const size = Number(f.size != null ? f.size : 0); if (!path || !md5) continue; rows.push({ path, md5, contentMd5: String(f.contentMd5 || '').trim() || BAIDU.encryptMd5(md5), size, }); } if (!rows.length) throw new Error('JSON 中没有可导入的文件条目'); mcLog('import', '开始导入,共 ' + rows.length + ' 条,当前目录 ' + (BAIDU.getCurrentDir ? BAIDU.getCurrentDir() : '/')); const currentDir = BAIDU.getCurrentDir(); const bdstoken = BAIDU.getBdstoken(); if (!bdstoken) throw new Error('未能获取百度 bdstoken,请刷新页面后重试'); const uk = await BAIDU.getUk(); if (!uk) throw new Error('未能获取当前账号 uk(oper_id),请刷新页面后重试'); await BAIDU.ensureAccessToken(); // 同账号 dlink 下载需要 access_token mcLog('import', 'uk=' + uk + ' token=' + (BAIDU.getAccessTokenCached ? '有' : '无')); const cloudServer = (obj && obj.tracker && obj.tracker.cloud && obj.tracker.cloud.server) ? String(obj.tracker.cloud.server) : (CONFIG.cloudServer || ''); if (cloudServer) mcLog('import', '云服务器:' + cloudServer); const SIZE256 = 262144; let success = 0, fail = 0, skipped = 0; const failures = []; const skippedDetail = []; const total = rows.length; async function locateSource(pathStr) { const p = '/' + String(pathStr || '').replace(/^\/+/, ''); const idx = p.lastIndexOf('/'); const parent = idx <= 0 ? '/' : p.slice(0, idx); const name = p.slice(idx + 1); let page = 1; for (;;) { let r; try { r = await BAIDU.listDir(parent, page); } catch (e) { return null; } const hit = (r.list || []).find(x => String(x.server_filename) === name && String(x.path) === p); if (hit) return { fsId: String(hit.fs_id), size: Number(hit.size || 0), isdir: Number(hit.isdir) === 1, path: String(hit.path || p) }; if (!r.hasMore) break; page++; await sleep(CONFIG.listPageDelay); } return null; } for (let i = 0; i < rows.length; i++) { const row = rows[i]; onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `百度秒传 ${i + 1}/${total}:${row.path}` }); try { const dataTime = Math.floor(Date.now() / 1000); const size = row.size > 0 ? row.size : 0; const offset = size <= SIZE256 ? 0 : BAIDU.computeDataOffset(uk, row.contentMd5, dataTime, size); let sliceBytes = null; let contentBytes = null; // 取块优先级:云服务器(跨账号)→ 同账号网盘内定位源文件 dlink(同账号/自秒) if (cloudServer) { try { mcLog('import', '进入云导入分支(row=' + row.path + ')'); const res = await baiduCloudImportRow(row, cloudServer, uk, currentDir, (t) => onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: t + `:${row.path}` })); if (res && res.ok) { mcLog('import', '云秒传成功:' + row.path); success++; await sleep(CONFIG.transferDelay); continue; } if (res && res.reason) { skippedDetail.push(`${row.path}:云秒传未成(${res.reason})`); mcLog('import', '云秒传未成:' + res.reason); } } catch (e) { mcLog('import', 'ERROR 云秒传异常:' + e.message); skippedDetail.push(`${row.path}:云秒传异常(${e.message})`); } } const src = await locateSource(row.path); if (!src || src.isdir) { skipped++; skippedDetail.push(`${row.path}:经云服务器未命中且当前账号云端未找到源文件(跨账号 JSON 无文件本体,无法秒传)`); continue; } const dlink = await BAIDU.getDlink(src.fsId, src.path || row.path); if (!dlink) { fail++; failures.push(`${row.path}:未能获取下载链接`); continue; } const realSize = row.size > 0 ? row.size : src.size; if (realSize <= SIZE256) { sliceBytes = await BAIDU.downloadSlice(dlink, 0, realSize); contentBytes = sliceBytes; } else if (offset === 0) { sliceBytes = await BAIDU.downloadSlice(dlink, 0, SIZE256); contentBytes = sliceBytes; } else { sliceBytes = await BAIDU.downloadSlice(dlink, 0, SIZE256); contentBytes = await BAIDU.downloadSlice(dlink, offset, SIZE256); } const sliceMd5 = size <= SIZE256 ? row.contentMd5 : BAIDU.encryptMd5(baiduBytesToMd5Hex(sliceBytes)); const targetPath = (currentDir === '/' ? '/' : currentDir + '/') + row.path.replace(/^\/+/, ''); const pcBody = new URLSearchParams({ path: targetPath, autoinit: '1', block_list: JSON.stringify(['00000000000000000000000000000000', '11111111111111111111111111111111']), target_path: currentDir === '/' ? '/' : currentDir, local_mtime: String(Math.floor(Date.now() / 1000)) }); const pcr = await fetch(BAIDU.API_BASE + '/api/precreate?rtype=1&bdstoken=' + encodeURIComponent(bdstoken) + '&app_id=' + BAIDU.APP_ID + '&channel=' + BAIDU.CHANNEL + '&web=1&clienttype=0&dp-logid=' + Date.now(), { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': location.origin + '/disk/main' }, body: pcBody.toString() }); const pcj = await pcr.json(); if (!pcj.uploadid) { fail++; failures.push(`${row.path}:precreate 失败(${pcj.errmsg || ('errno=' + pcj.errno)})`); continue; } let bin = ''; for (let j = 0; j < contentBytes.length; j += 0x8000) bin += String.fromCharCode.apply(null, contentBytes.subarray(j, j + 0x8000)); const ruBody = new URLSearchParams({ uploadid: pcj.uploadid, path: targetPath, 'content-length': String(size), 'content-md5': row.contentMd5, 'slice-md5': sliceMd5, target_path: currentDir === '/' ? '/' : currentDir, local_mtime: String(Math.floor(Date.now() / 1000)), data_time: String(dataTime), data_offset: String(offset), data_content: btoa(bin), }); const rur = await fetch(BAIDU.API_BASE + '/api/rapidupload?rtype=1&bdstoken=' + encodeURIComponent(bdstoken) + '&app_id=' + BAIDU.APP_ID + '&channel=' + BAIDU.CHANNEL + '&web=1&clienttype=0&dp-logid=' + Date.now(), { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': location.origin + '/disk/main' }, body: ruBody.toString() }); let ruj = null; try { ruj = await rur.json(); } catch (e) { /* ignore */ } const errno = ruj && ruj.errno; if (errno === 0) { success++; } else if (errno === 404) { fail++; failures.push(`${row.path}:未命中(errno=404)。可能原因:content-md5 与云端不一致(大文件分块上传的 md5 问题),或嵌入字节与源文件不符,或源文件已变动`); } else { fail++; failures.push(`${row.path}:百度秒传未成功(errno=${errno}${ruj && ruj.errmsg ? ',' + ruj.errmsg : ''})`); } } catch (e) { fail++; failures.push(`${row.path}:秒传失败(${e.message})`); } await sleep(CONFIG.transferDelay); } onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` }); mcLog('import', '导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped); if (failures.length) mcLog('import', '失败明细:' + failures.slice(0, 8).join(' | ')); return { total, success, fail, skipped, failures, skippedDetail }; } // ============================== 夸克网盘导出/导入(v1.2) ============================== // 设计要点: // - 导出:列表无 hash → 批量调 /1/clouddrive/file/download 取明文 md5+sha1 // - JSON 轻量:{path, sha1, md5, hashName:"sha1", etag:"", size},不内嵌文件字节 // - 导入:夸克秒传(upload/pre)**不需要 proof/验证块**——带 sha1+md5 直接命中 finish:true, // PDS 全局去重跨账号可用,因此导入**不需要云服务器**(与阿里云盘不同,这是夸克的最大优势) // - 建目录:/1/clouddrive/file {pdir_fid, file_name};path 中的目录逐级创建 /** 夸克导出:勾选 → 递归列表 → 批量取 hash → 轻量 JSON(含 md5+sha1) */ async function quarkGenerateJson() { const sel = QUARK.getCheckedItems(); if (!sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; } const modal = openModal({ title: '生成秒传 JSON(夸克网盘)', icon: ICONS.generate, width: 620, content(el) { el.innerHTML = `
准备中...
`; }, footer() { return ''; }, }); const statusEl = modal.contentEl.querySelector('#gy-ms-qk-gen-status'); const barEl = modal.contentEl.querySelector('.gy-ms-bar'); const sha1Check = modal.contentEl.querySelector('#gy-ms-qk-gen-sha1'); const wantSha1 = () => sha1Check && sha1Check.checked; const files = []; const warnMissing = []; const counter = { done: 0 }; try { // 递归收集(勾选项:文件直接收,目录递归) const rootParent = QUARK.getCurrentParentId(); setStatus(statusEl, `正在收集 ${sel.items.length} 项...`, 'info'); for (const item of sel.items) { const name = String(item.name || ''); if (!name) { warnMissing.push('(未命名条目)'); continue; } if (item.isDir && item.fid) { setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info'); await quarkWalkFolder(item.fid, name + '/', files, warnMissing, statusEl, barEl, counter); } else if (item.fid) { await quarkCollectFile(item, '', files, warnMissing); counter.done++; updateProgress(barEl, Math.round((counter.done / Math.max(1, sel.items.length)) * 100), `获取文件信息 ${counter.done}/${sel.items.length}:${name}`); await sleep(CONFIG.md5Delay); } else { warnMissing.push(name + ':未能匹配到文件ID'); } } // 批量取 hash(按 fid) const fids = files.filter(f => f.fileId).map(f => f.fileId); const hashMap = await QUARK.getFileHashes(fids); for (const f of files) { const h = hashMap[String(f.fileId)] || {}; if (h.md5 || h.sha1) { f.md5 = h.md5 || ''; f.sha1 = h.sha1 || ''; f.etag = h.md5 || ''; f.size = h.size || f.size || 0; // 有哈希才保留;否则进 warnMissing if (!f.md5 && !f.sha1) warnMissing.push(f.path + ':未能取得哈希'); } else { warnMissing.push(f.path + ':download 接口未返回哈希'); } } // 夸克 download 接口不返回 sha1;勾选后下载文件流式计算真实 SHA1(跨账号秒传的全局键) if (wantSha1()) { const needSha1 = files.filter(f => f.fileId && !f.sha1); if (needSha1.length) { setStatus(statusEl, `正在下载计算 SHA1(${needSha1.length} 个文件)...`, 'info'); for (let si = 0; si < needSha1.length; si++) { const f = needSha1[si]; setStatus(statusEl, `计算 SHA1 ${si + 1}/${needSha1.length}:${f.path}(${formatSize(f.size || 0)})`, 'info'); const sha1 = await QUARK.getFileSha1ByDownload(f.fileId, f.size, (pct) => { updateProgress(barEl, Math.round(pct), `SHA1 ${si + 1}/${needSha1.length}:${f.path} ${pct}%`); }); if (sha1 && /^[0-9a-f]{40}$/.test(sha1)) { f.sha1 = sha1; } else { warnMissing.push(f.path + ':SHA1 计算失败(文件下载受限),该条目仅含 MD5'); } } } } const finalFiles = files.filter(f => (f.md5 || f.sha1)); if (!finalFiles.length) { setStatus(statusEl, '未获取到任何文件的哈希(MD5/SHA1),无法生成 JSON。', 'err'); modal.footerEl.innerHTML = ``; return; } const totalSize = finalFiles.reduce((s, f) => s + (Number(f.size) || 0), 0); const json = { scriptVersion: SCRIPT_VERSION, platform: '夸克网盘', totalFilesCount: finalFiles.length, totalSize, formattedTotalSize: formatSize(totalSize), files: finalFiles.map(f => ({ path: f.path, sha1: f.sha1 || '', md5: f.md5 || '', hashName: 'sha1', etag: f.md5 || '', size: Number(f.size) || 0 })), }; const jsonStr = JSON.stringify(json, null, 2); const exportName = CONFIG.defaultExportName + '_夸克.json'; modal.contentEl.innerHTML = `
已生成 ${finalFiles.length} 项秒传 JSON(MD5+SHA1)${warnMissing.length ? `;${warnMissing.length} 项未获取到哈希` : ''}
${escapeHtml(jsonStr)}
`; modal.footerEl.innerHTML = ` `; modal.footerEl.querySelector('[data-act="copy"]').onclick = () => { try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); } catch (e) { const ta = document.createElement('textarea'); ta.value = jsonStr; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); showToast('秒传 JSON 已复制到剪贴板'); } }; modal.footerEl.querySelector('[data-act="download"]').onclick = () => { const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = exportName; a.click(); URL.revokeObjectURL(url); }; modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); } catch (e) { setStatus(statusEl, `生成失败:${e.message}`, 'err'); modal.footerEl.innerHTML = ``; } } /** 夸克单文件收集(暂存 fid,hash 待批量取) */ async function quarkCollectFile(it, prefix, files, warnMissing) { const name = String(it.name || it.file_name || ''); if (!it.fid) { warnMissing.push(prefix + name + ':无 fid'); return; } files.push({ path: prefix + name, fileId: String(it.fid), md5: String(it.md5 || '').toLowerCase(), sha1: String(it.sha1 || '').toLowerCase(), size: Number(it.size || 0), }); } /** 夸克递归收集目录(file/sort 分页) */ async function quarkWalkFolder(folderId, prefix, files, warnMissing, statusEl, barEl, counter) { let page = 1; for (;;) { let r; try { r = await QUARK.getFileList(folderId, page, 100); } catch (e) { setStatus(statusEl, `列出目录失败:${prefix}(${e.message})`, 'err'); return; } const list = r.list || []; for (const it of list) { const isDir = it.dir === true; const name = String(it.file_name || ''); if (isDir) { if (it.fid) await quarkWalkFolder(String(it.fid), prefix + name + '/', files, warnMissing, statusEl, barEl, counter); } else { await quarkCollectFile({ fid: it.fid, name: it.file_name, size: it.size }, prefix, files, warnMissing); counter.done++; if (statusEl) setStatus(statusEl, `正在扫描:${prefix}(已收集 ${files.length} 个文件)`, 'info'); if (barEl) updateProgress(barEl, 0, `已收集 ${files.length} 个文件:${name}`); await sleep(CONFIG.md5Delay); } } if (!r.hasMore) break; page++; await sleep(CONFIG.listPageDelay); } } /** 夸克导入(upload/pre 秒传,无需云服务器/proof) */ async function quarkImportJson(rawJson, onProgress) { let obj; try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); } const rows = []; for (const f of (obj.files || [])) { const path = String(f.path || f.name || '').trim(); // 夸克秒传优先 sha1(40hex);也兼容纯 md5(32hex) const rawSha1 = String(f.sha1 || '').trim(); const rawMd5 = String(f.md5 || f.etag || '').trim(); const sha1 = /^[0-9a-f]{40}$/i.test(rawSha1) ? rawSha1.toLowerCase() : ''; const md5 = /^[0-9a-f]{32}$/i.test(rawMd5) ? rawMd5.toLowerCase() : ''; const size = Number(f.size != null ? f.size : 0); if (!path || (!sha1 && !md5)) continue; rows.push({ path, sha1, md5, size }); } if (!rows.length) throw new Error('JSON 中没有可导入的文件条目(需要 sha1 或 md5)'); const rootParent = QUARK.getCurrentParentId(); let success = 0, fail = 0, skipped = 0; const failures = []; const skippedDetail = []; const total = rows.length; const dirCache = new Map(); for (let i = 0; i < rows.length; i++) { const row = rows[i]; onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `夸克秒传 ${i + 1}/${total}:${row.path}` }); try { // 逐级建目录 const dirSegments = dirSegmentsFromPath(row.path); const fileName = basenameFromPath(row.path); let parent = rootParent; let mkdirOk = true; for (const dirName of dirSegments) { const key = `${parent}::${dirName}`; if (dirCache.has(key)) { parent = dirCache.get(key); continue; } try { const dirId = await QUARK.createDir(parent, dirName); dirCache.set(key, dirId); parent = dirId; await sleep(CONFIG.mkdirDelay); } catch (e) { // 已存在目录可能报错,尝试直接秒传(父目录已建则跳过) mkdirOk = false; failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`); break; } } if (!mkdirOk) { fail++; continue; } // 秒传 const r = await QUARK.rapidUpload(parent, fileName, row.size, row.sha1, row.md5, ''); if (r && r.ok && r.rapid) { success++; } else { fail++; failures.push(`${row.path}:夸克未命中快速上传(flash),服务器上可能不存在该内容(${r && r.err || ''})`); } } catch (e) { fail++; failures.push(`${row.path}:秒传失败(${e.message})`); } await sleep(CONFIG.transferDelay); } onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` }); mcLog('import', '夸克导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped); if (failures.length) mcLog('import', '失败明细:' + failures.slice(0, 8).join(' | ')); return { total, success, fail, skipped, failures, skippedDetail }; } /** 夸克导入模态框 */ function showQuarkImportModal() { const modal = openModal({ title: '保存秒传 JSON 到夸克网盘', icon: ICONS.import, width: 640, content(el) { el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:sha1=40 位、md5=32 位、path、size)。
秒传优先使用 sha1,缺失时用 md5;path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`; }, footer(el) { el.innerHTML = ` `; }, }); const ta = modal.contentEl.querySelector('#gy-ms-qk-import-ta'); const statusEl = modal.contentEl.querySelector('#gy-ms-qk-import-status'); const barEl = modal.contentEl.querySelector('#gy-ms-qk-import-bar'); const detailEl = modal.contentEl.querySelector('#gy-ms-qk-import-detail'); const runBtn = modal.footerEl.querySelector('[data-act="run"]'); modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl); runBtn.onclick = async () => { const vr = validateImportJson(ta.value); if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; } runBtn.disabled = true; runBtn.textContent = '导入中...'; barEl.style.display = 'block'; detailEl.style.display = 'none'; let summary; try { summary = await quarkImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); }); } catch (e) { setStatus(statusEl, `导入失败:${e.message}`, 'err'); runBtn.disabled = false; runBtn.textContent = '开始导入'; return; } runBtn.disabled = false; runBtn.textContent = '开始导入'; const detailLines = []; if (summary.failures.length) detailLines.push(...summary.failures); if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(未提交接口) =========='); detailLines.push(...summary.skippedDetail); } if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; } const warn = summary.fail > 0 || summary.skipped > 0; setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok'); if (warn) { try { const logBtn = document.createElement('button'); logBtn.className = 'gy-ms-btn gy-ms-btn-outline'; logBtn.textContent = '导出日志'; logBtn.style.marginTop = '8px'; logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); }; statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling); } catch (e) { /* ignore */ } } }; } // ============================== UC 网盘导出/导入(v1.3) ============================== // 与夸克同源 PDS,差异:API host=pc-api.uc.cn / pr=UCBrowser / download 的 md5 为 base64 编码 // - 导出:列表无 hash → 批量调 download 取 base64 md5(JSON 保持 base64,与 UC upload/pre 一致) // - 导入:upload/pre 用 base64 md5 秒传(实测 hex 不命中、base64 命中);小文件 md5 即可 // - JSON 轻量:{path, md5, hashName:"md5", etag:md5, size} /** UC 导出 */ async function ucGenerateJson() { const sel = UC.getCheckedItems(); if (!sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; } const modal = openModal({ title: '生成秒传 JSON(UC网盘)', icon: ICONS.generate, width: 620, content(el) { el.innerHTML = `
准备中...
`; }, footer() { return ''; }, }); const statusEl = modal.contentEl.querySelector('#gy-ms-uc-gen-status'); const barEl = modal.contentEl.querySelector('.gy-ms-bar'); const sha1Check = modal.contentEl.querySelector('#gy-ms-uc-gen-sha1'); const wantSha1 = () => sha1Check && sha1Check.checked; const files = []; const warnMissing = []; const counter = { done: 0 }; try { const rootParent = UC.getCurrentParentId(); setStatus(statusEl, `正在收集 ${sel.items.length} 项...`, 'info'); for (const item of sel.items) { const name = String(item.name || ''); if (!name) { warnMissing.push('(未命名条目)'); continue; } if (item.isDir && item.fid) { setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info'); await ucWalkFolder(item.fid, name + '/', files, warnMissing, statusEl, barEl, counter); } else if (item.fid) { await ucCollectFile(item, '', files, warnMissing); counter.done++; updateProgress(barEl, Math.round((counter.done / Math.max(1, sel.items.length)) * 100), `获取文件信息 ${counter.done}/${sel.items.length}:${name}`); await sleep(CONFIG.md5Delay); } else { warnMissing.push(name + ':未能匹配到文件ID'); } } const fids = files.filter(f => f.fileId).map(f => f.fileId); const hashMap = await UC.getFileHashes(fids); for (const f of files) { const h = hashMap[String(f.fileId)] || {}; if (h.md5) { f.md5 = h.md5 || ''; f.etag = h.md5 || ''; f.size = h.size || f.size || 0; if (!f.md5) warnMissing.push(f.path + ':未能取得哈希'); } else { warnMissing.push(f.path + ':download 接口未返回哈希'); } } // UC download 接口同样不返回 sha1;勾选后下载文件流式计算真实 SHA1(跨账号秒传的全局键) if (wantSha1()) { const needSha1 = files.filter(f => f.fileId && !f.sha1); if (needSha1.length) { setStatus(statusEl, `正在下载计算 SHA1(${needSha1.length} 个文件)...`, 'info'); for (let si = 0; si < needSha1.length; si++) { const f = needSha1[si]; setStatus(statusEl, `计算 SHA1 ${si + 1}/${needSha1.length}:${f.path}(${formatSize(f.size || 0)})`, 'info'); const sha1 = await UC.getFileSha1ByDownload(f.fileId, f.size, (pct) => { updateProgress(barEl, Math.round(pct), `SHA1 ${si + 1}/${needSha1.length}:${f.path} ${pct}%`); }); if (sha1 && /^[0-9a-f]{40}$/.test(sha1)) { f.sha1 = sha1; } else { warnMissing.push(f.path + ':SHA1 计算失败(文件下载受限),该条目仅含 MD5'); } } } } const finalFiles = files.filter(f => (f.md5 || f.sha1)); if (!finalFiles.length) { setStatus(statusEl, '未获取到任何文件的哈希(MD5),无法生成 JSON。', 'err'); modal.footerEl.innerHTML = ``; return; } const totalSize = finalFiles.reduce((s, f) => s + (Number(f.size) || 0), 0); const json = { scriptVersion: SCRIPT_VERSION, platform: 'UC网盘', totalFilesCount: finalFiles.length, totalSize, formattedTotalSize: formatSize(totalSize), files: finalFiles.map(f => ({ path: f.path, sha1: f.sha1 || '', md5: f.md5 || '', hashName: f.sha1 ? 'sha1' : 'md5', etag: f.md5 || '', size: Number(f.size) || 0 })), }; const jsonStr = JSON.stringify(json, null, 2); const exportName = CONFIG.defaultExportName + '_UC.json'; modal.contentEl.innerHTML = `
已生成 ${finalFiles.length} 项秒传 JSON(MD5+SHA1)${warnMissing.length ? `;${warnMissing.length} 项未获取到哈希` : ''}
${escapeHtml(jsonStr)}
`; modal.footerEl.innerHTML = ` `; modal.footerEl.querySelector('[data-act="copy"]').onclick = () => { try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); } catch (e) { const ta = document.createElement('textarea'); ta.value = jsonStr; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); showToast('秒传 JSON 已复制到剪贴板'); } }; modal.footerEl.querySelector('[data-act="download"]').onclick = () => { const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = exportName; a.click(); URL.revokeObjectURL(url); }; modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); } catch (e) { setStatus(statusEl, `生成失败:${e.message}`, 'err'); modal.footerEl.innerHTML = ``; } } /** UC 单文件收集 */ async function ucCollectFile(it, prefix, files, warnMissing) { const name = String(it.name || it.file_name || ''); if (!it.fid) { warnMissing.push(prefix + name + ':无 fid'); return; } files.push({ path: prefix + name, fileId: String(it.fid), md5: String(it.md5 || ''), sha1: String(it.sha1 || '').toLowerCase(), size: Number(it.size || 0) }); } /** UC 递归收集目录 */ async function ucWalkFolder(folderId, prefix, files, warnMissing, statusEl, barEl, counter) { let page = 1; for (;;) { let r; try { r = await UC.getFileList(folderId, page, 100); } catch (e) { setStatus(statusEl, `列出目录失败:${prefix}(${e.message})`, 'err'); return; } const list = r.list || []; for (const it of list) { const isDir = it.dir === true; const name = String(it.file_name || ''); if (isDir) { if (it.fid) await ucWalkFolder(String(it.fid), prefix + name + '/', files, warnMissing, statusEl, barEl, counter); } else { await ucCollectFile({ fid: it.fid, name: it.file_name, size: it.size }, prefix, files, warnMissing); counter.done++; if (statusEl) setStatus(statusEl, `正在扫描:${prefix}(已收集 ${files.length} 个文件)`, 'info'); if (barEl) updateProgress(barEl, 0, `已收集 ${files.length} 个文件:${name}`); await sleep(CONFIG.md5Delay); } } if (!r.hasMore) break; page++; await sleep(CONFIG.listPageDelay); } } /** UC 导入(upload/pre 秒传,md5 用 base64) */ async function ucImportJson(rawJson, onProgress) { let obj; try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); } const rows = []; for (const f of (obj.files || [])) { const path = String(f.path || f.name || '').trim(); // UC md5 为 base64(download 原样);兼容 hex md5(转换成 base64)与 sha1 const rawMd5 = String(f.md5 || f.etag || '').trim(); const rawSha1 = String(f.sha1 || '').trim(); let md5 = ''; if (/^[0-9a-f]{32}$/i.test(rawMd5)) { // hex → base64 const bytes = new Uint8Array(16); for (let i = 0; i < 16; i++) bytes[i] = parseInt(rawMd5.substr(i * 2, 2), 16); md5 = btoa(String.fromCharCode.apply(null, bytes)); } else if (/^[A-Za-z0-9+/]{22}==$/.test(rawMd5) || /^[A-Za-z0-9+/]{22}=$/.test(rawMd5)) { md5 = rawMd5; // 已是 base64 } const size = Number(f.size != null ? f.size : 0); if (!path || (!md5 && !rawSha1)) continue; rows.push({ path, md5, sha1: rawSha1.toLowerCase(), size }); } if (!rows.length) throw new Error('JSON 中没有可导入的文件条目(需要 md5 或 sha1)'); const rootParent = UC.getCurrentParentId(); let success = 0, fail = 0, skipped = 0; const failures = []; const skippedDetail = []; const total = rows.length; const dirCache = new Map(); for (let i = 0; i < rows.length; i++) { const row = rows[i]; onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `UC秒传 ${i + 1}/${total}:${row.path}` }); try { const dirSegments = dirSegmentsFromPath(row.path); const fileName = basenameFromPath(row.path); let parent = rootParent; let mkdirOk = true; for (const dirName of dirSegments) { const key = `${parent}::${dirName}`; if (dirCache.has(key)) { parent = dirCache.get(key); continue; } try { const dirId = await UC.createDir(parent, dirName); dirCache.set(key, dirId); parent = dirId; await sleep(CONFIG.mkdirDelay); } catch (e) { mkdirOk = false; failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`); break; } } if (!mkdirOk) { fail++; continue; } // 有 sha1 用 sha1(大文件必需),否则 base64 md5 let r; if (row.sha1 && /^[0-9a-f]{40}$/i.test(row.sha1)) { r = await UC.rapidUploadWithSha1(parent, fileName, row.size, row.sha1, row.md5, ''); } else { r = await UC.rapidUpload(parent, fileName, row.size, row.md5, ''); } if (r && r.ok && r.rapid) { success++; } else { fail++; failures.push(`${row.path}:UC未命中快速上传(flash),服务器上可能不存在该内容(${r && r.err || ''})`); } } catch (e) { fail++; failures.push(`${row.path}:秒传失败(${e.message})`); } await sleep(CONFIG.transferDelay); } onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` }); mcLog('import', 'UC导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped); if (failures.length) mcLog('import', '失败明细:' + failures.slice(0, 8).join(' | ')); return { total, success, fail, skipped, failures, skippedDetail }; } /** UC 导入模态框 */ function showUcImportModal() { const modal = openModal({ title: '保存秒传 JSON 到 UC网盘', icon: ICONS.import, width: 640, content(el) { el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:md5=base64 或 32 位 hex、sha1=40 位、path、size)。
UC 秒传优先使用 sha1(大文件必需),缺失时用 md5(小文件可用);path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`; }, footer(el) { el.innerHTML = ` `; }, }); const ta = modal.contentEl.querySelector('#gy-ms-uc-import-ta'); const statusEl = modal.contentEl.querySelector('#gy-ms-uc-import-status'); const barEl = modal.contentEl.querySelector('#gy-ms-uc-import-bar'); const detailEl = modal.contentEl.querySelector('#gy-ms-uc-import-detail'); const runBtn = modal.footerEl.querySelector('[data-act="run"]'); modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl); runBtn.onclick = async () => { const vr = validateImportJson(ta.value); if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; } runBtn.disabled = true; runBtn.textContent = '导入中...'; barEl.style.display = 'block'; detailEl.style.display = 'none'; let summary; try { summary = await ucImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); }); } catch (e) { setStatus(statusEl, `导入失败:${e.message}`, 'err'); runBtn.disabled = false; runBtn.textContent = '开始导入'; return; } runBtn.disabled = false; runBtn.textContent = '开始导入'; const detailLines = []; if (summary.failures.length) detailLines.push(...summary.failures); if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(未提交接口) =========='); detailLines.push(...summary.skippedDetail); } if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; } const warn = summary.fail > 0 || summary.skipped > 0; setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok'); if (warn) { try { const logBtn = document.createElement('button'); logBtn.className = 'gy-ms-btn gy-ms-btn-outline'; logBtn.textContent = '导出日志'; logBtn.style.marginTop = '8px'; logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); }; statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling); } catch (e) { /* ignore */ } } }; } // ============================== 腾讯微云导出/导入(v1.4) ============================== // 设计要点: // - 列表自带 file_sha(完整 SHA1),JSON 输出 {path, sha1, hashName:'sha1', size} // - 微云秒传(ftn_pre_upload)需要"1MB 分块 SHA1 状态 + check_sha + check_data(末尾 ≤128 字节)", // 这些只能由导出方(持有原文件)下载计算后内嵌 JSON(wyBlocks/wyCheckSha/wyCheckData),导入方零下载直接提交; // - 未填写分块信息的 JSON(如夸克/UC 导出的)无法导入微云(微云不认纯 sha1 秒传),会提示; // - 导入未命中(file_exist=false)时微云会创建空壳条目 → 自动清理并报"未命中"。 /** 微云导出:勾选 → 递归列表(sha1 自带)→ 可选下载计算分块信息 → JSON */ async function weiyunGenerateJson() { let sel; try { sel = await WEIYUN.getCheckedItems(); } catch (e) { showToast('读取勾选失败:' + e.message, 'error'); return; } if (!sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; } const modal = openModal({ title: '生成秒传 JSON(腾讯微云)', icon: ICONS.generate, width: 640, content(el) { el.innerHTML = `
准备中...
`; }, footer() { return ''; }, }); const statusEl = modal.contentEl.querySelector('#gy-ms-wy-gen-status'); const barEl = modal.contentEl.querySelector('.gy-ms-bar'); const blocksCheck = modal.contentEl.querySelector('#gy-ms-wy-gen-blocks'); const wantBlocks = () => blocksCheck && blocksCheck.checked; const files = []; const warnMissing = []; const counter = { done: 0 }; try { setStatus(statusEl, sel.all ? `已全选当前目录 ${sel.items.length} 项,正在收集信息...` : `正在收集 ${sel.items.length} 项...`, 'info'); const curDirKey = await WEIYUN.resolveDirKey(WEIYUN.getCurrentDirKey()); for (const item of sel.items) { const name = String(item.name || ''); if (!name) { warnMissing.push('(未命名条目)'); continue; } if (item.kind === 'dir' && item.dirKey) { setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info'); await weiyunWalkFolder(item.dirKey, name + '/', curDirKey, files, warnMissing, statusEl, barEl, counter); } else if (item.kind === 'file' && item.fileId) { await weiyunCollectFile(item, '', files, warnMissing, item.pdirKey || curDirKey); counter.done++; updateProgress(barEl, Math.round((counter.done / Math.max(1, sel.items.length)) * 100), `获取文件信息 ${counter.done}/${sel.items.length}:${name}`); await sleep(CONFIG.md5Delay); } else { warnMissing.push(name + ':未能匹配到文件ID'); } } const finalFiles = files.filter(f => f.sha1); if (!finalFiles.length) { setStatus(statusEl, '未获取到任何文件的 SHA1,无法生成 JSON。', 'err'); modal.footerEl.innerHTML = ``; return; } // 下载计算微云秒传分块信息(每文件 Range 流式;用户可关闭——关闭后 JSON 仅含 sha1,供夸克/UC/阿里导入) const want = wantBlocks(); if (want) { setStatus(statusEl, `正在下载计算微云分块信息(${finalFiles.length} 个文件)...`, 'info'); for (let i = 0; i < finalFiles.length; i++) { const f = finalFiles[i]; if (!f.fileId) continue; setStatus(statusEl, `计算分块 ${i + 1}/${finalFiles.length}:${f.path}(${formatSize(f.size || 0)})`, 'info'); const binfo = await WEIYUN.computeBlockInfo(f.pdirKey || curDirKey, { fileId: f.fileId, size: f.size }, (pct) => { updateProgress(barEl, Math.round(pct), `分块 ${i + 1}/${finalFiles.length}:${f.path} ${pct}%`); }); if (binfo && binfo.blockInfoList && binfo.checkData) { f.wyBlocks = binfo.blockInfoList; f.wyCheckSha = binfo.checkSha; f.wyCheckData = binfo.checkData; f.sha1 = f.sha1 || binfo.fileHash; } else { warnMissing.push(f.path + ':分块信息计算失败(下载受限),该条目仅含 SHA1'); } } } const totalSize = finalFiles.reduce((s, f) => s + (Number(f.size) || 0), 0); const json = { scriptVersion: SCRIPT_VERSION, platform: '腾讯微云', totalFilesCount: finalFiles.length, totalSize, formattedTotalSize: formatSize(totalSize), files: finalFiles.map(f => ({ path: f.path, sha1: f.sha1 || '', hashName: 'sha1', etag: '', size: Number(f.size) || 0, ...(f.fileId ? { fileId: f.fileId } : {}), ...(f.wyBlocks ? { wyBlocks: f.wyBlocks, wyCheckSha: f.wyCheckSha, wyCheckData: f.wyCheckData } : {}), })), }; const jsonStr = JSON.stringify(json, null, 2); const exportName = CONFIG.defaultExportName + '_微云.json'; const withBlocks = finalFiles.filter(f => f.wyBlocks).length; modal.contentEl.innerHTML = `
已生成 ${finalFiles.length} 项秒传 JSON(SHA1${withBlocks ? `,${withBlocks} 项含微云分块信息` : ''})${warnMissing.length ? `;${warnMissing.length} 项未完整` : ''}
${withBlocks ? '含分块信息的 JSON 可被**任意微云账号**秒传导入;' : ''}SHA1 字段可被夸克/UC 导入。
${escapeHtml(jsonStr)}
`; modal.footerEl.innerHTML = ` `; modal.footerEl.querySelector('[data-act="copy"]').onclick = () => { try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); } catch (e) { const ta = document.createElement('textarea'); ta.value = jsonStr; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); showToast('秒传 JSON 已复制到剪贴板'); } }; modal.footerEl.querySelector('[data-act="download"]').onclick = () => { const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = exportName; a.click(); URL.revokeObjectURL(url); }; modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); } catch (e) { setStatus(statusEl, `生成失败:${e.message}`, 'err'); modal.footerEl.innerHTML = ``; } } /** 微云单文件收集(sha1 来自列表条目;pdirKey=文件所在目录,下载分块时用) */ async function weiyunCollectFile(it, prefix, files, warnMissing, pdirKey) { const name = String(it.name || it.file_name || ''); const sha1 = String(it.sha1 || it.file_sha || '').toLowerCase(); if (!it.fileId) { warnMissing.push(prefix + name + ':无 file_id'); return; } files.push({ path: prefix + name, fileId: String(it.fileId), pdirKey: String(pdirKey || ''), sha1: /^[0-9a-f]{40}$/.test(sha1) ? sha1 : '', size: Number(it.size || 0), }); } /** 微云递归收集目录(DiskDirBatchList 分页) */ async function weiyunWalkFolder(folderId, prefix, curDirKey, files, warnMissing, statusEl, barEl, counter) { let offset = 0; for (;;) { let r; try { r = await WEIYUN.getFileList(folderId, offset, 500); } catch (e) { setStatus(statusEl, `列出目录失败:${prefix}(${e.message})`, 'err'); return; } for (const d of r.dirs) { if (d.dirKey) await weiyunWalkFolder(d.dirKey, prefix + d.name + '/', curDirKey, files, warnMissing, statusEl, barEl, counter); } for (const f of r.files) { await weiyunCollectFile(f, prefix, files, warnMissing, String(folderId)); counter.done++; if (statusEl) setStatus(statusEl, `正在扫描:${prefix}(已收集 ${files.length} 个文件)`, 'info'); if (barEl) updateProgress(barEl, 0, `已收集 ${files.length} 个文件:${f.name}`); await sleep(CONFIG.md5Delay); } if (r.finishFlag || (!r.dirs.length && !r.files.length)) break; offset += 500; await sleep(CONFIG.listPageDelay); } } /** 微云导入(ftn_pre_upload 秒传;需 JSON 内嵌微云分块信息) */ async function weiyunImportJson(rawJson, onProgress) { let obj; try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); } const rows = []; for (const f of (obj.files || [])) { const path = String(f.path || f.name || '').trim(); const rawSha1 = String(f.sha1 || '').trim(); const sha1 = /^[0-9a-f]{40}$/i.test(rawSha1) ? rawSha1.toLowerCase() : ''; const blocks = Array.isArray(f.wyBlocks) && f.wyBlocks.length ? f.wyBlocks : null; const checkSha = String(f.wyCheckSha || '').trim() || ''; const checkData = String(f.wyCheckData || '').trim() || ''; const size = Number(f.size != null ? f.size : 0); if (!path || !sha1) continue; rows.push({ path, sha1, size, blocks, checkSha, checkData }); } if (!rows.length) throw new Error('JSON 中没有可导入的文件条目(需要 sha1)'); const noBlocks = rows.filter(r => !r.blocks).length; if (noBlocks === rows.length) { throw new Error('微云秒传需要分块信息(wyBlocks/wyCheckSha/wyCheckData)——该 JSON 未包含。请由「腾讯微云」页面导出(勾选"计算微云秒传分块信息")后再导入微云;含 sha1 的 JSON 可直接导入夸克/UC。'); } const rootParent = await WEIYUN.resolveDirKey(WEIYUN.getCurrentDirKey()); let success = 0, fail = 0, skipped = 0; const failures = []; const skippedDetail = []; const total = rows.length; const dirCache = new Map(); for (let i = 0; i < rows.length; i++) { const row = rows[i]; onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `微云秒传 ${i + 1}/${total}:${row.path}` }); try { if (!row.blocks) { skipped++; skippedDetail.push(`${row.path}:无微云分块信息,跳过(可导入夸克/UC)`); continue; } // 逐级建目录(同夸克/UC 逻辑) const dirSegments = dirSegmentsFromPath(row.path); const fileName = basenameFromPath(row.path); let parent = rootParent; let mkdirOk = true; for (const dirName of dirSegments) { const key = `${parent}::${dirName}`; if (dirCache.has(key)) { parent = dirCache.get(key); continue; } try { const dirId = await WEIYUN.createDir(parent, dirName); dirCache.set(key, dirId); parent = dirId; await sleep(CONFIG.mkdirDelay); } catch (e) { mkdirOk = false; failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`); break; } } if (!mkdirOk) { fail++; continue; } // 秒传(PreUpload) const pair = await WEIYUN.getPdirPair(parent); const r = await WEIYUN.preUpload(pair, fileName, row.size, 1048576, row.checkSha, row.checkData, row.blocks, 2); if (r.hit) { success++; } else { // 未命中 → 清理微云创建的空壳条目 if (r.fileId) { WEIYUN.deleteFiles([{ ppdir_key: pair.ppdir_key, pdir_key: parent, file_id: r.fileId, filename: fileName }], []).catch(() => {}); } fail++; failures.push(`${row.path}:微云未命中秒传(云端无该内容),空壳条目已清理`); } } catch (e) { fail++; failures.push(`${row.path}:秒传失败(${e.message})`); } await sleep(CONFIG.transferDelay); } onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` }); mcLog('import', '微云导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped); if (failures.length) mcLog('import', '失败明细:' + failures.slice(0, 8).join(' | ')); return { total, success, fail, skipped, failures, skippedDetail }; } /** 微云导入模态框 */ function showWeiyunImportModal() { const modal = openModal({ title: '保存秒传 JSON 到腾讯微云', icon: ICONS.import, width: 640, content(el) { el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:sha1=40 位、wyBlocks/wyCheckSha/wyCheckData=微云分块信息、path、size)。
微云秒传需要 JSON 携带微云分块信息(仅由微云页面导出时勾选"计算分块信息"生成);夸克/UC 导出的 JSON(只有 sha1)无法导入微云,可导入夸克/UC。
path 中的目录会自动创建;未命中时自动清理空壳条目。
${IMPORT_FILE_ROW_HTML}
`; }, footer(el) { el.innerHTML = ` `; }, }); const ta = modal.contentEl.querySelector('#gy-ms-wy-import-ta'); const statusEl = modal.contentEl.querySelector('#gy-ms-wy-import-status'); const barEl = modal.contentEl.querySelector('#gy-ms-wy-import-bar'); const detailEl = modal.contentEl.querySelector('#gy-ms-wy-import-detail'); const runBtn = modal.footerEl.querySelector('[data-act="run"]'); modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl); runBtn.onclick = async () => { const vr = validateImportJson(ta.value); if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; } runBtn.disabled = true; runBtn.textContent = '导入中...'; barEl.style.display = 'block'; detailEl.style.display = 'none'; let summary; try { summary = await weiyunImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); }); } catch (e) { setStatus(statusEl, `导入失败:${e.message}`, 'err'); runBtn.disabled = false; runBtn.textContent = '开始导入'; return; } runBtn.disabled = false; runBtn.textContent = '开始导入'; const detailLines = []; if (summary.failures.length) detailLines.push(...summary.failures); if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(无分块信息) =========='); detailLines.push(...summary.skippedDetail); } if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; } const warn = summary.fail > 0 || summary.skipped > 0; setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok'); if (warn) { try { const logBtn = document.createElement('button'); logBtn.className = 'gy-ms-btn gy-ms-btn-outline'; logBtn.textContent = '导出日志'; logBtn.style.marginTop = '8px'; logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); }; statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling); } catch (e) { /* ignore */ } } }; } // ============================== 小飞机网盘导出/导入(v1.5) ============================== // 设计要点: // - 导出【零下载】:每文件发 Range bytes=0-1 的极小请求,读 COS 对象元数据 x-amz-meta-callback // 直接取服务器记录的 md5(=小飞机去重池键,实测可秒传命中);列表不提供哈希,故无需下载整个文件。 // - 导入:getUpToken 秒传,upToken==='-1' 即命中;只认 md5(32hex),无 md5 的条目跳过并提示。 /** 小飞机导出:勾选 → 递归列表 → 服务器元数据取 md5(零下载)→ JSON */ async function feejiiGenerateJson() { const c = FEEJII.readCommon(); if (!c) { showToast('未检测到小飞机网盘登录态,请刷新页面', 'warning'); return; } let sel; try { sel = FEEJII.getCheckedItems(); } catch (e) { showToast('读取勾选失败:' + e.message, 'error'); return; } if (!sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; } const modal = openModal({ title: '生成秒传 JSON(小飞机网盘)', icon: ICONS.generate, width: 640, content(el) { el.innerHTML = `
准备中...
md5 从小飞机服务器直接获取(零下载、快,无需下载文件计算哈希)。
`; }, footer() { return ''; }, }); const statusEl = modal.contentEl.querySelector('#gy-ms-fj-gen-status'); const barEl = modal.contentEl.querySelector('.gy-ms-bar'); const files = []; const warnMissing = []; const counter = { done: 0 }; try { setStatus(statusEl, sel.all ? `已全选当前目录 ${sel.items.length} 项,正在收集信息...` : `正在收集 ${sel.items.length} 项...`, 'info'); for (const item of sel.items) { const name = String(item.fileName || item.name || ''); if (!name) { warnMissing.push('(未命名条目)'); continue; } if (Number(item.fileType) === 2 && item.fileId) { setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info'); await feejiiWalkFolder(item.fileId, name + '/', files, warnMissing, statusEl, barEl, counter); } else if (item.fileId) { await feejiiCollectFile(item, '', files, warnMissing); counter.done++; updateProgress(barEl, Math.round((counter.done / Math.max(1, sel.items.length)) * 100), `获取文件信息 ${counter.done}/${sel.items.length}:${name}`); await sleep(CONFIG.md5Delay); } else { warnMissing.push(name + ':未能匹配到文件ID'); } } // 零下载:从服务器元数据取 md5 const withMd5 = files.filter(f => f.fileId); setStatus(statusEl, `正在从服务器获取 md5(${withMd5.length} 个文件,零下载)...`, 'info'); const pending = []; for (let i = 0; i < withMd5.length; i++) { const f = withMd5[i]; const md5 = await FEEJII.getServerMd5(f.fileId); if (md5 && /^[0-9a-f]{32}$/.test(md5)) { f.md5 = md5; } else { pending.push(f); } setStatus(statusEl, `获取 md5 ${i + 1}/${withMd5.length}:${f.path}`, 'info'); await sleep(60); } // 大文件/分片文件回退:CDN 无哈希头 → 询问是否下载计算 if (pending.length) { const totalBytes = pending.reduce((s, f) => s + (Number(f.size) || 0), 0); if (await confirmBigDownloadModal(modal, statusEl, pending.length, totalBytes)) { for (let i = 0; i < pending.length; i++) { const f = pending[i]; setStatus(statusEl, `下载计算 md5 ${i + 1}/${pending.length}:${f.path}(${formatSize(Number(f.size) || 0)})`, 'info'); const m = await FEEJII.computeMd5ByDownload(f.fileId, f.size, (pct) => updateProgress(barEl, Math.round(pct * 0.9), `计算 ${f.path}:${pct}%`)); if (m && /^[0-9a-f]{32}$/.test(m)) { f.md5 = m; } else { warnMissing.push(f.path + ':下载计算 md5 失败'); } updateProgress(barEl, 90, `下载计算 ${i + 1}/${pending.length}`); await sleep(60); } } else { pending.forEach(f => warnMissing.push(f.path + ':服务器无 md5(大文件/分片),已跳过')); } } const finalFiles = files.filter(f => f.md5); if (!finalFiles.length) { setStatus(statusEl, '未获取到任何文件的 md5,无法生成 JSON。', 'err'); modal.footerEl.innerHTML = ``; return; } const totalSize = finalFiles.reduce((s, f) => s + (Number(f.size) || 0), 0); const json = { scriptVersion: SCRIPT_VERSION, platform: '小飞机网盘', totalFilesCount: finalFiles.length, totalSize, formattedTotalSize: formatSize(totalSize), files: finalFiles.map(f => ({ path: f.path, md5: f.md5, hashName: 'md5', etag: f.md5, size: Number(f.size) || 0, })), }; const jsonStr = JSON.stringify(json, null, 2); const exportName = CONFIG.defaultExportName + '_小飞机.json'; modal.contentEl.innerHTML = `
已生成 ${finalFiles.length} 项秒传 JSON(md5,零下载)${warnMissing.length ? `;${warnMissing.length} 项未获取到 md5` : ''}
md5 可直接导入小飞机/百度/夸克/UC;size 已换算为字节(与其他平台 JSON 一致)。
${escapeHtml(jsonStr)}
`; modal.footerEl.innerHTML = ` `; modal.footerEl.querySelector('[data-act="copy"]').onclick = () => { try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); } catch (e) { const ta = document.createElement('textarea'); ta.value = jsonStr; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); showToast('秒传 JSON 已复制到剪贴板'); } }; modal.footerEl.querySelector('[data-act="download"]').onclick = () => { const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = exportName; a.click(); URL.revokeObjectURL(url); }; modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); } catch (e) { setStatus(statusEl, `生成失败:${e.message}`, 'err'); modal.footerEl.innerHTML = ``; } } /** 小飞机单文件收集(暂存 fileId;注意列表 fileSize 单位是 KB,导出统一换算为字节与其他平台 JSON 一致) */ async function feejiiCollectFile(it, prefix, files, warnMissing) { const name = String(it.fileName || it.file_name || it.name || ''); if (!it.fileId) { warnMissing.push(prefix + name + ':无 fileId'); return; } const kb = Number(it.fileSize || it.size || 0); files.push({ path: prefix + name, fileId: String(it.fileId), size: kb > 0 ? kb * 1024 : 0, folderId: String(it.folderId || ''), md5: '', sha1: '', }); } /** 小飞机递归收集目录(file/list 分页) */ async function feejiiWalkFolder(folderId, prefix, files, warnMissing, statusEl, barEl, counter) { let offset = 1; for (;;) { let r; try { r = await FEEJII.getFileList(folderId, offset); } catch (e) { setStatus(statusEl, `列出目录失败:${prefix}(${e.message})`, 'err'); return; } const list = r.list || []; for (const it of list) { const isDir = Number(it.fileType) === 2; const name = String(it.fileName || ''); if (isDir) { if (it.fileId) await feejiiWalkFolder(String(it.fileId), prefix + name + '/', files, warnMissing, statusEl, barEl, counter); } else { await feejiiCollectFile({ fileId: it.fileId, fileName: it.fileName, fileSize: it.fileSize, folderId: it.folderId }, prefix, files, warnMissing); counter.done++; if (statusEl) setStatus(statusEl, `正在扫描:${prefix}(已收集 ${files.length} 个文件)`, 'info'); if (barEl) updateProgress(barEl, 0, `已收集 ${files.length} 个文件:${name}`); await sleep(CONFIG.md5Delay); } } if (!r.hasMore) break; offset++; await sleep(CONFIG.listPageDelay); } } /** 小飞机导入(getUpToken 秒传,只认 md5) */ async function feejiiImportJson(rawJson, onProgress) { const c = FEEJII.readCommon(); if (!c) throw new Error('未检测到小飞机网盘登录态,请刷新页面'); let obj; try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); } const rows = []; const skippedDetail = []; for (const f of (obj.files || [])) { const path = String(f.path || f.name || '').trim(); const md5 = normalizeMd5(f.md5 || f.etag); const size = Number(f.size != null ? f.size : 0); if (!path) continue; if (!md5) { skippedDetail.push(`${path}:无 md5(小飞机秒传只认 md5;阿里导出的仅含 sha1 无法导入)`); continue; } rows.push({ path, md5, size }); } if (!rows.length) throw new Error('JSON 中没有可导入的条目(小飞机秒传需要 md5,32 位 hex)'); const rootParent = FEEJII.getCurrentParentId(); let success = 0, fail = 0, skipped = 0; const failures = []; const total = rows.length; const dirCache = new Map(); for (let i = 0; i < rows.length; i++) { const row = rows[i]; onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `小飞机秒传 ${i + 1}/${total}:${row.path}` }); try { // 逐级建目录 const dirSegments = dirSegmentsFromPath(row.path); const fileName = basenameFromPath(row.path); let parent = rootParent; let mkdirOk = true; for (const dirName of dirSegments) { const key = `${parent}::${dirName}`; if (dirCache.has(key)) { parent = dirCache.get(key); continue; } try { const dirId = await FEEJII.createDir(parent, dirName); dirCache.set(key, dirId); parent = dirId; await sleep(CONFIG.mkdirDelay); } catch (e) { mkdirOk = false; failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`); break; } } if (!mkdirOk) { fail++; continue; } // 秒传 const r = await FEEJII.rapidUpload(fileName, row.md5, row.size, parent); if (r.hit) { success++; } else { fail++; failures.push(`${row.path}:小飞机未命中秒传(云端不存在该 md5 内容)`); } } catch (e) { fail++; failures.push(`${row.path}:秒传失败(${e.message})`); } await sleep(CONFIG.transferDelay); } onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` }); mcLog('import', '小飞机导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped); if (failures.length) mcLog('import', '失败明细:' + failures.slice(0, 8).join(' | ')); return { total, success, fail, skipped, failures, skippedDetail }; } /** 小飞机导入模态框 */ function showFeejiiImportModal() { const modal = openModal({ title: '保存秒传 JSON 到小飞机网盘', icon: ICONS.import, width: 640, content(el) { el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:md5=32 位、path、size)。
小飞机秒传只认 md5:夸克/UC/百度/光鸭导出的 JSON(含 md5)可直接秒传;阿里云盘导出的(仅 sha1)将被跳过并提示。
path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`; }, footer(el) { el.innerHTML = ` `; }, }); const ta = modal.contentEl.querySelector('#gy-ms-fj-import-ta'); const statusEl = modal.contentEl.querySelector('#gy-ms-fj-import-status'); const barEl = modal.contentEl.querySelector('#gy-ms-fj-import-bar'); const detailEl = modal.contentEl.querySelector('#gy-ms-fj-import-detail'); const runBtn = modal.footerEl.querySelector('[data-act="run"]'); modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl); runBtn.onclick = async () => { const vr = validateImportJson(ta.value); if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; } runBtn.disabled = true; runBtn.textContent = '导入中...'; barEl.style.display = 'block'; detailEl.style.display = 'none'; let summary; try { summary = await feejiiImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); }); } catch (e) { setStatus(statusEl, `导入失败:${e.message}`, 'err'); runBtn.disabled = false; runBtn.textContent = '开始导入'; return; } runBtn.disabled = false; runBtn.textContent = '开始导入'; const detailLines = []; if (summary.failures.length) detailLines.push(...summary.failures); if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(无 md5) =========='); detailLines.push(...summary.skippedDetail); } if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; } const warn = summary.fail > 0 || summary.skipped > 0; setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok'); if (warn) { try { const logBtn = document.createElement('button'); logBtn.className = 'gy-ms-btn gy-ms-btn-outline'; logBtn.textContent = '导出日志'; logBtn.style.marginTop = '8px'; logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); }; statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling); } catch (e) { /* ignore */ } } }; } // ============================== 蓝奏云导出(v1.6:分享链 + CDN Content-Md5 零下载取 md5) ============================== // 蓝奏云网页无多选机制(行尾菜单单文件操作),故导出流程为:弹窗列出当前目录(文件夹+文件)→ 勾选(勾文件夹=递归导出)→ // 逐文件 task=22 分享信息 → 分享页解析签名 → ajaxfile.php 取直链 → CDN 响应头 Content-Md5 取 md5(零下载)。 // ============================== 蓝奏云 iframe 勾选注入(v1.6) ============================== // 蓝奏云网页无原生多选(老式 UI,行尾仅单文件操作菜单),这里向文件管理 iframe 注入: // - 工具栏「秒传」按钮(挂到 .mydisk_file_bar,与 上传文件/新建文件夹 并排) // - 每行 .f_tb 前勾选框(文件行 id=f{fileId} / 文件夹行 id=fol{folId}) // - 表头 .f_th 文件列「全选」勾选框(覆盖当前已加载行;先点「显示更多文件」可加载更多) // - MutationObserver 跟随列表重绘自动给新行补勾选框 const LZ_CB_STYLE = 'margin:0 5px 0 0;vertical-align:middle;cursor:pointer;accent-color:#10b981;width:14px;height:14px;'; const LZ_STYLE_CSS = ` .f_tb input.lzms-cb, .f_th input.lzms-all { ${LZ_CB_STYLE} } .f_tb .f_name, .f_tb .f_name2 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } `; /** 更新全选勾选框状态(indeterminate) */ function lzUpdateAllState(doc) { try { const all = doc.querySelector('input.lzms-all'); if (!all) return; const cbs = doc.querySelectorAll('.f_tb input.lzms-cb'); const checked = doc.querySelectorAll('.f_tb input.lzms-cb:checked'); all.checked = cbs.length > 0 && checked.length === cbs.length; all.indeterminate = checked.length > 0 && checked.length < cbs.length; } catch (e) { /* ignore */ } } /** 给 iframe 当前已渲染的行 + 表头补勾选框(幂等) */ function lanzouInjectCheckboxes(doc) { if (!doc || !doc.body) return; // 行勾选框 doc.querySelectorAll('div.f_tb').forEach(row => { try { if (row.querySelector('input.lzms-cb')) return; const id = String(row.id || ''); if (!/^f\d+$/.test(id) && !/^fol\d+$/.test(id)) return; const cb = doc.createElement('input'); cb.type = 'checkbox'; cb.className = 'lzms-cb'; cb.title = '勾选后点「秒传 → 生成秒传JSON」导出'; cb.addEventListener('change', () => lzUpdateAllState(doc)); const anchor = row.querySelector('.f_name, .f_name2'); if (anchor) anchor.insertBefore(cb, anchor.firstChild); } catch (e) { /* ignore */ } }); // 表头全选 const th = doc.querySelector('div.f_th'); if (th && !th.querySelector('input.lzms-all')) { try { const nameCol = th.querySelector('.f_name'); if (nameCol) { const cb = doc.createElement('input'); cb.type = 'checkbox'; cb.className = 'lzms-all'; cb.title = '全选当前已加载项(先点「显示更多文件」可加载更多)'; cb.addEventListener('change', () => { doc.querySelectorAll('.f_tb input.lzms-cb').forEach(c => c.checked = cb.checked); lzUpdateAllState(doc); }); nameCol.insertBefore(cb, nameCol.firstChild); } } catch (e) { /* ignore */ } } lzUpdateAllState(doc); } /** 从 iframe 页面勾选读取 {id, name, size, isDir}[](文件 id=fileId / 文件夹 id=folderId) */ function lanzouGetCheckedRows() { const doc = LANZOU.frameDoc(); if (!doc || !doc.body) return []; const out = []; doc.querySelectorAll('div.f_tb').forEach(row => { try { const cb = row.querySelector('input.lzms-cb'); if (!cb || !cb.checked) return; const id = String(row.id || ''); let item = null; if (/^f\d+$/.test(id)) { const nameEl = row.querySelector('.f_name_title'); const sizeEl = row.querySelector('.f_size'); item = { id: id.slice(1), name: nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : '', size: sizeEl ? sizeEl.textContent.replace(/\s+/g, ' ').trim() : '0 B', isDir: false, }; } else if (/^fol\d+$/.test(id)) { const nameEl = row.querySelector('.f_name2, .f_name'); item = { id: id.slice(3), name: nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : '', size: '', isDir: true, }; } if (item && item.name) out.push(item); } catch (e) { /* ignore */ } }); return out; } /** 蓝奏云顶层:等待文件管理 iframe 加载后注入(按钮 + 勾选框 + MutationObserver) */ function lanzouSetupIframe() { let frame = null; try { frame = document.querySelector('iframe[name="mainframe"], iframe[src*="item=files"], iframe[src*="mydisk.php"]'); } catch (e) { /* ignore */ } if (!frame) { setTimeout(lanzouSetupIframe, 600); return; } let doc = null; try { doc = frame.contentDocument; } catch (e) { /* ignore */ } if (!doc || doc.readyState !== 'complete' || !doc.body) { setTimeout(lanzouSetupIframe, 600); return; } // iframe 内样式 try { if (!doc.getElementById('gy-ms-lz-style')) { const st = doc.createElement('style'); st.id = 'gy-ms-lz-style'; st.textContent = LZ_STYLE_CSS; (doc.head || doc.documentElement).appendChild(st); } } catch (e) { /* ignore */ } // 工具栏按钮(与 上传文件/新建文件夹 并排) try { if (!doc.querySelector('.gy-ms-toolbar-btn')) { const bar = doc.querySelector('div.mydisk_file_bar, .f_upb'); if (bar) { const wrap = doc.createElement('div'); wrap.className = 'gy-ms-toolbar-btn'; wrap.style.cssText = 'display:inline-flex;margin-right:8px;vertical-align:middle;position:relative;z-index:100;'; wrap.innerHTML = buildButtonHtml(); bar.appendChild(wrap); bindDropdown(wrap); } } } catch (e) { /* ignore */ } // 勾选框(全选 + 行) lanzouInjectCheckboxes(doc); // 跟随列表重绘维护勾选框 try { if (!doc.__lzmsMo) { const MO = (doc.defaultView && doc.defaultView.MutationObserver) || window.MutationObserver; if (MO) { const mo = new MO(() => { lanzouInjectCheckboxes(doc); }); mo.observe(doc.body, { childList: true, subtree: true }); doc.__lzmsMo = mo; } } } catch (e) { /* ignore */ } // iframe 重新加载后重建 try { if (frame && !frame.__lzmsBoundLoad) { frame.__lzmsBoundLoad = true; frame.addEventListener('load', () => { setTimeout(lanzouSetupIframe, 400); }); } } catch (e) { /* ignore */ } } /** 蓝奏云大小字符串 → 字节("526.0 B"/"12.5 MB"/"1.2 GB") */ function parseLanzouSizeStr(str) { const m = String(str || '').match(/([\d.]+)\s*([BKMGT]?)/i); if (!m) return 0; const v = parseFloat(m[1]) || 0; const unit = (m[2] || 'B').toUpperCase(); const f = { B: 1, K: 1024, M: 1048576, G: 1073741824, T: 1099511627776 }[unit] || 1; return Math.round(v * f); } /** 蓝奏云递归收集目录(folderId 下全部文件) */ async function lanzouWalkFolder(folderId, prefix, files, warnMissing, statusEl, counter) { let r; try { r = await LANZOU.getFileList(folderId); } catch (e) { setStatus(statusEl, `列出目录失败:${prefix}(${e.message})`, 'err'); return; } for (const fol of r.folders) { await lanzouWalkFolder(fol.id, prefix + fol.name + '/', files, warnMissing, statusEl, counter); } for (const f of r.files) { files.push({ path: prefix + f.name, fileId: f.id, size: parseLanzouSizeStr(f.size), md5: '', sha1: '' }); if (counter) counter.done++; if (statusEl) setStatus(statusEl, `正在扫描:${prefix}(已收集 ${files.length} 个文件)`, 'info'); await sleep(CONFIG.md5Delay); } } /** 蓝奏云导出(优先读取页面勾选 checkbox;无勾选时走弹窗选择兜底) */ async function lanzouGenerateJson() { // 1) 页面勾选优先(脚本注入的 .lzms-cb checkbox) const selRows = lanzouGetCheckedRows(); if (selRows.length) { await lanzouRunExport(selRows); return; } // 2) 兜底:弹窗选择器 let folderList; try { folderList = await LANZOU.getFileList(LANZOU.getCurrentFolderId()); } catch (e) { showToast('读取蓝奏云当前目录失败:' + e.message, 'error'); return; } const all = [...folderList.folders.map(f => ({ ...f, isDir: true })), ...folderList.files.map(f => ({ ...f, isDir: false }))]; if (!all.length) { showToast('当前目录为空,没有可导出的文件', 'warning'); return; } // 弹窗选择 const modal = openModal({ title: '选择要导出的蓝奏云文件', icon: ICONS.generate, width: 640, content(el) { el.innerHTML = `
当前目录 ${folderList.folders.length} 个文件夹、${folderList.files.length} 个文件。勾选后导出(勾选文件夹=递归导出其全部文件)。
`; }, footer(el) { el.innerHTML = ` `; }, }); const listEl = modal.contentEl.querySelector('#gy-ms-lz-list'); const rowHtml = (it) => ` `; listEl.innerHTML = all.map(rowHtml).join(''); modal.contentEl.querySelector('[data-act="all"]').onclick = () => listEl.querySelectorAll('input[type=checkbox]').forEach(cb => cb.checked = true); modal.contentEl.querySelector('[data-act="none"]').onclick = () => listEl.querySelectorAll('input[type=checkbox]').forEach(cb => cb.checked = false); modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); modal.footerEl.querySelector('[data-act="run"]').onclick = async () => { const selected = []; listEl.querySelectorAll('input[type=checkbox]:checked').forEach(cb => { const it = all.find(x => String(x.id) === cb.getAttribute('data-id')); if (it) selected.push(it); }); if (!selected.length) { showToast('请至少勾选一个文件或文件夹', 'warning'); return; } modal.overlay.remove(); await lanzouRunExport(selected); }; } /** 蓝奏云导出主流程(收集 + CDN Content-Md5 零下载取 md5) */ async function lanzouRunExport(selected) { const modal = openModal({ title: '生成秒传 JSON(蓝奏云)', icon: ICONS.generate, width: 640, content(el) { el.innerHTML = `
准备中...
md5 经蓝奏云分享链路从 CDN 响应头直接读取(零下载、快)。
`; }, footer() { return ''; }, }); const statusEl = modal.contentEl.querySelector('#gy-ms-lz-gen-status'); const barEl = modal.contentEl.querySelector('.gy-ms-bar'); const files = []; const warnMissing = []; const counter = { done: 0 }; try { setStatus(statusEl, `正在收集文件信息(${selected.length} 项)...`, 'info'); for (const it of selected) { if (it.isDir) { setStatus(statusEl, `正在扫描文件夹:${it.name}...`, 'info'); await lanzouWalkFolder(it.id, it.name + '/', files, warnMissing, statusEl, counter); } else { files.push({ path: it.name, fileId: it.id, size: parseLanzouSizeStr(it.size), md5: '', sha1: '' }); counter.done++; } } if (!files.length) { setStatus(statusEl, '未收集到任何文件(勾选的文件夹为空?)', 'err'); modal.footerEl.innerHTML = ``; return; } setStatus(statusEl, `正在从蓝奏云 CDN 获取 md5(${files.length} 个文件,零下载)...`, 'info'); const pending = []; for (let i = 0; i < files.length; i++) { const f = files[i]; let md5 = ''; try { md5 = await LANZOU.getServerMd5(f.fileId); } catch (e) { /* 计入 pending 走回退 */ } if (md5 && /^[0-9a-f]{32}$/.test(md5)) { f.md5 = md5; } else { pending.push(f); } updateProgress(barEl, Math.round(((i + 1) / files.length) * 100), `获取 md5 ${i + 1}/${files.length}:${f.path}`); await sleep(CONFIG.md5Delay); } // 蓝奏云仅做导出:CDN 未返回 md5 的直接报错(蓝奏云文件上限 550MB,正常均带头) if (pending.length) { for (const f of pending) { warnMissing.push(`${f.path}:未获取到服务器 md5(CDN 无哈希头或分享链路失败),无法导出`); } } const finalFiles = files.filter(f => f.md5); if (!finalFiles.length) { setStatus(statusEl, '未获取到任何文件的 md5,无法生成 JSON。', 'err'); modal.footerEl.innerHTML = ``; return; } const totalSize = finalFiles.reduce((s, f) => s + (Number(f.size) || 0), 0); const json = { scriptVersion: SCRIPT_VERSION, platform: '蓝奏云', totalFilesCount: finalFiles.length, totalSize, formattedTotalSize: formatSize(totalSize), files: finalFiles.map(f => ({ path: f.path, md5: f.md5, hashName: 'md5', etag: f.md5, size: Number(f.size) || 0, })), }; const jsonStr = JSON.stringify(json, null, 2); const exportName = CONFIG.defaultExportName + '_蓝奏云.json'; modal.contentEl.innerHTML = `
已生成 ${finalFiles.length} 项秒传 JSON(md5,零下载)${warnMissing.length ? `;${warnMissing.length} 项未获取到 md5` : ''}
md5 可直接导入小飞机/蓝奏云优享版等支持 md5 秒传的平台;蓝奏云本身无秒传接口,无法导入。
${escapeHtml(jsonStr)}
`; modal.footerEl.innerHTML = ` `; modal.footerEl.querySelector('[data-act="copy"]').onclick = () => { try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); } catch (e) { const ta = document.createElement('textarea'); ta.value = jsonStr; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); showToast('秒传 JSON 已复制到剪贴板'); } }; modal.footerEl.querySelector('[data-act="download"]').onclick = () => { const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = exportName; a.click(); URL.revokeObjectURL(url); }; modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); } catch (e) { setStatus(statusEl, `生成失败:${e.message}`, 'err'); modal.footerEl.innerHTML = ``; } } // ============================== 蓝奏云优享版导出/导入(v1.6:ilanzou.com,同 openware 框架) ============================== // 设计要点: // - 导出【零下载】:redirect 302 后的 CDN 响应头 Content-Md5(base64 文件 md5)直接取; // 列表不提供哈希,无需下载整个文件。 // - 导入:getUpToken 秒传,upToken==='-1' 创建记录(map.fileId);只认 md5(32hex)。 // 实测任意 md5 都返回 -1 —— md5 不在内容池时记录会指向无关对象(幽灵文件), // 故导入后零下载校验(CDN Content-Md5 对比),不一致则删除记录并报"未命中"。 /** 优享版单文件收集(列表 fileSize 单位 KB,导出统一换算为字节) */ async function ilanzouCollectFile(it, prefix, files, warnMissing) { const name = String(it.fileName || it.file_name || it.name || ''); if (!it.fileId) { warnMissing.push(prefix + name + ':无 fileId'); return; } const kb = Number(it.fileSize || it.size || 0); files.push({ path: prefix + name, fileId: String(it.fileId), size: kb > 0 ? kb * 1024 : 0, folderId: String(it.folderId || ''), md5: '', sha1: '', }); } /** 优享版递归收集目录(file/list 分页) */ async function ilanzouWalkFolder(folderId, prefix, files, warnMissing, statusEl, barEl, counter) { let offset = 1; for (;;) { let r; try { r = await ILANZOU.getFileList(folderId, offset); } catch (e) { setStatus(statusEl, `列出目录失败:${prefix}(${e.message})`, 'err'); return; } const list = r.list || []; for (const it of list) { const isDir = Number(it.fileType) === 2; const name = String(it.fileName || ''); if (isDir) { if (it.fileId) await ilanzouWalkFolder(String(it.fileId), prefix + name + '/', files, warnMissing, statusEl, barEl, counter); } else { await ilanzouCollectFile({ fileId: it.fileId, fileName: it.fileName, fileSize: it.fileSize, folderId: it.folderId }, prefix, files, warnMissing); counter.done++; if (statusEl) setStatus(statusEl, `正在扫描:${prefix}(已收集 ${files.length} 个文件)`, 'info'); if (barEl) updateProgress(barEl, 0, `已收集 ${files.length} 个文件:${name}`); await sleep(CONFIG.md5Delay); } } if (!r.hasMore) break; offset++; await sleep(CONFIG.listPageDelay); } } /** 优享版导出:勾选 → 递归列表 → CDN Content-Md5 取 md5(零下载)→ JSON */ async function ilanzouGenerateJson() { const c = ILANZOU.readCommon(); if (!c) { showToast('未检测到蓝奏云优享版登录态,请刷新页面', 'warning'); return; } let sel; try { sel = ILANZOU.getCheckedItems(); } catch (e) { showToast('读取勾选失败:' + e.message, 'error'); return; } if (!sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; } const modal = openModal({ title: '生成秒传 JSON(蓝奏云优享版)', icon: ICONS.generate, width: 640, content(el) { el.innerHTML = `
准备中...
md5 从优享版 CDN 响应头直接获取(零下载、快)。
`; }, footer() { return ''; }, }); const statusEl = modal.contentEl.querySelector('#gy-ms-ilz-gen-status'); const barEl = modal.contentEl.querySelector('.gy-ms-bar'); const files = []; const warnMissing = []; const counter = { done: 0 }; try { setStatus(statusEl, sel.all ? `已全选当前目录 ${sel.items.length} 项,正在收集信息...` : `正在收集 ${sel.items.length} 项...`, 'info'); for (const item of sel.items) { const name = String(item.fileName || item.name || ''); if (!name) { warnMissing.push('(未命名条目)'); continue; } if (Number(item.fileType) === 2 && item.fileId) { setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info'); await ilanzouWalkFolder(item.fileId, name + '/', files, warnMissing, statusEl, barEl, counter); } else if (item.fileId) { await ilanzouCollectFile(item, '', files, warnMissing); counter.done++; updateProgress(barEl, Math.round((counter.done / Math.max(1, sel.items.length)) * 100), `获取文件信息 ${counter.done}/${sel.items.length}:${name}`); await sleep(CONFIG.md5Delay); } else { warnMissing.push(name + ':未能匹配到文件ID'); } } const withMd5 = files.filter(f => f.fileId); setStatus(statusEl, `正在从 CDN 获取 md5(${withMd5.length} 个文件,零下载)...`, 'info'); const pending = []; for (let i = 0; i < withMd5.length; i++) { const f = withMd5[i]; const md5 = await ILANZOU.getServerMd5(f.fileId); if (md5 && /^[0-9a-f]{32}$/.test(md5)) f.md5 = md5; else pending.push(f); setStatus(statusEl, `获取 md5 ${i + 1}/${withMd5.length}:${f.path}`, 'info'); await sleep(60); } // 优享版仅支持 ≤512MB 零下载取 md5(分片大文件 CDN 无哈希头,不做下载计算) if (pending.length) { for (const f of pending) { const bytes = Number(f.size) || 0; if (bytes > 512 * 1024 * 1024) { warnMissing.push(`${f.path}:超过 512MB 使用限制(${formatSize(bytes)}),优享版导出仅支持 ≤512MB 文件`); } else { warnMissing.push(`${f.path}:CDN 未返回 md5(≤512MB 但无哈希头,无法导出)`); } } } const finalFiles = files.filter(f => f.md5); if (!finalFiles.length) { setStatus(statusEl, '未获取到任何文件的 md5,无法生成 JSON。', 'err'); modal.footerEl.innerHTML = ``; return; } const totalSize = finalFiles.reduce((s, f) => s + (Number(f.size) || 0), 0); const json = { scriptVersion: SCRIPT_VERSION, platform: '蓝奏云优享版', totalFilesCount: finalFiles.length, totalSize, formattedTotalSize: formatSize(totalSize), files: finalFiles.map(f => ({ path: f.path, md5: f.md5, hashName: 'md5', etag: f.md5, size: Number(f.size) || 0, })), }; const jsonStr = JSON.stringify(json, null, 2); const exportName = CONFIG.defaultExportName + '_优享版.json'; modal.contentEl.innerHTML = `
已生成 ${finalFiles.length} 项秒传 JSON(md5,零下载)${warnMissing.length ? `;${warnMissing.length} 项未获取到 md5` : ''}
优享版导出仅支持 ≤512MB 文件(零下载取 md5);md5 可直接导入小飞机/百度/夸克/UC 等支持 md5 秒传的平台;size 已换算为字节(与其他平台 JSON 一致)。
${escapeHtml(jsonStr)}
`; modal.footerEl.innerHTML = ` `; modal.footerEl.querySelector('[data-act="copy"]').onclick = () => { try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); } catch (e) { const ta = document.createElement('textarea'); ta.value = jsonStr; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); showToast('秒传 JSON 已复制到剪贴板'); } }; modal.footerEl.querySelector('[data-act="download"]').onclick = () => { const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = exportName; a.click(); URL.revokeObjectURL(url); }; modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); } catch (e) { setStatus(statusEl, `生成失败:${e.message}`, 'err'); modal.footerEl.innerHTML = ``; } } /** 优享版零下载校验:CDN Content-Md5 与期望 md5 是否一致;返回 '' 表示无法校验(不判幽灵) */ async function ilanzouVerifyMd5(fileId, expectMd5) { try { const real = await ILANZOU.getServerMd5(fileId); return real ? String(real).toLowerCase() === String(expectMd5 || '').toLowerCase() : null; } catch (e) { return null; } } /** 优享版导入(getUpToken 秒传 + CDN Content-Md5 幽灵校验,只认 md5;仅支持 ≤8MB 小文件) */ async function ilanzouImportJson(rawJson, onProgress) { const c = ILANZOU.readCommon(); if (!c) throw new Error('未检测到蓝奏云优享版登录态,请刷新页面'); let obj; try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); } const rows = []; const skippedDetail = []; const ILZ_IMPORT_MAX_BYTES = 8 * 1024 * 1024; // 优享版秒传池仅收录单请求直传的小文件(>8MB 分片不入池) for (const f of (obj.files || [])) { const path = String(f.path || f.name || '').trim(); const md5 = normalizeMd5(f.md5 || f.etag); const size = Number(f.size != null ? f.size : 0); if (!path) continue; if (!md5) { skippedDetail.push(`${path}:无 md5(优享版秒传只认 md5;阿里导出的仅含 sha1 无法导入)`); continue; } if (size > ILZ_IMPORT_MAX_BYTES) { skippedDetail.push(`${path}:超小文件导入限制(${formatSize(size)} > 8MB)。优享版秒传池只收录单请求直传的小文件,>8MB 分片文件无法秒传导入`); continue; } rows.push({ path, md5, size }); } if (!rows.length) throw new Error('JSON 中没有可导入的条目(优享版秒传需要 md5,32 位 hex,且文件需 ≤8MB)'); const rootParent = ILANZOU.getCurrentParentId(); let success = 0, fail = 0, skipped = 0; const failures = []; const warns = []; const total = rows.length; const dirCache = new Map(); for (let i = 0; i < rows.length; i++) { const row = rows[i]; onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `优享版秒传 ${i + 1}/${total}:${row.path}` }); try { const dirSegments = dirSegmentsFromPath(row.path); const fileName = basenameFromPath(row.path); let parent = rootParent; let mkdirOk = true; for (const dirName of dirSegments) { const key = `${parent}::${dirName}`; if (dirCache.has(key)) { parent = dirCache.get(key); continue; } try { const dirId = await ILANZOU.createDir(parent, dirName); dirCache.set(key, dirId); parent = dirId; await sleep(CONFIG.mkdirDelay); } catch (e) { mkdirOk = false; failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`); break; } } if (!mkdirOk) { fail++; continue; } const r = await ILANZOU.rapidUpload(fileName, row.md5, row.size, parent); if (r.hit && r.fileId) { // 幽灵校验:CDN Content-Md5 对比(零下载) const verified = await ilanzouVerifyMd5(r.fileId, row.md5); if (verified === null) { success++; warns.push(`${row.path}:已创建但无法校验(CDN 未返回 md5 头,请自行确认)`); } else if (verified) { success++; } else { try { await ILANZOU.deleteItems([], [r.fileId]); } catch (e) { /* ignore */ } fail++; failures.push(`${row.path}:优享版内容池无该 md5 内容(记录的 CDN md5 不一致,已清理)`); } } else if (r.hit) { fail++; failures.push(`${row.path}:getUpToken 返回命中但无 fileId`); } else { fail++; failures.push(`${row.path}:优享版秒传池未收录该内容(仅服务器有记录的直传小文件可秒传;分片/大文件或非近期上传的内容不在池中,无法秒传导入)`); } } catch (e) { fail++; failures.push(`${row.path}:秒传失败(${e.message})`); } await sleep(CONFIG.transferDelay); } onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` }); mcLog('import', '优享版导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped); if (failures.length) mcLog('import', '失败明细:' + failures.slice(0, 8).join(' | ')); return { total, success, fail, skipped, failures, skippedDetail, warns }; } /** 优享版导入模态框 */ function showIlanzouImportModal() { const modal = openModal({ title: '保存秒传 JSON 到蓝奏云优享版', icon: ICONS.import, width: 640, content(el) { el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:md5=32 位、path、size)。
优享版秒传只认 md5,且仅支持 ≤8MB 小文件(秒传池只收录单请求直传的小文件,>8MB 分片文件无法秒传导入,超出将报错跳过)。夸克/UC/百度/光鸭/蓝奏云/小飞机导出的 JSON(含 md5)可直接秒传;阿里云盘导出的(仅 sha1)将被跳过并提示。
path 中的目录会自动创建;导入后脚本会零下载校验 CDN md5,内容池不存在的会清理并报未命中。
${IMPORT_FILE_ROW_HTML}
`; }, footer(el) { el.innerHTML = ` `; }, }); const ta = modal.contentEl.querySelector('#gy-ms-ilz-import-ta'); const statusEl = modal.contentEl.querySelector('#gy-ms-ilz-import-status'); const barEl = modal.contentEl.querySelector('#gy-ms-ilz-import-bar'); const detailEl = modal.contentEl.querySelector('#gy-ms-ilz-import-detail'); const runBtn = modal.footerEl.querySelector('[data-act="run"]'); modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl); runBtn.onclick = async () => { const vr = validateImportJson(ta.value); if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; } runBtn.disabled = true; runBtn.textContent = '导入中...'; barEl.style.display = 'block'; detailEl.style.display = 'none'; let summary; try { summary = await ilanzouImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); }); } catch (e) { setStatus(statusEl, `导入失败:${e.message}`, 'err'); runBtn.disabled = false; runBtn.textContent = '开始导入'; return; } runBtn.disabled = false; runBtn.textContent = '开始导入'; const detailLines = []; if (summary.failures.length) detailLines.push(...summary.failures); if (summary.warns && summary.warns.length) { detailLines.push('', '========== 提示(无法校验) =========='); detailLines.push(...summary.warns); } if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(无 md5) =========='); detailLines.push(...summary.skippedDetail); } if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; } const warn = summary.fail > 0 || summary.skipped > 0 || (summary.warns && summary.warns.length > 0); setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok'); if (warn) { try { const logBtn = document.createElement('button'); logBtn.className = 'gy-ms-btn gy-ms-btn-outline'; logBtn.textContent = '导出日志'; logBtn.style.marginTop = '8px'; logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); }; statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling); } catch (e) { /* ignore */ } } }; } // ============================== 城通网盘平台(v1.8:drive.ctfile.com 标准版 v4 API,实测 2026-10) ============================== // 实测逆向结论: // - 凭证:sessionStorage['ctfile_basic_token'](Bearer,15 分钟过期);刷新 POST /v4/user/auth/refresh-token(body {},凭 cookie 会话) // - 工作区:POST /v4/workspace/manage/list → workspaces[];路由 #/public=共享(is_private 0)、#/private=私密(is_private 1,超级会员) // - 列表:POST /v4/file/browse/list {workspace_id, folder_id, orderby:'new', filter:'all', keyword:'', start, pagesize:100} // 文件 fid 'f...'、size、file_real_name="{checksum}-{size}";目录 fid 'd...'、ext='folder'、size=0 // 接口 folder_id 参数 = fid 去掉 'd'/'f' 前缀(根为 '0') // - checksum 算法:md5(文件前 1,000,000 字节)(前端 Ua(): FileReader.readAsArrayBuffer(file.slice(0,1e6)) → SparkMD5.ArrayBuffer) // ≤1MB 文件:checksum == 完整内容 MD5(file_real_name 可直接作 etag) // >1MB 文件:checksum ≠ 完整 MD5(不下载计算完整 MD5;ctChecksum 零下载、供城通→城通秒传) // - 建目录:POST /v4/file/manage/create-folder {workspace_id, folder_id, name} → data.folder_id:"d..." // - 秒传:POST /v4/file/manage/upload {workspace_id, folder_id, path, size, checksum, carrier:0} // data.exists===1 / instant_upload===true → 「文件已存在,秒传成功」直接入库(零上传,实测验证) // 实测池收录规则:按 (checksum=md5前1MB, size) 收录网页上传的 ≥约10MB 文件(即时收录无延迟); // 8MB 以下不入池;完整 MD5 永远对不上池键(16/32/64MB 用完整 md5 查询全部 exists:0) // - 删除:POST /v4/file/manage/delete {workspace_id, ids:"fid1,fid2"}(ids 为逗号分隔字符串,实测数组会 500) const CTFILE = { API_BASE: 'https://api.ctfile.com', CHECKSUM_SLICE: 1000000, // 前端只哈希文件前 100 万字节 /** Bearer token:sessionStorage ctfile_basic_token */ getToken() { try { const t = String(sessionStorage.getItem('ctfile_basic_token') || '').trim(); if (t) return t; } catch (e) { /* ignore */ } return ''; }, /** 刷新 Bearer token(cookie 会话有效时直接换新 token) */ async refreshToken() { const j = await CTFILE.request('/v4/user/auth/refresh-token', {}, { noToken: true, autoRetry: false }); const t = j && j.data && j.data.token; if (t) { try { sessionStorage.setItem('ctfile_basic_token', t); } catch (e) { /* ignore */ } } return t || ''; }, /** POST JSON 请求(同源 fetch,cookie 会话 + Bearer;401/403 token 类错误自动刷新重试一次) */ async request(path, body, opts = {}) { const headers = { 'Content-Type': 'application/json', 'Origin': 'https://drive.ctfile.com', 'Referer': 'https://drive.ctfile.com/' }; if (!opts.noToken) { let token = CTFILE.getToken(); if (!token) token = await CTFILE.refreshToken(); if (token) headers['Authorization'] = 'Bearer ' + token; } let resp; try { resp = await fetch(CTFILE.API_BASE + path, { method: 'POST', headers, credentials: 'include', body: JSON.stringify(body || {}) }); } catch (e) { throw new Error('城通网络请求失败: ' + e.message); } let j; try { j = await resp.json(); } catch (e) { throw new Error('城通 HTTP ' + resp.status + ' 响应解析失败'); } const badAuth = resp.status === 401 || (resp.status === 403 && j && /token|auth|session|login/i.test(String(j.message || ''))); if (resp.status < 200 || resp.status >= 300 || (j && j.code && j.code !== 200)) { if (!opts.noToken && opts.autoRetry !== false && !opts.retried && badAuth) { const t2 = await CTFILE.refreshToken(); if (t2) return CTFILE.request(path, body, Object.assign({}, opts, { retried: true })); } const err = new Error((j && j.message) || ('城通接口错误 code=' + (j && j.code) + ' HTTP ' + resp.status)); err.code = j && j.code; throw err; } return j; }, /** 工作区列表 → {shared, private}(is_private 0=共享空间,1=私密空间) */ async getWorkspaces() { const j = await CTFILE.request('/v4/workspace/manage/list', {}); const list = Array.isArray(j.data && j.data.workspaces) ? j.data.workspaces : []; return { shared: list.find(w => Number(w.is_private) === 0) || null, private: list.find(w => Number(w.is_private) === 1) || null, list, }; }, /** 当前路由对应的工作区 id(#/public→共享、#/private→私密) */ async getWorkspaceId() { const ws = await CTFILE.getWorkspaces(); const priv = /#\/private/.test(location.hash || ''); const w = priv ? ws.private : ws.shared; if (!w) throw new Error('未获取到城通工作区' + (priv ? '(私密空间为超级会员功能,免费账号不可用)' : ',请确认已登录并刷新页面')); return String(w.id); }, /** 当前目录 folder_id(路由 #/public/{id} 或 #/private/{id};根为 '0') */ getCurrentFolderId() { try { const m = (location.hash || '').match(/(?:\/public|\/private)\/(\d+)/); return m ? m[1] : '0'; } catch (e) { return '0'; } }, /** folder_id 规范化:fid('d...'/'f...') → 纯数字(接口参数);根 → '0' */ normFolderId(folderId) { const s = String(folderId == null || folderId === '' ? '0' : folderId).trim(); return s === '0' || /^\d+$/.test(s) ? s : s.replace(/^[df]/, ''); }, /** 文件列表(分页):返回 {list, total};条目为原始结果 */ async getFileList(workspaceId, folderId, start) { const j = await CTFILE.request('/v4/file/browse/list', { workspace_id: Number(workspaceId || 0), folder_id: CTFILE.normFolderId(folderId), orderby: 'new', filter: 'all', keyword: '', start: Number(start || 0), pagesize: 100, }); const d = (j && j.data) || {}; return { list: Array.isArray(d.results) ? d.results : [], total: Number(d.totalNum || 0), }; }, /** 从列表条目取 checksum(file_real_name="{checksum}-{size}")+ size */ parseItemMeta(it) { const m = String(it.file_real_name || '').match(/^([0-9a-fA-F]{32})-(\d+)$/); if (m) return { checksum: m[1].toLowerCase(), size: Number(m[2]) }; return { checksum: '', size: Number(it.size || 0) }; }, /** 建目录:返回 folder_id('d...',用于后续列表需去掉前缀) */ async createFolder(workspaceId, folderId, name) { const j = await CTFILE.request('/v4/file/manage/create-folder', { workspace_id: Number(workspaceId || 0), folder_id: CTFILE.normFolderId(folderId), name: String(name), }); const id = j && j.data && j.data.folder_id; if (!id) throw new Error('城通创建目录未返回 ID'); return String(id); }, /** 秒传(导入):checksum 用 ctChecksum 优先、否则完整 md5;返回 {instant, exists, uploadUrl, fileId, name} */ async uploadInit(workspaceId, folderId, name, size, checksum) { const j = await CTFILE.request('/v4/file/manage/upload', { workspace_id: Number(workspaceId || 0), folder_id: CTFILE.normFolderId(folderId), path: String(name), size: Number(size || 0), checksum: String(checksum || '').toLowerCase(), carrier: 0, }); const d = (j && j.data) || {}; return { instant: Number(d.exists) === 1 || d.instant_upload === true, exists: Number(d.exists || 0), uploadUrl: String(d.uploadUrl || ''), fileId: String(d.file_id || ''), name: String(d.file_name || name || ''), }; }, /** 删除(ids 为逗号分隔字符串) */ async deleteItems(workspaceId, idsStr) { const j = await CTFILE.request('/v4/file/manage/delete', { workspace_id: Number(workspaceId || 0), ids: String(idsStr || '') }); return j; }, }; // ---- 城通导出(勾选 → 递归列表 → file_real_name 取 checksum;≤1MB 即完整 md5,>1MB 仅 ctChecksum)---- /** 城通行文件名提取:.ct-entry-name > .ct-entry-text > button.ct-entry(去掉行内 svg 类型徽标文本) */ function ctfileRowName(row) { const el = row.querySelector('.ct-cell-name .ct-entry-name') || row.querySelector('.ct-cell-name .ct-entry-text') || row.querySelector('.ct-cell-name button.ct-entry'); if (!el) return ''; let t = (el.textContent || '').replace(/\s+/g, ' ').trim(); // 兜底(仅 button 路径):去掉文件名前的类型徽标(如 BIN/TXT/MP4 等大写短词) if (!row.querySelector('.ct-cell-name .ct-entry-name, .ct-cell-name .ct-entry-text')) { t = t.replace(/^[A-Z0-9]{1,8}(?=.)/, ''); } return t; } /** 城通勾选读取:行复选框(.ct-row:not(.is-head) input.ct-check)+ 表头全选 */ function ctfileGetCheckedItems() { const names = []; document.querySelectorAll('.ct-row:not(.is-head)').forEach(row => { const cb = row.querySelector('input.ct-check'); if (cb && cb.checked) { const name = ctfileRowName(row); if (name) names.push(name); } }); const headCb = document.querySelector('.ct-row.is-head input.ct-check'); const all = !!(headCb && headCb.checked); return { names, all }; } /** 按名称匹配当前目录 API 列表 → 勾选项条目(含 fid/ext/size/file_real_name) */ async function ctfileResolveCheckedItems(workspaceId, folderId, names, all) { const items = []; let start = 0; for (;;) { const r = await CTFILE.getFileList(workspaceId, folderId, start); const list = r.list || []; for (const it of list) { const nm = String(it.name || ''); if (all || names.includes(nm)) items.push(it); } if (!list.length || start + list.length >= r.total) break; start += list.length; } return items; } /** 城通单文件收集 */ async function ctfileCollectFile(it, prefix, files, warnMissing) { const name = String(it.name || ''); const meta = CTFILE.parseItemMeta(it); if (meta.checksum) { files.push({ path: prefix + name, fileId: String(it.fid || ''), size: meta.size || Number(it.size || 0), ctChecksum: meta.checksum, // md5(前1MB),零下载;城通→城通秒传键 etag: '', // 完整内容 MD5(仅 ≤1MB:ctChecksum 即完整 MD5,由主流程补齐) }); } else { warnMissing.push(prefix + name + ':无 file_real_name,无法取哈希'); } } /** 城通递归收集目录(browse/list 分页) */ async function ctfileWalkFolder(workspaceId, folderId, prefix, files, warnMissing, statusEl, barEl, counter) { let start = 0; for (;;) { let r; try { r = await CTFILE.getFileList(workspaceId, folderId, start); } catch (e) { setStatus(statusEl, `列出目录失败:${prefix}(${e.message})`, 'err'); return; } const list = r.list || []; for (const it of list) { const isDir = String(it.ext) === 'folder'; const name = String(it.name || ''); if (isDir) { if (it.fid) await ctfileWalkFolder(workspaceId, it.fid, prefix + name + '/', files, warnMissing, statusEl, barEl, counter); } else { await ctfileCollectFile(it, prefix, files, warnMissing); counter.done++; if (statusEl) setStatus(statusEl, `正在扫描:${prefix}(已收集 ${files.length} 个文件)`, 'info'); if (barEl) updateProgress(barEl, 0, `已收集 ${files.length} 个文件:${name}`); await sleep(CONFIG.md5Delay); } } if (!list.length || start + list.length >= r.total) break; start += list.length; await sleep(CONFIG.listPageDelay); } } /** 城通导出主流程 */ async function ctfileGenerateJson() { const workspaceId = await CTFILE.getWorkspaceId(); const folderId = CTFILE.getCurrentFolderId(); const sel = ctfileGetCheckedItems(); if (!sel.names.length && !sel.all) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; } const modal = openModal({ title: '生成秒传 JSON(城通网盘)', icon: ICONS.generate, width: 640, content(el) { el.innerHTML = `
准备中...
城通哈希 = md5(文件前1MB):≤1MB 即完整内容 MD5(同时带 etag,可导入其他盘);>1MB 仅 ctChecksum(零下载),可直接导入城通秒传。
`; }, footer() { return ''; }, }); const statusEl = modal.contentEl.querySelector('#gy-ms-ct-gen-status'); const barEl = modal.contentEl.querySelector('.gy-ms-bar'); const files = []; const warnMissing = []; const counter = { done: 0 }; try { setStatus(statusEl, `正在匹配勾选项(当前目录 ${folderId === '0' ? '根目录' : folderId})...`, 'info'); const items = await ctfileResolveCheckedItems(workspaceId, folderId, sel.names, sel.all); if (!items.length) { setStatus(statusEl, '未能匹配到勾选条目,请刷新页面重试', 'err'); modal.footerEl.innerHTML = ``; return; } setStatus(statusEl, sel.all ? `已全选当前目录 ${items.length} 项,正在收集信息...` : `正在收集 ${items.length} 项...`, 'info'); for (const item of items) { const name = String(item.name || ''); if (!name) { warnMissing.push('(未命名条目)'); continue; } if (String(item.ext) === 'folder' && item.fid) { setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info'); await ctfileWalkFolder(workspaceId, item.fid, name + '/', files, warnMissing, statusEl, barEl, counter); } else if (item.fid) { await ctfileCollectFile(item, '', files, warnMissing); counter.done++; updateProgress(barEl, Math.round((counter.done / Math.max(1, items.length)) * 100), `获取文件信息 ${counter.done}/${items.length}:${name}`); await sleep(CONFIG.md5Delay); } else { warnMissing.push(name + ':未能匹配到文件ID'); } } // ≤1MB:ctChecksum 即完整内容 MD5,直接补 etag;>1MB 仅 ctChecksum(城通秒传专用键,不下载计算完整 MD5) for (const f of files) { if (f.size <= CTFILE.CHECKSUM_SLICE) { f.etag = f.ctChecksum; } } const finalFiles = files.filter(f => f.ctChecksum); if (!finalFiles.length) { setStatus(statusEl, '未获取到任何文件的哈希,无法生成 JSON。', 'err'); modal.footerEl.innerHTML = ``; return; } const totalSize = finalFiles.reduce((s, f) => s + (Number(f.size) || 0), 0); const json = { scriptVersion: SCRIPT_VERSION, platform: '城通网盘', totalFilesCount: finalFiles.length, totalSize, formattedTotalSize: formatSize(totalSize), files: finalFiles.map(f => { const o = { path: f.path, size: Number(f.size) || 0 }; if (f.etag) { o.md5 = f.etag; o.hashName = 'md5'; o.etag = f.etag; } o.ctChecksum = f.ctChecksum; // 城通秒传键:md5(前1MB) return o; }), }; const jsonStr = JSON.stringify(json, null, 2); const exportName = CONFIG.defaultExportName + '_城通.json'; const bigMissing = finalFiles.filter(f => f.size > CTFILE.CHECKSUM_SLICE && !f.etag).length; modal.contentEl.innerHTML = `
已生成 ${finalFiles.length} 项秒传 JSON(ctChecksum 零下载)${warnMissing.length ? `;${warnMissing.length} 项异常` : ''}
${bigMissing ? `${bigMissing} 项 >1MB 仅含 ctChecksum(可导入城通秒传;不含完整 MD5,无法导入其他盘)` : '含完整 MD5(etag),可导入百度/夸克/UC/小飞机等支持 md5 的平台'}
${escapeHtml(jsonStr)}
`; modal.footerEl.innerHTML = ` `; modal.footerEl.querySelector('[data-act="copy"]').onclick = () => { try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); } catch (e) { const ta = document.createElement('textarea'); ta.value = jsonStr; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); showToast('秒传 JSON 已复制到剪贴板'); } }; modal.footerEl.querySelector('[data-act="download"]').onclick = () => { const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = exportName; a.click(); URL.revokeObjectURL(url); }; modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); } catch (e) { setStatus(statusEl, `生成失败:${e.message}`, 'err'); modal.footerEl.innerHTML = ``; } } /** 城通导入(/v4/file/manage/upload checksum 秒传;优先 ctChecksum,否则 etag/md5;未命中如实报告) */ async function ctfileImportJson(rawJson, onProgress) { const workspaceId = await CTFILE.getWorkspaceId(); let obj; try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); } const rows = []; const skippedDetail = []; for (const f of (obj.files || [])) { const path = String(f.path || f.name || '').trim(); const md5 = normalizeMd5(f.md5 || f.etag); const ctChecksum = normalizeMd5(f.ctChecksum); const size = Number(f.size != null ? f.size : 0); if (!path) continue; if (!md5 && !ctChecksum) { skippedDetail.push(`${path}:无 md5/ctChecksum(城通秒传需要 md5 或 ctChecksum;阿里导出的仅含 sha1 无法导入)`); continue; } rows.push({ path, md5, ctChecksum, size }); } if (!rows.length) throw new Error('JSON 中没有可导入的条目(城通秒传需要 md5/ctChecksum)'); const rootFolder = CTFILE.getCurrentFolderId(); let success = 0, fail = 0, skipped = 0; const failures = []; const total = rows.length; const dirCache = new Map(); for (let i = 0; i < rows.length; i++) { const row = rows[i]; onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `城通秒传 ${i + 1}/${total}:${row.path}` }); try { const dirSegments = dirSegmentsFromPath(row.path); const fileName = basenameFromPath(row.path); let parent = rootFolder; let mkdirOk = true; for (const dirName of dirSegments) { const key = `${parent}::${dirName}`; if (dirCache.has(key)) { parent = dirCache.get(key); continue; } try { const dirId = await CTFILE.createFolder(workspaceId, parent, dirName); dirCache.set(key, dirId); parent = dirId; await sleep(CONFIG.mkdirDelay); } catch (e) { mkdirOk = false; failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`); break; } } if (!mkdirOk) { fail++; continue; } // 秒传:优先 ctChecksum(城通导出的前1MB哈希),否则完整 md5(仅 ≤1MB 文件与池键一致) const checksum = row.ctChecksum || row.md5; const r = await CTFILE.uploadInit(workspaceId, parent, fileName, row.size, checksum); if (r.instant) { success++; } else { fail++; failures.push(`${row.path}:城通秒传池未命中(池按 md5(前1MB) 收录 ≥约10MB 的已上传文件;小文件/未收录内容需真实上传,JSON 不含文件本体无法上传)`); } } catch (e) { fail++; failures.push(`${row.path}:秒传失败(${e.message})`); } await sleep(CONFIG.transferDelay); } onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` }); mcLog('import', '城通导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped); if (failures.length) mcLog('import', '失败明细:' + failures.slice(0, 8).join(' | ')); return { total, success, fail, skipped, failures, skippedDetail }; } /** 城通导入模态框 */ function showCtfileImportModal() { const modal = openModal({ title: '保存秒传 JSON 到城通网盘', icon: ICONS.import, width: 640, content(el) { el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:md5/etag=32 位完整 MD5、ctChecksum=城通前1MB 哈希、path、size)。
城通秒传优先 ctChecksum(城通导出的 JSON 自带),其次 md5。实测池按 md5(前1MB) 收录 ≥约10MB 的已上传文件——城通导出的大文件(ctChecksum)可秒传;<1MB 或新内容一般未命中。
path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`; }, footer(el) { el.innerHTML = ` `; }, }); const ta = modal.contentEl.querySelector('#gy-ms-ct-import-ta'); const statusEl = modal.contentEl.querySelector('#gy-ms-ct-import-status'); const barEl = modal.contentEl.querySelector('#gy-ms-ct-import-bar'); const detailEl = modal.contentEl.querySelector('#gy-ms-ct-import-detail'); const runBtn = modal.footerEl.querySelector('[data-act="run"]'); modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl); runBtn.onclick = async () => { const vr = validateImportJson(ta.value); if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; } runBtn.disabled = true; runBtn.textContent = '导入中...'; barEl.style.display = 'block'; detailEl.style.display = 'none'; let summary; try { summary = await ctfileImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); }); } catch (e) { setStatus(statusEl, `导入失败:${e.message}`, 'err'); runBtn.disabled = false; runBtn.textContent = '开始导入'; return; } runBtn.disabled = false; runBtn.textContent = '开始导入'; const detailLines = []; if (summary.failures.length) detailLines.push(...summary.failures); if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(无 md5/ctChecksum) =========='); detailLines.push(...summary.skippedDetail); } if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; } const warn = summary.fail > 0 || summary.skipped > 0; setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok'); if (warn) { try { const logBtn = document.createElement('button'); logBtn.className = 'gy-ms-btn gy-ms-btn-outline'; logBtn.textContent = '导出日志'; logBtn.style.marginTop = '8px'; logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); }; statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling); } catch (e) { /* ignore */ } } }; } // ============================== 阿里云盘导出/导入(v1.1) ============================== // 设计要点:JSON 保持轻量(只含 sha1/size/path/fileId,不内嵌文件字节,可直接发社交平台); // 验证块(proof 8 字节)经夸克/UC 中转获取(或同账号直取)——导入者用自己 token 算 offset 后取块,零服务器依赖。 /** 阿里云盘导出:勾选 → 列表(含 content_hash=SHA1) → 轻量 JSON(同账号直取/夸克 UC 中转取块) */ async function aliyunGenerateJson() { const t = ALIYUN.getToken(); if (!t) { showToast('未检测到阿里云盘登录态,请刷新页面', 'warning'); return; } const driveId = await ALIYUN.getDriveId(); if (!driveId) { showToast('未能获取阿里云盘 drive_id,请刷新页面', 'warning'); return; } const sel = ALIYUN.getCheckedItems(); let items = sel.items.slice(); if (sel.all) { showToast('检测到全选,正在拉取当前目录全部条目...', 'info', 2000); const parentId = ALIYUN.getCurrentParentId(); try { let marker = ''; for (;;) { const r = await ALIYUN.getFileList(driveId, parentId, marker); for (const it of r.list) { items.push(ALIYUN.normalizeListItem(it)); } if (!r.nextMarker) break; marker = r.nextMarker; await sleep(CONFIG.listPageDelay); } } catch (e) { showToast(`拉取目录失败:${e.message}`, 'error'); return; } } // 去重 const seen = new Set(); items = items.filter(it => { const k = String(it.fileId || it.name); if (!k || seen.has(k)) return false; seen.add(k); return true; }); if (!items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; } const modal = openModal({ title: '生成秒传 JSON(阿里云盘)', icon: ICONS.generate, width: 620, content(el) { el.innerHTML = `
准备中...
`; }, footer() { return ''; }, }); const statusEl = modal.contentEl.querySelector('#gy-ms-al-gen-status'); const barEl = modal.contentEl.querySelector('.gy-ms-bar'); const files = []; const warnMissing = []; const counter = { done: 0 }; const total = items.length; try { setStatus(statusEl, `正在获取 ${items.length} 项的文件信息...`, 'info'); for (const it of items) { if (it.type === 'folder') { setStatus(statusEl, `正在扫描文件夹:${it.name}...`, 'info'); await aliyunWalkFolder(driveId, it.fileId, it.name + '/', files, warnMissing, statusEl, barEl, counter); } else { await aliyunCollectFile(driveId, it, '', files, warnMissing); counter.done++; updateProgress(barEl, Math.round((counter.done / Math.max(1, total)) * 100), `获取文件信息 ${counter.done}/${total}:${it.name}`); await sleep(CONFIG.md5Delay); } } if (!files.length) { setStatus(statusEl, '未获取到任何文件的 SHA1 哈希,无法生成 JSON。', 'err'); modal.footerEl.innerHTML = ``; return; } const totalSize = files.reduce((s, f) => s + (Number(f.size) || 0), 0); const json = { scriptVersion: SCRIPT_VERSION, platform: '阿里云盘', totalFilesCount: files.length, totalSize, formattedTotalSize: formatSize(totalSize), files, }; const jsonStr = JSON.stringify(json, null, 2); const exportName = CONFIG.defaultExportName + '_阿里云盘.json'; modal.contentEl.innerHTML = `
已生成 ${files.length} 项秒传 JSON(SHA1)${warnMissing.length ? `;${warnMissing.length} 项未获取到 SHA1` : ''}
JSON 含 fileId:同账号导入可直接秒传;跨盘导入在阿里导入框粘贴夸克/UC Cookie 即可(不需云服务器)。
${escapeHtml(jsonStr)}
`; modal.footerEl.innerHTML = ` `; modal.footerEl.querySelector('[data-act="copy"]').onclick = () => { try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); } catch (e) { const ta = document.createElement('textarea'); ta.value = jsonStr; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); showToast('秒传 JSON 已复制到剪贴板'); } }; modal.footerEl.querySelector('[data-act="download"]').onclick = () => { const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = exportName; a.click(); URL.revokeObjectURL(url); }; modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); } catch (e) { setStatus(statusEl, `生成失败:${e.message}`, 'err'); modal.footerEl.innerHTML = ``; } } /** 阿里云盘单文件收集:content_hash(SHA1) + size + fileId → 轻量条目 */ async function aliyunCollectFile(driveId, it, prefix, files, warnMissing) { const sha1 = String(it.contentHash || it.content_hash || '').toUpperCase(); const name = String(it.name || ''); if (sha1 && /^[0-9A-F]{40}$/.test(sha1)) { files.push({ path: prefix + name, sha1: sha1.toLowerCase(), hashName: 'sha1', etag: '', size: Number(it.size || 0), driveId: String(it.driveId || driveId || ''), fileId: String(it.fileId || ''), }); } else { warnMissing.push(prefix + name + ':未能取得合法 SHA1(可能仍在转码/未完成)'); } } /** 阿里云盘递归收集目录下所有文件(v3 list 分页) */ async function aliyunWalkFolder(driveId, folderId, prefix, files, warnMissing, statusEl, barEl, counter) { let marker = ''; for (;;) { let list, next; try { const r = await ALIYUN.getFileList(driveId, folderId, marker); list = r.list; next = r.nextMarker; } catch (e) { setStatus(statusEl, `列出目录失败:${prefix}(${e.message})`, 'err'); return; } for (const item of list) { const it = ALIYUN.normalizeListItem(item); if (it.type === 'folder') { if (it.fileId) await aliyunWalkFolder(driveId, it.fileId, prefix + it.name + '/', files, warnMissing, statusEl, barEl, counter); } else { await aliyunCollectFile(driveId, it, prefix, files, warnMissing); counter.done++; if (statusEl) setStatus(statusEl, `正在扫描:${prefix}(已收集 ${files.length} 个文件)`, 'info'); if (barEl) updateProgress(barEl, 0, `已收集 ${files.length} 个文件:${it.name}`); await sleep(CONFIG.md5Delay); } } if (!next) break; marker = next; await sleep(CONFIG.listPageDelay); } } /** * 阿里导入时经「夸克/UC 中转」取 proof 块(用户提供夸克或 UC 的完整 Cookie): * 1) 用 sha1(全局键)调夸克/UC upload/pre 秒传 → 命中返回 fid(不下载原文件) * 2) 用 fid 调 download 拿 download_url → Range 取 [offset, offset+8) 8 字节 = proof * 这样阿里导入完全不需要云服务器,跨账号秒传由夸克/UC 的 PDS 全局去重兜底。 * cookie 决定走夸克还是 UC(按 hostname 判断)。 */ /** * 夸克/UC(阿里云 PDS 生态)通用中转取字节:先把文件(按 sha1 全局键)丢给夸克/UC 秒传 * (夸克/UC 秒传宽松,只需 sha1+size 即命中,内容在全库即登记成功)→ download 拿临时直链 → * Range 取 [start,end] 任意范围字节。115 二次验证的 sign_check 范围即用此中转; * 所有底层用阿里云的网盘(夸克/UC 等)都可当此"sha1 登记中转站"。 * cookie 前缀 QK:/UC: 区分平台;直链只在内存、用完即弃,全程不产生分享链接。 */ async function pdsProxyFetchRange(cookie, sha1, size, start, end) { if (!cookie) throw new Error('未提供夸克/UC Cookie'); // 平台判定:前缀 QK:/UC: 显式区分(cookie 内无法可靠区分夸克/UC) let platform = 'quark'; if (/^UC:/i.test(cookie)) { platform = 'uc'; cookie = cookie.replace(/^UC:/i, '').trim(); } else if (/^QK:/i.test(cookie)) { platform = 'quark'; cookie = cookie.replace(/^QK:/i, '').trim(); } const host = platform === 'uc' ? 'https://pc-api.uc.cn' : 'https://drive-pc.quark.cn'; const pr = platform === 'uc' ? 'UCBrowser' : 'ucpro'; const referer = platform === 'uc' ? 'https://drive.uc.cn/' : 'https://pan.quark.cn/'; const ua = navigator.userAgent; // 统一请求(夸克/UC API 跨域 → 必须 GM_xmlhttpRequest,页面 fetch 会被 CORS 拦截) const gmReq = (opts) => new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('无 GM_xmlhttpRequest')); return; } GM_xmlhttpRequest(Object.assign({ timeout: 60000, withCredentials: true, onload: (r) => resolve(r), onerror: () => reject(new Error('网络错误')), ontimeout: () => reject(new Error('请求超时')), }, opts)); }); // 1) + 2) 秒传 → 取 fid → download 拿直链(可重试单元): // 实测:秒传响应必有 fid(finish:true + upload_id:rapid-*),download 按 fid 可取直链; // 但偶发「file not found」(秒传新建文件尚未就绪/被清理)→ 用新文件名重新秒传换新 fid 重试。 // 文件名带随机后缀,避免同名 auto_rename 干扰。 const doPre = async (name) => { const body = { pdir_fid: '0', dir_name: '', size: Number(size) || 0, file_name: name, format_type: 'application/octet-stream', l_updated_at: Date.now(), l_created_at: Date.now(), ccp_hash_update: true, parallel_upload: true, sha1: String(sha1).toLowerCase(), }; const resp = await gmReq({ method: 'POST', url: host + '/1/clouddrive/file/upload/pre?pr=' + pr + '&fr=pc', headers: { 'Content-Type': 'application/json', 'Referer': referer, 'Origin': referer.replace(/\/$/, ''), 'Cookie': cookie, 'User-Agent': ua }, data: JSON.stringify(body), }); const j = JSON.parse(resp.responseText || '{}'); if (j.code !== 0 && j.code !== 200 && !j.data) throw new Error('夸克/UC 秒传失败: ' + (j.message || ('code ' + j.code))); return j.data || {}; }; const doDl = async (fidNow) => { const resp = await gmReq({ method: 'POST', url: host + '/1/clouddrive/file/download?pr=' + pr + '&fr=pc', headers: { 'Content-Type': 'application/json', 'Referer': referer, 'Origin': referer.replace(/\/$/, ''), 'Cookie': cookie, 'User-Agent': ua }, data: JSON.stringify({ fids: [fidNow] }), }); const j = JSON.parse(resp.responseText || '{}'); if (j.code !== undefined && j.code !== 0 && j.code !== 200) { return { code: j.code, message: String(j.message || '') }; } const arr = Array.isArray(j.data) ? j.data : []; if (!arr.length) return { code: 21001, message: 'empty data' }; const dd = arr[0] || {}; return { code: 0, url: dd.download_url || '', message: '' }; }; let url = ''; let lastPreErr = ''; for (let attempt = 1; attempt <= 3 && !url; attempt++) { const tmpName = 'proof_proxy_' + String(sha1).slice(0, 8) + '_' + Date.now().toString(36) + '.bin'; let pd; try { pd = await doPre(tmpName); } catch (e) { lastPreErr = e.message; await sleep(500); continue; } const fid = String(pd.fid || pd.file_id || ''); if (!fid) { lastPreErr = '秒传未返回 fid(finish=' + pd.finish + ')'; await sleep(500); continue; } let dl = await doDl(fid); if (dl.code !== 0) { if (dl.code === 21001 || dl.code === 30107 || dl.message === 'empty data') { lastPreErr = dl.message; await sleep(600); continue; } if (/login|auth|token|31001|401/i.test(dl.message)) { throw new Error('夸克/UC Cookie 无效或已过期(download 接口拒绝:' + dl.message + '),请重新获取 Cookie 后重试'); } throw new Error('夸克/UC download 接口错误: ' + dl.message + '(code ' + dl.code + ')'); } if (!dl.url) { lastPreErr = 'download 未返回直链'; await sleep(600); continue; } url = dl.url; } if (!url) { throw new Error('夸克/UC 中转取字节失败:' + (lastPreErr || '未知错误') + ' —— 请确认 Cookie 有效且内容在夸克/UC 池中后重试'); } // 3) Range 取 [start, end] 字节 // 关键:夸克/UC 的 OSS 直链(Tengine)拒绝 XMLHttpRequest 请求(一律 412 Precondition Failed), // 只接受 fetch 发起的请求(实测 fetch 206 vs XHR 412,与 cookie/头无关)。 // 因此这里优先用页面 fetch(CDN 下载链接通常开放 CORS,跨域可访问);若被 CORS 拦截 // 再回退 GM_xmlhttpRequest(部分环境可通)。 const rEnd = Math.min(Number(end), Math.max(Number(size) - 1, Number(start))); let rng = null; try { const fr = await fetch(String(url), { method: 'GET', credentials: 'include', headers: { 'Range': `bytes=${start}-${rEnd}`, 'Referer': referer }, }); if (fr.status >= 200 && fr.status < 300) { const ab = await fr.arrayBuffer(); return new Uint8Array(ab); } } catch (e) { /* 回退 GM */ } try { rng = await gmReq({ method: 'GET', url: String(url), headers: { 'Range': `bytes=${start}-${rEnd}`, 'Referer': referer }, responseType: 'arraybuffer', }); } catch (e) { throw new Error('夸克/UC 直链下载失败: ' + e.message); } if (rng.status < 200 || rng.status >= 300) throw new Error('夸克/UC 直链下载 HTTP ' + rng.status); const raw = rng.response; let bytes; if (raw instanceof ArrayBuffer) bytes = new Uint8Array(raw); else if (Array.isArray(raw)) bytes = new Uint8Array(raw); else if (raw && raw.byteLength != null) bytes = new Uint8Array(raw); else if (typeof raw === 'string') bytes = new Uint8Array(raw.split('').map(c => c.charCodeAt(0) & 0xFF)); else throw new Error('夸克/UC 直链下载返回类型异常'); return bytes; } /** 兼容旧调用:取 8 字节 proof 块(阿里导入用;固定 [offset, offset+7]) */ async function pdsProxyFetchProof(cookie, sha1, size, offset) { return pdsProxyFetchRange(cookie, sha1, size, offset, Math.min(offset + 7, Math.max(Number(size) - 1, offset))); } function pdsCookieAutoFetch() { if (!(isQuark() || isUc())) return; let pending = ''; try { pending = String(GM_getValue('__pds_cookie_pending', '') || ''); } catch (e) { /* ignore */ } if (!pending) return; // 清 pending(一次性),无论成功失败 try { GM_setValue('__pds_cookie_pending', ''); } catch (e) { /* ignore */ } const platform = pending === 'uc' ? 'uc' : 'quark'; if ((platform === 'uc' && !isUc()) || (platform === 'quark' && !isQuark())) return; // 开错了站 const take = () => { const isLoggedIn = (() => { try { // 登录态判断:关键 cookie 存在(b-user-id/ctoken/__puus),且尝试取列表成功 const ck = document.cookie || ''; if (!/b-user-id=/.test(ck) && !/ctoken=/.test(ck) && !/__puus=/.test(ck)) return false; return true; } catch (e) { return false; } })(); if (!isLoggedIn) { showToast('未登录:请先在' + (platform === 'uc' ? 'UC网盘' : '夸克网盘') + '登录,登录后会自动获取 Cookie', 'warning', 6000); return; } // 已登录:取 document.cookie(夸克/UC 的关键会话 cookie 均为非 HttpOnly,前实测可见) const cookieStr = (document.cookie || '').trim(); if (!cookieStr) { showToast('未获取到 Cookie,请刷新页面后重试', 'warning'); return; } const prefix = platform === 'uc' ? 'UC:' : 'QK:'; try { GM_setValue('__pds_cookie_result', prefix + cookieStr); } catch (e) { /* ignore */ } showToast('Cookie 已获取,正在返回阿里云盘导入页...', 'ok', 4000); // 尝试自动切回:本标签由 window.open 打开且非用户主动 → 可尝试 close;失败则提示 setTimeout(() => { try { if (window.opener && !window.opener.closed) { window.close(); } } catch (e) { /* ignore */ } }, 1200); }; // 稍等页面就绪(SPA 加载后 cookie 完整) setTimeout(take, 1200); } /** 阿里云盘导入(证明块经夸克/UC 中转或同账号直取,createWithFolders rapid_upload) */ async function aliyunImportJson(rawJson, opts, onProgress) { const proxyCookie = opts && opts.proxyCookie ? String(opts.proxyCookie).trim() : ''; let obj; try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); } const rows = []; for (const f of (obj.files || [])) { const path = String(f.path || f.name || '').trim(); // 优先 sha1(hashName=sha1 或 sha1 字段);兼容阿里导出的轻量 JSON。 // 注意:sha1 是 40 位 hex,不能走 MD5 专用的 normalizeMd5(会把 sha1 清空) const rawSha1 = String(f.sha1 || '').trim(); const rawEtag = String(f.etag || '').trim(); let sha1 = /^[0-9a-f]{40}$/i.test(rawSha1) ? rawSha1.toLowerCase() : ''; if (!sha1 && String(f.hashName || '') === 'sha1' && /^[0-9a-f]{40}$/i.test(rawEtag)) { sha1 = rawEtag.toLowerCase(); } const size = Number(f.size != null ? f.size : 0); if (!path || !/^[0-9a-f]{40}$/.test(sha1)) continue; rows.push({ path, sha1: sha1.toUpperCase(), size, fileId: String(f.fileId || f.file_id || '').trim() }); } if (!rows.length) throw new Error('JSON 中没有可导入的文件条目(需要 sha1 + hashName=sha1 或 sha1 字段)'); const t = ALIYUN.getToken(); if (!t) throw new Error('未检测到阿里云盘登录态,请刷新页面'); const driveId = await ALIYUN.getDriveId(); if (!driveId) throw new Error('未能获取阿里云盘 drive_id,请刷新页面'); const currentParent = ALIYUN.getCurrentParentId(); const accessToken = t.access_token; let success = 0, fail = 0, skipped = 0; const failures = []; const skippedDetail = []; const total = rows.length; for (let i = 0; i < rows.length; i++) { const row = rows[i]; onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `阿里秒传 ${i + 1}/${total}:${row.path}` }); try { const size = row.size > 0 ? row.size : 0; // 1) 导入者用自己的 token 算 offset(与创建请求的 Authorization 一致) const offset = ALIYUN.computeProofOffset(accessToken, size); // 2) 取 8 字节 proof 块(零服务器依赖): // a) JSON 自带 fileId(阿里同账号导出的 JSON)→ 直接从自己网盘取块 // b) 否则 → 经夸克/UC 中转(用户提供 cookie,先把文件按 sha1 秒传到夸克/UC 再取块) let proofBytes = null; let proofSource = ''; if (row.fileId) { try { proofBytes = await ALIYUN.downloadProofRange(driveId, row.fileId, offset); proofSource = '同账号直取'; } catch (e) { proofBytes = null; } } if (!proofBytes || !proofBytes.length) { if (proxyCookie) { try { proofBytes = await pdsProxyFetchProof(proxyCookie, row.sha1, size, offset); proofSource = '夸克/UC中转'; } catch (e) { proofBytes = null; skippedDetail.push(`${row.path}:夸克/UC 中转取块失败(${e.message})`); skipped++; continue; } } } if (!proofBytes || !proofBytes.length) { skipped++; skippedDetail.push(`${row.path}:无法获取 proof 块。阿里同账号导入请导出的 JSON 需带 fileId;跨盘导入请在输入框粘贴夸克或 UC 的完整 Cookie(夸克/UC 中转取块,不需服务器)。`); continue; } const proof = ALIYUN.proofFromBytes(proofBytes); // 3) createWithFolders 快速上传(name 含 "/" 时自动创建父目录,实测支持) const r = await ALIYUN.rapidUpload(driveId, currentParent, row.path, size, row.sha1, proof); if (r.rapid) { success++; } else { fail++; failures.push(`${row.path}:阿里云盘未命中快速上传(rapid_upload=false),服务器上不存在该 sha1 内容(proof 来源:${proofSource || '无'})`); } } catch (e) { // 特判:proof 无效 / 需要 proof const msg = String(e.message || ''); if (msg.includes('InvalidRapidProof')) { fail++; failures.push(`${row.path}:proof 校验失败(取到的验证块与该文件不匹配,或 offset 算法需更新)`); } else if (msg.includes('RapidProofNeed')) { fail++; failures.push(`${row.path}:缺少 proof 验证块(未能获取验证块)`); } else { fail++; failures.push(`${row.path}:秒传失败(${e.message})`); } } await sleep(CONFIG.transferDelay); } onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` }); mcLog('import', '阿里导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped); if (failures.length) mcLog('import', '失败明细:' + failures.slice(0, 8).join(' | ')); return { total, success, fail, skipped, failures, skippedDetail }; } /** 阿里云盘导入模态框 */ function showAliyunImportModal() { const modal = openModal({ title: '保存秒传 JSON 到阿里云盘', icon: ICONS.import, width: 640, content(el) { el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:sha1=40 位、hashName=sha1、path、size)。
proof 块经夸克/UC 中转获取(不需云服务器):阿里导入时填入夸克或 UC 网的完整 Cookie,脚本先把文件按 sha1 秒传到夸克/UC,再从云端取 proof 字节;path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
夸克/UC Cookie:
`; }, footer(el) { el.innerHTML = ` `; }, }); const ta = modal.contentEl.querySelector('#gy-ms-al-import-ta'); const statusEl = modal.contentEl.querySelector('#gy-ms-al-import-status'); const barEl = modal.contentEl.querySelector('#gy-ms-al-import-bar'); const detailEl = modal.contentEl.querySelector('#gy-ms-al-import-detail'); const cookieEl = modal.contentEl.querySelector('#gy-ms-al-proxy-cookie'); const runBtn = modal.footerEl.querySelector('[data-act="run"]'); modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl); // ========== 一键获取夸克/UC Cookie(跨标签通道) ========== const qkBtn = modal.contentEl.querySelector('#gy-ms-al-proxy-qk'); const ucBtn = modal.contentEl.querySelector('#gy-ms-al-proxy-uc'); let pendingTimer = null; function startPendingPoll() { if (pendingTimer) return; pendingTimer = setInterval(() => { let result = ''; try { result = GM_getValue('__pds_cookie_result', ''); } catch (e) { /* ignore */ } if (result) { try { GM_setValue('__pds_cookie_result', ''); } catch (e) { /* ignore */ } cookieEl.value = result; setStatus(statusEl, '已自动填入 ' + (result.indexOf('QK:') === 0 ? '夸克' : 'UC') + ' Cookie,可以开始导入。', 'ok'); clearInterval(pendingTimer); pendingTimer = null; } }, 500); } qkBtn.onclick = () => { try { GM_setValue('__pds_cookie_pending', 'quark'); GM_setValue('__pds_cookie_result', ''); } catch (e) { /* ignore */ } showToast('正在打开夸克网盘获取 Cookie...', 'info'); window.open('https://pan.quark.cn/', '_blank'); startPendingPoll(); }; ucBtn.onclick = () => { try { GM_setValue('__pds_cookie_pending', 'uc'); GM_setValue('__pds_cookie_result', ''); } catch (e) { /* ignore */ } showToast('正在打开 UC 网盘获取 Cookie...', 'info'); window.open('https://drive.uc.cn/', '_blank'); startPendingPoll(); }; runBtn.onclick = async () => { const vr = validateImportJson(ta.value); if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; } runBtn.disabled = true; runBtn.textContent = '导入中...'; barEl.style.display = 'block'; detailEl.style.display = 'none'; let summary; try { summary = await aliyunImportJson(ta.value, { proxyCookie: cookieEl ? cookieEl.value.trim() : '', }, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); }); } catch (e) { setStatus(statusEl, `导入失败:${e.message}`, 'err'); runBtn.disabled = false; runBtn.textContent = '开始导入'; return; } runBtn.disabled = false; runBtn.textContent = '开始导入'; const detailLines = []; if (summary.failures.length) detailLines.push(...summary.failures); if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(未提交接口) =========='); detailLines.push(...summary.skippedDetail); } if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; } const warn = summary.fail > 0 || summary.skipped > 0; setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok'); if (warn) { try { const logBtn = document.createElement('button'); logBtn.className = 'gy-ms-btn gy-ms-btn-outline'; logBtn.textContent = '导出日志'; logBtn.style.marginTop = '8px'; logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); }; statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling); } catch (e) { /* ignore */ } } }; } function showBaiduImportModal() { const modal = openModal({ title: '保存秒传 JSON 到百度网盘', icon: ICONS.import, width: 640, content(el) { el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:etag=32 位 MD5、contentMd5=加密 MD5、path、size)。
导入优先经云服务器取验证块;path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`; }, footer(el) { el.innerHTML = ` `; }, }); const ta = modal.contentEl.querySelector('#gy-ms-bd-import-ta'); const statusEl = modal.contentEl.querySelector('#gy-ms-bd-import-status'); const barEl = modal.contentEl.querySelector('#gy-ms-bd-import-bar'); const detailEl = modal.contentEl.querySelector('#gy-ms-bd-import-detail'); const runBtn = modal.footerEl.querySelector('[data-act="run"]'); modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl); runBtn.onclick = async () => { const vr = validateImportJson(ta.value); if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; } runBtn.disabled = true; runBtn.textContent = '导入中...'; barEl.style.display = 'block'; detailEl.style.display = 'none'; let summary; try { summary = await baiduImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); }); } catch (e) { setStatus(statusEl, `导入失败:${e.message}`, 'err'); runBtn.disabled = false; runBtn.textContent = '开始导入'; return; } runBtn.disabled = false; runBtn.textContent = '开始导入'; const detailLines = []; if (summary.failures.length) detailLines.push(...summary.failures); if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(未提交接口) =========='); detailLines.push(...summary.skippedDetail); } if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; } const warn = summary.fail > 0 || summary.skipped > 0; setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok'); // 导出日志按钮(本地,便于排查) if (warn) { try { const logBtn = document.createElement('button'); logBtn.className = 'gy-ms-btn gy-ms-btn-outline'; logBtn.textContent = '导出日志'; logBtn.style.marginTop = '8px'; logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); }; statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling); } catch (e) { /* ignore */ } } }; } /** 首启提示:云令牌授权(仅导出 / 继续) */ function showCloudConsentModal() { const modal = openModal({ title: '百度网盘秒传 - 云令牌授权', icon: ICONS.settings, width: 560, content(el) { el.innerHTML = `
百度网盘 23 年后加强了秒传验证,现在秒传需要一个绑定账号的随机值对应的一段原文件随机位置的 256KB 数据块。

我们默认提供一个云服务器来存储您的账号令牌,以用于在不向其他用户暴露您账号真实令牌的情况下,为导入您秒传文件的用户提供这个随机数据块以实现秒传。

我们承诺只会将令牌用于秒传的实现。

如果您不想将这个令牌提供给我们以用于秒传,请选择「仅导出」;如果您愿意,请选择「继续」。
选择「仅导出」将导致您无法使用导入功能,导出的秒传文件也仅能用于其他网盘的秒传。设置里可以切换是否提供令牌以恢复完整功能。
`; }, footer(el) { el.innerHTML = ` `; }, }); modal.footerEl.querySelector('[data-act="no"]').onclick = () => { BAIDU.setCloudConsent(false); mcLog('consent', '用户选择「仅导出」(不采集令牌)'); modal.overlay.remove(); showToast('已选择「仅导出」:不采集令牌,导入功能已禁用;设置里可切换恢复'); }; modal.footerEl.querySelector('[data-act="yes"]').onclick = () => { BAIDU.setCloudConsent(true); mcLog('consent', '用户同意提供令牌(完整功能)'); modal.overlay.remove(); showToast('已授权:云秒传完整功能可用'); }; } // ---- 保存秒传 JSON(导入,仅光鸭;迅雷无秒传接口)---- function validateImportJson(text) { const trim = String(text || '').trim(); if (!trim) return { ok: false, message: '内容为空' }; let obj; try { obj = JSON.parse(trim); } catch { return { ok: false, message: '不是合法 JSON' }; } if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return { ok: false, message: 'JSON 顶层须为对象' }; if (!Array.isArray(obj.files)) return { ok: false, message: '须包含 files 数组' }; if (!obj.files.length) return { ok: false, message: 'files 数组为空' }; return { ok: true, fileCount: obj.files.length }; } /** * 给导入模态框挂「选择 .json 文件」功能(各平台共用;光鸭原有实现抽取)。 * modal:openModal 返回对象;ta:主 textarea;fileRowEl:含 input 的行容器;onFileLoaded(text) 回调。 */ function attachImportFilePicker(modal, ta, fileNameEl, statusEl, onFileLoaded) { const fileInput = modal.contentEl.querySelector('#gy-ms-import-file'); if (!fileInput) return; fileInput.addEventListener('change', async () => { const f = fileInput.files && fileInput.files[0]; if (!f) return; try { const text = await f.text(); const vr = validateImportJson(text); if (!vr.ok) { if (statusEl) setStatus(statusEl, `格式不符:${vr.message}`, 'err'); return; } ta.value = text; if (fileNameEl) fileNameEl.textContent = `${f.name}(${(f.size / 1024).toFixed(1)} KB,共 ${vr.fileCount} 条)`; if (statusEl) setStatus(statusEl, '', 'info'); if (onFileLoaded) onFileLoaded(text, f); } catch (e) { if (statusEl) setStatus(statusEl, '读取文件失败,请改用粘贴', 'err'); } }); const clearBtn = modal.contentEl.querySelector('#gy-ms-import-clear'); if (clearBtn) { clearBtn.onclick = () => { fileInput.value = ''; ta.value = ''; if (fileNameEl) fileNameEl.textContent = ''; if (statusEl) setStatus(statusEl, '', 'info'); }; } } /** 导入框公共说明:粘贴或选择 .json 文件(跨平台一致) */ const IMPORT_FILE_ROW_HTML = `
选择文件:
`; function showImportModal() { if (isXunlei()) { showToast('迅雷云盘无秒传接口,请到光鸭云盘页面导入', 'warning'); return; } const modal = openModal({ title: '保存秒传 JSON 到当前账号', icon: ICONS.import, width: 640, content(el) { el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:etag=32 位十六进制 MD5、gcid=40 位 GCID、path、size)。
秒传优先使用 MD5,缺失时尝试 GCID;path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`; }, footer(el) { el.innerHTML = ` `; }, }); const ta = modal.contentEl.querySelector('#gy-ms-import-ta'); const fileNameEl = modal.contentEl.querySelector('#gy-ms-import-filename'); const statusEl = modal.contentEl.querySelector('#gy-ms-import-status'); const barEl = modal.contentEl.querySelector('#gy-ms-import-bar'); const detailEl = modal.contentEl.querySelector('#gy-ms-import-detail'); const runBtn = modal.footerEl.querySelector('[data-act="run"]'); modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); attachImportFilePicker(modal, ta, fileNameEl, statusEl); runBtn.onclick = async () => { const vr = validateImportJson(ta.value); if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; } runBtn.disabled = true; runBtn.textContent = '导入中...'; barEl.style.display = 'block'; detailEl.style.display = 'none'; const summary = await runImport(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); }); runBtn.disabled = false; runBtn.textContent = '开始导入'; const detailLines = []; if (summary.failures.length) detailLines.push(...summary.failures); if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 已跳过(未提交接口) =========='); detailLines.push(...summary.skippedDetail); } if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; } const warn = summary.fail > 0 || summary.skipped > 0; setStatus( statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok' ); }; } async function runImport(rawJson, onProgress) { const api = new GuangyaApi(); const obj = JSON.parse(rawJson); const list = Array.isArray(obj.files) ? obj.files : []; const rows = []; const skipped = []; for (let i = 0; i < list.length; i++) { const f = list[i]; const path = String(f.path || f.name || '').trim() || 'file'; const size = Number(f.size != null ? f.size : 0); const gcid = normalizeGcid(f.gcid); const md5 = normalizeMd5(f.etag || f.md5); if (!path) { skipped.push('(无路径条目)'); continue; } if (!md5 && !gcid) { skipped.push(`${path}:etag/md5 与 gcid 均缺失,无法秒传`); continue; } rows.push({ path, md5, gcid, size }); } const parentId = getLastParentId(); const dirCache = new Map(); let success = 0; let fail = 0; const failures = []; const total = rows.length; for (let i = 0; i < rows.length; i++) { const row = rows[i]; onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `秒传 ${i + 1}/${total}:${row.path}` }); const dirSegments = dirSegmentsFromPath(row.path); const fileName = basenameFromPath(row.path); let targetParent = parentId; let mkdirOk = true; for (const dirName of dirSegments) { const key = `${targetParent}::${dirName}`; if (dirCache.has(key)) { targetParent = dirCache.get(key); continue; } try { const dirId = await api.createDir(targetParent, dirName); dirCache.set(key, dirId); targetParent = dirId; await sleep(CONFIG.mkdirDelay); } catch (e) { mkdirOk = false; failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`); break; } } if (!mkdirOk) { fail++; continue; } try { const r = await api.flashUpload(fileName, row.md5, row.gcid, row.size, targetParent); if (r.hit) { success++; } else { fail++; failures.push(`${row.path}:未命中秒传库存(服务器上不存在该文件)`); } } catch (e) { fail++; failures.push(`${row.path}:秒传接口失败(${e.message})`); } await sleep(CONFIG.transferDelay); } onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped.length ? `,跳过 ${skipped.length}` : ''}` }); return { total, success, fail, skipped: skipped.length, failures, skippedDetail: skipped }; } // ---- 设置 ---- function showSettingsModal() { const modal = openModal({ title: '设置', icon: ICONS.settings, width: 460, content(el) { const fields = [ ['debug', '调试模式(控制台日志)', 'checkbox', CONFIG.debug], ['md5Delay', '获取MD5间隔 (ms)', 'number', CONFIG.md5Delay], ['mkdirDelay', '创建目录间隔 (ms)', 'number', CONFIG.mkdirDelay], ['transferDelay', '秒传间隔 (ms)', 'number', CONFIG.transferDelay], ['listPageDelay', '分页间隔 (ms)', 'number', CONFIG.listPageDelay], ['md5BatchSize', 'MD5批量大小', 'number', CONFIG.md5BatchSize], ['defaultExportName', '默认导出文件名', 'text', CONFIG.defaultExportName], ]; if (isBaidu()) { fields.push(['baiduCloudConsent', '提供令牌给云服务器(完整秒传功能)', 'checkbox', BAIDU.cloudConsentEnabled()]); } el.innerHTML = fields.map(([key, label, type, value]) => `
${type === 'checkbox' ? `` : ``}
`).join(''); }, footer(el) { el.innerHTML = ` ${isGuangya() ? `` : ''}
`; }, }); modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); modal.footerEl.querySelector('[data-act="save"]').onclick = () => { modal.contentEl.querySelectorAll('[data-field]').forEach(inp => { const key = inp.getAttribute('data-field'); if (key === 'baiduCloudConsent') { BAIDU.setCloudConsent(inp.checked); return; } if (inp.type === 'checkbox') CONFIG[key] = inp.checked; else if (key === 'defaultExportName') CONFIG[key] = inp.value.trim(); else CONFIG[key] = Number(inp.value) || 0; }); saveSettings(); modal.overlay.remove(); showToast('设置已保存'); }; const clearBtn = modal.footerEl.querySelector('[data-act="cleartoken"]'); if (clearBtn) { clearBtn.onclick = () => { if (isGuangya()) { new GuangyaApi().clearToken(); showToast('已清除保存的 access_token', 'info'); } }; } } // ---- 工具栏按钮(按平台挂载) ---- function isUploadLabel(text) { const t = String(text || '').replace(/\s+/g, ''); return t === '上传' || /^upload$/i.test(t); } function rowLooksLikeToolbar(row, uploadBtn) { if (!row || !uploadBtn) return false; const n = (row.textContent || '').replace(/\s+/g, ''); if (isGuangya() && (n.includes('新建文件夹') || n.includes('云添加'))) return true; if (isXunlei() && n.includes('云添加')) return true; if (isBaidu() && (n.includes('上传') || n.includes('新建文件夹') || n.includes('云添加'))) return true; const primaries = Array.from(row.querySelectorAll('button.ant-btn-primary')).filter((b) => !b.closest('.gy-ms-toolbar-btn')); return primaries.length === 1 && primaries[0] === uploadBtn; } function resolveToolbarRow() { if (isP115()) { // 115 新版网页端(115.com/storage/allfiles):上传按钮的二级父容器即工具栏行(含 上传/新建) const upBtn = Array.from(document.querySelectorAll('button')).find((b) => { if (b.closest('.gy-ms-toolbar-btn')) return false; return (b.textContent || '').trim() === '上传'; }); if (upBtn && upBtn.parentElement && upBtn.parentElement.parentElement) { const toolbar = upBtn.parentElement.parentElement; return { row: toolbar, uploadAnchor: upBtn, toolbar }; } return null; } if (isY139()) { // 移动云盘(yun.139.com):上传按钮的父容器即工具栏行(document_top_upload_button → 父容器 / .document_main .top_button) const upWrap = document.querySelector('.document_top_upload_button'); const row = upWrap ? upWrap.parentElement : document.querySelector('.document_main .top_button'); if (row) return { row, uploadAnchor: upWrap || row, toolbar: row }; return null; } if (isP123()) { // 123云盘:.home-operator-button-group 即工具栏容器(上传/新建文件夹所在行) const container = document.querySelector('.home-operator-button-group'); if (container) return { row: container, uploadAnchor: container.firstElementChild || container, toolbar: container }; return null; } if (isBaidu()) { // 精确选中工具栏上传按钮(u-button nd-file-list-toolbar-action-item 且文本为「上传」) const uploadBtn = Array.from(document.querySelectorAll('button.nd-file-list-toolbar-action-item')).find((b) => { if (b.closest('.gy-ms-toolbar-btn')) return false; const t = (b.textContent || '').trim(); return t === '上传' || t.indexOf('上传') === 0; }); if (uploadBtn) { // 工具栏容器 = .wp-s-agile-tool-bar__header(直链助手同容器;秒传按钮 prepend 到此层, // 与上传按钮平级分离,绝不嵌入上传内部) const toolbar = uploadBtn.closest('.wp-s-agile-tool-bar__header, .nd-file-list-toolbar') || uploadBtn.parentElement; return { row: toolbar, uploadAnchor: uploadBtn, toolbar }; } return null; } if (isAliyun()) { // 阿里云盘:优先复用「直链下载助手」同款容器 actions--xxx(header 右侧操作区,与下载助手按钮并列)。 const actionsEl = Array.from(document.querySelectorAll('[class*="actions--"], [class*="action--"], [class*="actions-"]')).find((b) => { if (b.closest('.gy-ms-toolbar-btn')) return false; const r = b.getBoundingClientRect(); // 右侧工具栏:宽 > 20、高 > 20、且在页面顶部工具栏区域(y < 150) return r.width > 20 && r.height > 20 && r.y > 0 && r.y < 150; }); if (actionsEl) { return { row: actionsEl, uploadAnchor: actionsEl, toolbar: actionsEl }; } // 回退:挂到阿里原生 header(header--xxx / header / [class*="navbar"]),append 到末尾即右上角。 // 不依赖「新建/上传」按钮(部分视图无该按钮)或第三方脚本容器。 const headerEl = document.querySelector('header, .header--wVY7B, [class*="header--"], [class*="navbar"]'); if (headerEl) { return { row: headerEl, uploadAnchor: headerEl, toolbar: headerEl }; } return null; } if (isQuark()) { // 夸克网盘:直接定位工具栏按钮组容器(btn-main,含 下载助手/上传/新建文件夹/备份), // 秒传按钮 append 到 btn-main 末尾。绝不能挂进 ant-upload 容器 // (Ant Design Upload 内点击任意处会触发隐藏 input → 唤起文件选择器)。 const btnMain = Array.from(document.querySelectorAll('.btn-main, [class*="btn-main"]')).find((b) => { if (b.closest('.gy-ms-toolbar-btn')) return false; const r = b.getBoundingClientRect(); // 工具栏:宽 > 100、高 > 20、顶部区域 y < 150 return r.width > 100 && r.height > 20 && r.y > 0 && r.y < 150; }); if (btnMain) { return { row: btnMain, uploadAnchor: btnMain, toolbar: btnMain }; } // 回退:以「上传文件」按钮为锚点 const uploadBtn = Array.from(document.querySelectorAll('button.upload-btn, button[class*="upload"], button.btn-file')).find((b) => { if (b.closest('.gy-ms-toolbar-btn')) return false; const r = b.getBoundingClientRect(); return r.width > 0 && r.height > 0; }) || Array.from(document.querySelectorAll('button')).find((b) => { if (b.closest('.gy-ms-toolbar-btn')) return false; const t = (b.textContent || '').replace(/\s+/g, ''); const r = b.getBoundingClientRect(); return /^上传文件$/.test(t) && r.width > 0 && r.height > 0; }); if (uploadBtn) { let toolbar = uploadBtn.closest('.btn-main, .btn-operate, .section-header.list-header, [class*="toolbar"]') || uploadBtn.parentElement; // 兜底:若父级仍在 ant-upload 内则继续上跳一层 if (toolbar && toolbar.closest('.ant-upload')) { toolbar = toolbar.parentElement; } return { row: toolbar, uploadAnchor: uploadBtn, toolbar }; } return null; } if (isUc()) { // UC 网盘(与夸克同源前端):同样挂到 btn-main 工具栏组,避开 ant-upload(点击会唤起文件选择器) const btnMain = Array.from(document.querySelectorAll('.btn-main, [class*="btn-main"]')).find((b) => { if (b.closest('.gy-ms-toolbar-btn')) return false; const r = b.getBoundingClientRect(); return r.width > 100 && r.height > 20 && r.y > 0 && r.y < 150; }); if (btnMain) { return { row: btnMain, uploadAnchor: btnMain, toolbar: btnMain }; } // 回退:以「上传文件」按钮为锚点 const uploadBtn = Array.from(document.querySelectorAll('button.upload-btn, button[class*="upload"], button.btn-file')).find((b) => { if (b.closest('.gy-ms-toolbar-btn')) return false; const r = b.getBoundingClientRect(); return r.width > 0 && r.height > 0; }) || Array.from(document.querySelectorAll('button')).find((b) => { if (b.closest('.gy-ms-toolbar-btn')) return false; const t = (b.textContent || '').replace(/\s+/g, ''); const r = b.getBoundingClientRect(); return /^上传文件$/.test(t) && r.width > 0 && r.height > 0; }); if (uploadBtn) { let toolbar = uploadBtn.closest('.btn-main, .btn-operate, .section-header.list-header, [class*="toolbar"]') || uploadBtn.parentElement; if (toolbar && toolbar.closest('.ant-upload')) { toolbar = toolbar.parentElement; } return { row: toolbar, uploadAnchor: uploadBtn, toolbar }; } return null; } if (isWeiyun()) { // 腾讯微云:挂到 .mod-nav(上传/新建/更多 按钮组的外层布局容器)末尾, // 与"新建"同排右侧。绝不能挂 .mod-action-wrap*(mod-action-wrap-upload 内含 #formFileInputCt // 上传按钮容器,点击其内部任意位置会冒泡触发 uploadFile → 唤起文件选择器,与夸克/UC ant-upload 陷阱同类)。 const nav = document.querySelector('.mod-nav, [class*="mod-nav"]'); if (nav) { return { row: nav, uploadAnchor: nav, toolbar: nav }; } // 回退:最外层工具栏(安全但不与按钮组同排) const toolbar = document.querySelector('.layout-toolbar, [class*="layout-toolbar"]'); if (toolbar) { return { row: toolbar, uploadAnchor: toolbar, toolbar }; } return null; } if (isFeejii() || isIlanzou()) { // 小飞机/蓝奏云优享版控制台:顶部 .title-bar 工具栏容器(含 上传/新建 等按钮组),秒传按钮 append 到末尾 const titleBar = document.querySelector('.title-bar, [class*="title-bar"]'); if (titleBar) { return { row: titleBar, uploadAnchor: titleBar, toolbar: titleBar }; } // 回退:整个文件列表区顶部 const files = document.querySelector('.files, [class*="file-list"]'); if (files) { return { row: files, uploadAnchor: files, toolbar: files }; } return null; } if (isLanzou()) { // 蓝奏云为 iframe 结构,顶层仅悬浮按钮(fixed 兜底) return null; } if (isCtfile()) { // 城通网盘标准版:顶部 .ct-toolbar 工具栏(含 上传/上传文件夹/新建文件夹/搜索/排序),秒传按钮 append 到末尾 const toolbar = document.querySelector('.ct-toolbar, [class*="ct-toolbar"]'); if (toolbar) { return { row: toolbar, uploadAnchor: toolbar, toolbar }; } // 回退:整个文件列表容器 const listEl = document.querySelector('.ct-list, [class*="ct-list"], [class*="file-list"]'); if (listEl) { return { row: listEl, uploadAnchor: listEl, toolbar: listEl }; } return null; } if (isTianyi()) { // 天翼云盘:以「新建文件夹」文本按钮为锚(scoped hash 类名不稳定,锚文本最稳), // 其父级即左侧按钮组容器(上传/新建文件夹/刷新一组),秒传按钮 append 到该组末尾 const btns = Array.from(document.querySelectorAll('div,span,a')).filter(el => (el.textContent || '').trim() === '新建文件夹' && el.children.length === 0); if (btns.length && btns[0].parentElement) { const group = btns[0].parentElement; return { row: group, uploadAnchor: group, toolbar: group }; } // 回退:整个文件区容器 const panel = document.querySelector('.cloud-file, .p-file, [class*="File_p-file"], [class*="file-list"], [class*="c-file-list"]'); if (panel) return { row: panel, uploadAnchor: panel, toolbar: panel }; return null; } if (isWopan()) { // 联通云盘:以「新建」文本按钮为锚,其父级 .actions 即顶部按钮组(上传/新建),秒传按钮 append 到末尾。 // 注意「上传」是 el-dropdown(点击展开菜单),秒传按钮作为独立兄弟节点不受影响。 const btns = Array.from(document.querySelectorAll('button,span,div,a')).filter(el => (el.textContent || '').trim() === '新建' && /el-button/.test(el.className || '')); if (btns.length && btns[0].parentElement) { const group = btns[0].parentElement; return { row: group, uploadAnchor: group, toolbar: group }; } // 回退:左侧操作区/整个文件列表容器 const op = document.querySelector('[class*="operation-container"], [class*="left-operation"], .file-list-container'); if (op) return { row: op, uploadAnchor: op, toolbar: op }; return null; } const triggers = document.querySelectorAll('.ant-dropdown-trigger'); for (const trig of triggers) { const inner = trig.matches('button.ant-btn, .ant-btn') ? trig : trig.querySelector('button.ant-btn, .ant-btn'); if (!inner || !inner.classList.contains('ant-btn-primary')) continue; if (!isUploadLabel(inner.textContent)) continue; const row = trig.parentElement; if (!row || !rowLooksLikeToolbar(row, inner)) continue; return { row, uploadAnchor: trig }; } const primaries = document.querySelectorAll('button.ant-btn-primary'); for (const inner of primaries) { if (inner.closest('.gy-ms-toolbar-btn')) continue; if (!isUploadLabel(inner.textContent)) continue; const row = inner.parentElement; if (!row || !rowLooksLikeToolbar(row, inner)) continue; return { row, uploadAnchor: inner }; } return null; } function addToolbarButton() { // 115 旧版 UI:不挂秒传按钮(p115PlatformGate 已显示提示条) if (isP115() && !p115IsNewUi()) return; const existing = document.querySelector('.gy-ms-toolbar-btn'); if (existing) { // 菜单打开时绝不触碰(防止 MutationObserver 重建导致菜单消失) const dd = existing.querySelector('.gy-ms-dropdown'); if (dd && dd.style.display === 'block') return; const hitCheck = resolveToolbarRow(); if (hitCheck) { // 幂等:只要按钮仍在期望容器内(或是其子孙)就直接返回,不重建; // 优享版/小飞机等 append 到工具栏末尾,百度 prepend 到最前,其它平台在 uploadAnchor 前 if (existing.parentElement && (existing.parentElement === hitCheck.toolbar || (hitCheck.toolbar && hitCheck.toolbar.contains(existing)))) { return; } if (hitCheck.uploadAnchor && hitCheck.uploadAnchor.previousElementSibling === existing) return; } else { // 无目标容器(fixed 悬浮兜底):只要还挂在 body 上就不重建 if (existing.parentElement === document.body) return; } existing.remove(); } const hit = resolveToolbarRow(); if (hit) { const wrap = document.createElement('div'); wrap.className = 'gy-ms-toolbar-btn'; wrap.style.cssText = 'display:inline-flex;margin-right:8px;vertical-align:middle;position:relative;z-index:100;'; wrap.innerHTML = buildButtonHtml(); try { const tb = hit.toolbar; if (tb && typeof tb.appendChild === 'function') { if (isAliyun() || isQuark() || isUc() || isWeiyun() || isFeejii() || isP115() || isCtfile() || isTianyi() || isWopan()) { // 阿里:append 到 actions 容器末尾(与直链下载助手并列);夸克/UC/微云/小飞机/优享版/城通/天翼:append 到工具栏末尾(与新建文件夹并列) tb.appendChild(wrap); } else { // 百度及其它平台:prepend 到工具栏最前 / uploadAnchor 前 tb.insertBefore(wrap, tb.firstChild); } } else { (hit.uploadAnchor.parentElement || document.body).insertBefore(wrap, hit.uploadAnchor); } } catch (e) { (hit.uploadAnchor.parentElement || document.body).insertBefore(wrap, hit.uploadAnchor); } bindDropdown(wrap); return; } const fab = document.createElement('div'); fab.className = 'gy-ms-toolbar-btn'; fab.style.cssText = 'position:fixed;left:24px;top:24px;z-index:2147483645;'; fab.innerHTML = buildButtonHtml(); document.body.appendChild(fab); bindDropdown(fab); } function buildButtonHtml() { return ` `; } function bindDropdown(container) { const mainBtn = container.querySelector('.gy-ms-main-btn'); const dropdown = container.querySelector('.gy-ms-dropdown'); const show = () => { dropdown.style.display = 'block'; }; const hide = () => { dropdown.style.display = 'none'; }; // 记录鼠标坐标:绕过按钮与菜单间空隙导致的 mouseleave 误判。 // 注意:按钮可能在 iframe 内(蓝奏云),mousemove 必须监听按钮所属 document, // iframe 内的鼠标移动不会冒泡到顶层 document。 const ownerDoc = (container.ownerDocument) || document; const ownerWin = (ownerDoc.defaultView) || window; let lastX = 0, lastY = 0; try { ownerDoc.addEventListener('mousemove', (e) => { lastX = e.clientX; lastY = e.clientY; }, { passive: true }); } catch (e) { /* ignore */ } const pointerIn = (el) => { try { const r = el.getBoundingClientRect(); return lastX >= r.left && lastX <= r.right && lastY >= r.top && lastY <= r.bottom; } catch (e) { return false; } }; const scheduleHide = () => { setTimeout(() => { if (pointerIn(container) || pointerIn(dropdown)) { show(); return; } if (!container.matches(':hover') && !dropdown.matches(':hover')) hide(); }, 300); }; mainBtn.addEventListener('mouseenter', show); mainBtn.addEventListener('click', (e) => { e.stopPropagation(); dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block'; }); container.addEventListener('mouseleave', scheduleHide); dropdown.addEventListener('mouseleave', scheduleHide); dropdown.addEventListener('mouseenter', show); dropdown.addEventListener('click', (e) => { const item = e.target.closest('.gy-ms-dropdown-item'); if (!item) return; const act = item.getAttribute('data-act'); hide(); if (act === 'generate') { const job = (isP115() ? p115GenerateJson() : isBaidu() ? baiduGenerateJson() : isAliyun() ? aliyunGenerateJson() : isQuark() ? quarkGenerateJson() : isUc() ? ucGenerateJson() : isWeiyun() ? weiyunGenerateJson() : isFeejii() ? feejiiGenerateJson() : isIlanzou() ? ilanzouGenerateJson() : isLanzou() ? lanzouGenerateJson() : isCtfile() ? ctfileGenerateJson() : isTianyi() ? tianyiGenerateJson() : isWopan() ? wopanGenerateJson() : isY139() ? y139GenerateJson() : isP123() ? p123GenerateJson() : generateJson()); job.catch(err => showToast(err.message, 'error')); } else if (act === 'import') { if (isP115()) { showP115ImportModal(); } else if (isBaidu()) { if (!BAIDU.cloudConsentEnabled()) { showToast('已选择「仅导出」:导入功能已禁用(设置里可切换提供令牌恢复)', 'warning'); return; } showBaiduImportModal(); } else if (isAliyun()) { showAliyunImportModal(); } else if (isQuark()) { showQuarkImportModal(); } else if (isUc()) { showUcImportModal(); } else if (isWeiyun()) { showWeiyunImportModal(); } else if (isFeejii()) { showFeejiiImportModal(); } else if (isIlanzou()) { showIlanzouImportModal(); } else if (isLanzou()) { showToast('蓝奏云无哈希秒传接口(html5up.php 必须携带文件本体,实测 md5-only 被拒、服务器不做内容去重),无法秒传导入;该盘导出的 JSON 可在优享版/小飞机等支持 md5 的平台导入', 'warning', 6000); } else if (isCtfile()) { showCtfileImportModal(); } else if (isTianyi()) { showTianyiImportModal(); } else if (isWopan()) { showToast('联通云盘无哈希秒传接口(上传必须携带文件本体,服务器不做内容去重,列表也不返回哈希),无法秒传导入;该盘导出的 md5 JSON 可在百度/夸克/UC/小飞机等支持 md5 的平台导入', 'warning', 6000); } else if (isY139()) { showY139ImportModal(); } else if (isP123()) { showP123ImportModal(); } else showImportModal(); } else if (act === 'settings') showSettingsModal(); }); } // ============================== 115 平台(v1.0:导出秒传JSON + 导入秒传,含二次验证云端中转) ============================== // 协议来源:2026-10 逆向实测(uplb.115.com/4.0/initupload.php 固定 AES 方案 + proapi chrome/downurl rsa 加密直链) // 边界:<1MB 纯 sha1 秒传;≥1MB 必触发二次验证(sign_check 随机范围)→ 115→115 导入用导出内嵌的 pc 走 CDN Range 中转 const P115_API = { UPLOADINFO: 'https://proapi.115.com/app/uploadinfo', INIT: 'https://uplb.115.com/4.0/initupload.php', LIST: 'https://webapi.115.com/files', DIR_ADD: 'https://webapi.115.com/files/add', DELETE: 'https://webapi.115.com/rb/delete', DOWNURL: 'https://proapi.115.com/app/chrome/downurl', UA_DISK: 'Mozilla/5.0 115disk/36.2.28 115Browser/36.2.28 115wangpan_android/36.2.28', APP_VERSION: '36.2.28', }; // ---- 115 加密栈(k_ec / AES-CBC / LZ4 / sig / token / RSA 直链)---- const P115_CRYPTO = (() => { const enc = new TextEncoder(); const dec = new TextDecoder('utf-8'); const hexOf = (u8) => Array.from(u8, b => b.toString(16).padStart(2, '0')).join(''); const toU8 = (arr) => new Uint8Array(arr); // MD5(blueimp 风格,ASCII/字节串) function md5Hex(str) { const s = String(str); const safeAdd = (x, y) => { const lsw = (x & 0xffff) + (y & 0xffff); const msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xffff); }; const bitRotateLeft = (num, cnt) => (num << cnt) | (num >>> (32 - cnt)); const md5cmn = (q, a, b, x, s, t) => safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b); const md5ff = (a, b, c, d, x, s, t) => md5cmn((b & c) | (~b & d), a, b, x, s, t); const md5gg = (a, b, c, d, x, s, t) => md5cmn((b & d) | (c & ~d), a, b, x, s, t); const md5hh = (a, b, c, d, x, s, t) => md5cmn(b ^ c ^ d, a, b, x, s, t); const md5ii = (a, b, c, d, x, s, t) => md5cmn(c ^ (b | ~d), a, b, x, s, t); const binlMD5 = (x, len) => { x[len >> 5] |= 0x80 << (len % 32); x[(((len + 64) >>> 9) << 4) + 14] = len; let i, oa, ob, oc, od; let a = 1732584193, b = -271733879, c = -1732584194, d = 271733878; for (i = 0; i < x.length; i += 16) { oa = a; ob = b; oc = c; od = d; a = md5ff(a, b, c, d, x[i], 7, -680876936); d = md5ff(d, a, b, c, x[i + 1], 12, -389564586); c = md5ff(c, d, a, b, x[i + 2], 17, 606105819); b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330); a = md5ff(a, b, c, d, x[i + 4], 7, -176418897); d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426); c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341); b = md5ff(b, c, d, a, x[i + 7], 22, -45705983); a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416); d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417); c = md5ff(c, d, a, b, x[i + 10], 17, -42063); b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162); a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682); d = md5ff(d, a, b, c, x[i + 13], 12, -40341101); c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290); b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329); a = md5gg(a, b, c, d, x[i + 1], 5, -165796510); d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632); c = md5gg(c, d, a, b, x[i + 11], 14, 643717713); b = md5gg(b, c, d, a, x[i], 20, -373897302); a = md5gg(a, b, c, d, x[i + 5], 5, -701558691); d = md5gg(d, a, b, c, x[i + 10], 9, 38016083); c = md5gg(c, d, a, b, x[i + 15], 14, -660478335); b = md5gg(b, c, d, a, x[i + 4], 20, -405537848); a = md5gg(a, b, c, d, x[i + 9], 5, 568446438); d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690); c = md5gg(c, d, a, b, x[i + 3], 14, -187363961); b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501); a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467); d = md5gg(d, a, b, c, x[i + 2], 9, -51403784); c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473); b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734); a = md5hh(a, b, c, d, x[i + 5], 4, -378558); d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463); c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562); b = md5hh(b, c, d, a, x[i + 14], 23, -35309556); a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060); d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353); c = md5hh(c, d, a, b, x[i + 7], 16, -155497632); b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640); a = md5hh(a, b, c, d, x[i + 13], 4, 681279174); d = md5hh(d, a, b, c, x[i], 11, -358537222); c = md5hh(c, d, a, b, x[i + 3], 16, -722521979); b = md5hh(b, c, d, a, x[i + 6], 23, 76029189); a = md5hh(a, b, c, d, x[i + 9], 4, -640364487); d = md5hh(d, a, b, c, x[i + 12], 11, -421815835); c = md5hh(c, d, a, b, x[i + 15], 16, 530742520); b = md5hh(b, c, d, a, x[i + 2], 23, -995338651); a = md5ii(a, b, c, d, x[i], 6, -198630844); d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415); c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905); b = md5ii(b, c, d, a, x[i + 5], 21, -57434055); a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571); d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606); c = md5ii(c, d, a, b, x[i + 10], 15, -1051523); b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799); a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359); d = md5ii(d, a, b, c, x[i + 15], 10, -30611744); c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380); b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649); a = md5ii(a, b, c, d, x[i + 4], 6, -145523070); d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379); c = md5ii(c, d, a, b, x[i + 2], 15, 718787259); b = md5ii(b, c, d, a, x[i + 9], 21, -343485551); a = safeAdd(a, oa); b = safeAdd(b, ob); c = safeAdd(c, oc); d = safeAdd(d, od); } return [a, b, c, d]; }; const rstr2binl = (input) => { const output = []; output[(input.length >> 2) - 1] = undefined; for (let i = 0; i < output.length; i += 1) output[i] = 0; const length8 = input.length * 8; for (let i = 0; i < length8; i += 8) output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << (i % 32); return output; }; const binl2rstr = (input) => { let output = ''; const length32 = input.length * 32; for (let i = 0; i < length32; i += 8) output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xff); return output; }; const rstr2hex = (input) => { const hexTab = '0123456789abcdef'; let output = ''; for (let i = 0; i < input.length; i += 1) { const x = input.charCodeAt(i); output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f); } return output; }; const rstrMD5 = (ss) => binl2rstr(binlMD5(rstr2binl(ss), ss.length * 8)); return rstr2hex(rstrMD5(s)); } // SHA1 hex(WebCrypto;失败回退纯 JS SHA-1) const sha1Hex = (str) => { try { return crypto.subtle.digest('SHA-1', enc.encode(str)).then((buf) => hexOf(new Uint8Array(buf))); } catch (e) { return Promise.resolve(sha1HexFallback(str)); } }; // 纯 JS SHA-1 回退(FIPS 180-4) function sha1HexFallback(str) { const bytes = []; for (let i = 0; i < str.length; i++) bytes.push(str.charCodeAt(i) & 0xff); const ml = bytes.length * 8; bytes.push(0x80); while (bytes.length % 64 !== 56) bytes.push(0); for (let i = 7; i >= 0; i--) bytes.push((ml / Math.pow(2, i * 8)) & 0xff); let h0 = 0x67452301, h1 = 0xEFCDAB89, h2 = 0x98BADCFE, h3 = 0x10325476, h4 = 0xC3D2E1F0; const rol = (n, c) => ((n << c) | (n >>> (32 - c))) >>> 0; for (let i = 0; i < bytes.length; i += 64) { const w = new Array(80); for (let t = 0; t < 16; t++) w[t] = ((bytes[i + t * 4] << 24) | (bytes[i + t * 4 + 1] << 16) | (bytes[i + t * 4 + 2] << 8) | bytes[i + t * 4 + 3]) >>> 0; for (let t = 16; t < 80; t++) w[t] = rol(w[t - 3] ^ w[t - 8] ^ w[t - 14] ^ w[t - 16], 1); let a = h0, b = h1, c = h2, d = h3, e = h4; for (let t = 0; t < 80; t++) { const f = t < 20 ? ((b & c) | (~b & d)) : t < 40 ? (b ^ c ^ d) : t < 60 ? ((b & c) | (b & d) | (c & d)) : (b ^ c ^ d); const k = t < 20 ? 0x5A827999 : t < 40 ? 0x6ED9EBA1 : t < 60 ? 0x8F1BBCDC : 0xCA62C1D6; const tmp = (rol(a, 5) + f + e + k + w[t]) >>> 0; e = d; d = c; c = rol(b, 30) >>> 0; b = a; a = tmp; } h0 = (h0 + a) >>> 0; h1 = (h1 + b) >>> 0; h2 = (h2 + c) >>> 0; h3 = (h3 + d) >>> 0; h4 = (h4 + e) >>> 0; } return [h0, h1, h2, h3, h4].map(h => ('00000000' + h.toString(16)).slice(-8)).join(''); } // CRC32 const crcTable = (() => { const t = new Int32Array(256); for (let i = 0; i < 256; i++) { let c = i; for (let k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1); t[i] = c; } return t; })(); const crc32 = (bytes) => { let c = -1; for (const b of bytes) c = crcTable[(c ^ b) & 0xff] ^ (c >>> 8); return (c ^ -1) >>> 0; }; // ---- 常量 ---- const AES_PUBKEY = toU8([0x1d, 0x03, 0x0e, 0x80, 0xa1, 0x78, 0xdc, 0xee, 0xce, 0xcd, 0xa3, 0x77, 0xde, 0x12, 0x8d, 0x8e, 0xd9, 0xdd, 0xcf, 0x55, 0xae, 0x61, 0xed, 0x46, 0xea, 0x12, 0x1a, 0x1c, 0xfc, 0x81]); const AES_KEY = toU8([0xfb, 0x1a, 0x19, 0xd6, 0x52, 0xf5, 0xaa, 0xf7, 0xbc, 0x65, 0x1d, 0x0f, 0x69, 0xbf, 0x42, 0x2f]); const AES_IV = toU8([0x69, 0xbf, 0x42, 0x2f, 0x49, 0x96, 0x05, 0x50, 0xa0, 0xad, 0x44, 0xec, 0x34, 0x46, 0xcb, 0x4c]); const CRC_SALT = toU8([0x5e, 0x6a, 0x3e, 0x57, 0x44, 0x33, 0x4b, 0x72, 0x3f, 0x4a, 0x32, 0x67, 0x4c, 0x46, 0x6a, 0x44, 0x34, 0x57, 0x32, 0x79, 0x40]); const MD5_SALT = 'Qclm8MGWUv59TnrR0XPg'; // RSA(下载直链,p115cipher 风格) const RSA_N = BigInt('0x' + '8686980c0f5a24c4b9d43020cd2c22703ff3f450756529058b1cf88f09b86021' + '36477198a6e2683149659bd122c33592fdb5ad47944ad1ea4d36c6b172aad633' + '8c3bb6ac6227502d010993ac967d1aef00f0c8e038de2e4d3bc2ec368af2e9f1' + '0a6f1eda4f7262f136420c07c331b871bf139f74f3010e3c4fe57df3afb71683'); const RSA_E = 65537n; const RSA_KEY = toU8([0x8d, 0xa5, 0xa5, 0x8d]); const G_KEY_L = toU8([0x78, 0x06, 0xad, 0x4c, 0x33, 0x86, 0x5d, 0x18, 0x4c, 0x01, 0x3f, 0x46]); const G_KTS = toU8([0xf0,0xe5,0x69,0xae,0xbf,0xdc,0xbf,0x8a,0x1a,0x45,0xe8,0xbe,0x7d,0xa6,0x73,0xb8,0xde,0x8f,0xe7,0xc4,0x45,0xda,0x86,0xc4,0x9b,0x64,0x8b,0x14,0x6a,0xb4,0xf1,0xaa,0x38,0x01,0x35,0x9e,0x26,0x69,0x2c,0x86,0x00,0x6b,0x4f,0xa5,0x36,0x34,0x62,0xa6,0x2a,0x96,0x68,0x18,0xf2,0x4a,0xfd,0xbd,0x6b,0x97,0x8f,0x4d,0x8f,0x89,0x13,0xb7,0x6c,0x8e,0x93,0xed,0x0e,0x0d,0x48,0x3e,0xd7,0x2f,0x88,0xd8,0xfe,0xfe,0x7e,0x86,0x50,0x95,0x4f,0xd1,0xeb,0x83,0x26,0x34,0xdb,0x66,0x7b,0x9c,0x7e,0x9d,0x7a,0x81,0x32,0xea,0xb6,0x33,0xde,0x3a,0xa9,0x59,0x34,0x66,0x3b,0xaa,0xba,0x81,0x60,0x48,0xb9,0xd5,0x81,0x9c,0xf8,0x6c,0x84,0x77,0xff,0x54,0x78,0x26,0x5f,0xbe,0xe8,0x1e,0x36,0x9f,0x34,0x80,0x5c,0x45,0x2c,0x9b,0x76,0xd5,0x1b,0x8f,0xcc,0xc3,0xb8,0xf5]); // ---- AES-128-CBC(手写,无 PKCS7 自动校验)---- const SBOX = [0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16]; const INV_SBOX = new Uint8Array(256); for (let i = 0; i < 256; i++) INV_SBOX[SBOX[i]] = i; const RCON = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]; const xtime = (x) => ((x << 1) ^ ((x >> 7) * 0x1b)) & 0xff; const gmul = (a, b) => { let p = 0; for (let i = 0; i < 8; i++) { if (b & 1) p ^= a; a = xtime(a); b >>= 1; } return p & 0xff; }; const expandKey = (key) => { const w = new Uint8Array(176); w.set(key, 0); for (let i = 4; i < 44; i++) { let t0 = w[4 * (i - 1)], t1 = w[4 * (i - 1) + 1], t2 = w[4 * (i - 1) + 2], t3 = w[4 * (i - 1) + 3]; if (i % 4 === 0) { const tmp = t0; t0 = SBOX[t1] ^ RCON[i / 4 - 1]; t1 = SBOX[t2]; t2 = SBOX[t3]; t3 = SBOX[tmp]; } w[4 * i] = w[4 * (i - 4)] ^ t0; w[4 * i + 1] = w[4 * (i - 4) + 1] ^ t1; w[4 * i + 2] = w[4 * (i - 4) + 2] ^ t2; w[4 * i + 3] = w[4 * (i - 4) + 3] ^ t3; } return w; }; const addRoundKey = (st, w, r) => { for (let c = 0; c < 4; c++) for (let rw = 0; rw < 4; rw++) st[rw + 4 * c] ^= w[16 * r + 4 * c + rw]; }; const shiftRows = (st) => { for (let r = 1; r < 4; r++) { const row = [st[r], st[r + 4], st[r + 8], st[r + 12]]; for (let c = 0; c < 4; c++) st[r + 4 * c] = row[(c + r) % 4]; } }; const invShiftRows = (st) => { for (let r = 1; r < 4; r++) { const row = [st[r], st[r + 4], st[r + 8], st[r + 12]]; for (let c = 0; c < 4; c++) st[r + 4 * c] = row[(c - r + 4) % 4]; } }; const mixColumns = (st) => { for (let c = 0; c < 4; c++) { const a = [st[4 * c], st[4 * c + 1], st[4 * c + 2], st[4 * c + 3]]; st[4 * c] = gmul(a[0], 2) ^ gmul(a[1], 3) ^ a[2] ^ a[3]; st[4 * c + 1] = a[0] ^ gmul(a[1], 2) ^ gmul(a[2], 3) ^ a[3]; st[4 * c + 2] = a[0] ^ a[1] ^ gmul(a[2], 2) ^ gmul(a[3], 3); st[4 * c + 3] = gmul(a[0], 3) ^ a[1] ^ a[2] ^ gmul(a[3], 2); } }; const invMixColumns = (st) => { for (let c = 0; c < 4; c++) { const a = [st[4 * c], st[4 * c + 1], st[4 * c + 2], st[4 * c + 3]]; st[4 * c] = gmul(a[0], 14) ^ gmul(a[1], 11) ^ gmul(a[2], 13) ^ gmul(a[3], 9); st[4 * c + 1] = gmul(a[0], 9) ^ gmul(a[1], 14) ^ gmul(a[2], 11) ^ gmul(a[3], 13); st[4 * c + 2] = gmul(a[0], 13) ^ gmul(a[1], 9) ^ gmul(a[2], 14) ^ gmul(a[3], 11); st[4 * c + 3] = gmul(a[0], 11) ^ gmul(a[1], 13) ^ gmul(a[2], 9) ^ gmul(a[3], 14); } }; const encryptBlock = (st, w) => { addRoundKey(st, w, 0); for (let r = 1; r <= 9; r++) { for (let i = 0; i < 16; i++) st[i] = SBOX[st[i]]; shiftRows(st); mixColumns(st); addRoundKey(st, w, r); } for (let i = 0; i < 16; i++) st[i] = SBOX[st[i]]; shiftRows(st); addRoundKey(st, w, 10); }; const decryptBlock = (st, w) => { addRoundKey(st, w, 10); for (let r = 9; r >= 1; r--) { invShiftRows(st); for (let i = 0; i < 16; i++) st[i] = INV_SBOX[st[i]]; addRoundKey(st, w, r); invMixColumns(st); } invShiftRows(st); for (let i = 0; i < 16; i++) st[i] = INV_SBOX[st[i]]; addRoundKey(st, w, 0); }; // ---- AES-128-ECB/PKCS7(天翼云盘上传参数加密,标准 PKCS7 尾块)---- const aesEcbEncrypt = (plain, key) => { const w = expandKey(key); const pad = 16 - (plain.length % 16); const n = plain.length + pad; const out = new Uint8Array(n); out.set(plain, 0); for (let i = plain.length; i < n; i++) out[i] = pad; for (let off = 0; off < n; off += 16) { const block = out.slice(off, off + 16); encryptBlock(block, w); out.set(block, off); } return out; }; // ---- RSA PKCS#1 v1.5(任意 SPKI 公钥;天翼 generateRsaKey 公钥 DER 解析 + BigInt 模幂)---- const derReadLen = (bytes, i) => { let len = bytes[i]; i++; if (len & 0x80) { const n = len & 0x7f; len = 0; for (let k = 0; k < n; k++) { len = (len << 8) | bytes[i]; i++; } } return { len, next: i }; }; const rsaEncryptWithKey = (pubKeyB64, data) => { const der = b64Decode(pubKeyB64); let i = 0; if (der[i++] !== 0x30) throw new Error('bad SPKI'); const { next: i1 } = derReadLen(der, i); i = i1; if (der[i++] !== 0x30) throw new Error('bad alg seq'); const alg = derReadLen(der, i); i = alg.next + alg.len; // 跳过算法 SEQUENCE 全部内容 if (der[i++] !== 0x03) throw new Error('bad bitstring'); const { next: i3 } = derReadLen(der, i); i = i3; // BIT STRING 内容起点(含未用位字节) if (der[i] === 0x00) i++; if (der[i++] !== 0x30) throw new Error('bad rsa seq'); const { next: i4 } = derReadLen(der, i); i = i4; const readInt = () => { if (der[i++] !== 0x02) throw new Error('bad int'); const { len, next } = derReadLen(der, i); let v = 0n; for (let k = next; k < next + len; k++) v = (v << 8n) | BigInt(der[k]); i = next + len; return v; }; const n = readInt(); const e = readInt(); const blockLen = (n.toString(16).length + 1) >> 1; const maxMsg = blockLen - 11; const out = []; const src = data instanceof Uint8Array ? data : new Uint8Array(data); for (let off = 0; off < src.length; off += maxMsg) { const slice = src.subarray(off, Math.min(off + maxMsg, src.length)); const padCount = blockLen - 2 - slice.length; const buf = new Uint8Array(blockLen); buf[0] = 0; for (let k = 1; k <= padCount; k++) buf[k] = 0x02; buf[padCount + 1] = 0; buf.set(slice, padCount + 2); let msg = 0n; for (const b of buf) msg = (msg << 8n) | BigInt(b); const c = modpow(msg, e, n); const cb = toBytes(c, blockLen); for (const b of cb) out.push(b); } return b64Encode(new Uint8Array(out)); }; const aesCbcEncrypt = (plain, key, iv) => { // p115client 式填充:不足 16 对齐时补 1..15(对齐时不补) let data = plain; const mod = plain.length % 16; if (mod !== 0) { const pad = 16 - mod; data = new Uint8Array(plain.length + pad); data.set(plain, 0); for (let i = plain.length; i < data.length; i++) data[i] = pad; } const w = expandKey(key); const n = data.length; const out = new Uint8Array(n); let prev = iv; for (let off = 0; off < n; off += 16) { const block = data.slice(off, off + 16).slice(); for (let i = 0; i < 16; i++) block[i] ^= prev[i]; encryptBlock(block, w); out.set(block, off); prev = block; } return out; }; const aesCbcDecrypt = (cipher, key, iv) => { const w = expandKey(key); const n = cipher.length - (cipher.length % 16); const out = new Uint8Array(n); let prev = iv; for (let off = 0; off < n; off += 16) { const block = cipher.slice(off, off + 16).slice(); decryptBlock(block, w); for (let i = 0; i < 16; i++) out[off + i] = block[i] ^ prev[i]; prev = cipher.slice(off, off + 16); } return out; }; const unpad115 = (v) => { if (v.length && v[v.length - 1] > 0 && v[v.length - 1] < 16) { const p = v[v.length - 1]; let all = true; for (let i = v.length - p; i < v.length; i++) if (v[i] !== p) { all = false; break; } if (all) return v.slice(0, v.length - p); } return v; }; // ---- LZ4 块解压(2 字节 LE 长度头 + LZ4 block 循环)---- const lz4BlockDecompress = (src) => { const dest = []; let srcPtr = 0; const srcLen = src.length; while (srcPtr < srcLen) { const token = src[srcPtr++]; let litLen = token >> 4; if (litLen === 15) { let sp = srcPtr; try { while (src[srcPtr] === 255) srcPtr++; litLen += (srcPtr - sp) * 255 + src[srcPtr]; srcPtr++; } catch (e) { } } if (litLen) { for (let i = 0; i < litLen; i++) dest.push(src[srcPtr + i]); srcPtr += litLen; } if (srcPtr >= srcLen) break; const offset = src[srcPtr] | (src[srcPtr + 1] << 8); srcPtr += 2; let matchLen = token & 0x0F; if (matchLen === 15) { let sp = srcPtr; try { while (src[srcPtr] === 255) srcPtr++; matchLen += (srcPtr - sp) * 255 + src[srcPtr]; srcPtr++; } catch (e) { } } matchLen += 4; const matchPos = dest.length - offset; if (offset >= matchLen) { for (let i = 0; i < matchLen; i++) dest.push(dest[matchPos + i]); } else if (offset === 1) { for (let i = 0; i < matchLen; i++) dest.push(dest[matchPos]); } else { const endPtr = dest.length + matchLen; let dp = dest.length; while (dp < endPtr) { const copySize = offset < (endPtr - dp) ? offset : (endPtr - dp); for (let i = 0; i < copySize; i++) dest.push(dest[matchPos + i]); dp += copySize; } } } return dest; }; const lz4Decompress = (src) => { const out = []; let p = 0; while (src.length - p > 2 && ((src[p] + (src[p + 1] << 8)) !== 0)) { const blkLen = src[p] + (src[p + 1] << 8); const block = src.slice(p + 2, p + 2 + blkLen); const d = lz4BlockDecompress(block); for (const b of d) out.push(b); p += 2 + blkLen; } return out; }; // ---- k_ec ---- const le32 = (n) => [n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff]; const ecdhEncodeToken = (timestamp) => { const body = []; for (let i = 0; i < 15; i++) body.push(AES_PUBKEY[i]); body.push(0x00, 0x73, 0x00, 0x00, 0x00); for (const b of le32(timestamp >>> 0)) body.push(b); for (let i = 15; i < AES_PUBKEY.length; i++) body.push(AES_PUBKEY[i]); body.push(0x00, 0x01, 0x00, 0x00, 0x00); const crcInput = new Uint8Array(CRC_SALT.length + body.length); crcInput.set(CRC_SALT, 0); crcInput.set(body, CRC_SALT.length); for (const b of le32(crc32(crcInput))) body.push(b); let s = ''; for (const b of body) s += String.fromCharCode(b); return btoa(s); }; // ---- 生成 init 加密请求体 ---- async function makeInitPayload(payload) { const t = Math.floor(Date.now() / 1000); payload.t = String(t); const userid = String(payload.userid); const inner = await sha1Hex(userid + payload.fileid + payload.target + '0'); const sig = (await sha1Hex(payload.userkey + inner + '000000')).toUpperCase(); payload.sig = sig; const token = md5Hex(MD5_SALT + payload.fileid + payload.filesize + (payload.sign_key || '') + (payload.sign_val || '') + userid + t + md5Hex(userid) + payload.appversion); payload.token = token; const sorted = Object.keys(payload).filter(k => payload[k]).sort(); const parts = sorted.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(String(payload[k]))); const data = parts.join('&') + '&'; const body = aesCbcEncrypt(toU8(Array.from(data, ch => ch.charCodeAt(0) & 0xff)), AES_KEY, AES_IV); return { k_ec: ecdhEncodeToken(t), body }; } // ---- 解密 init 响应 ---- async function decryptInitResp(u8) { const raw = unpad115(aesCbcDecrypt(u8, AES_KEY, AES_IV)); const head = String.fromCharCode(raw[0], raw[1]); let text; if (head === '{') text = dec.decode(raw); else text = dec.decode(new Uint8Array(lz4Decompress(raw))); return JSON.parse(text); } // ---- RSA(下载直链,p115cipher 风格)---- const modpow = (base, exp, mod) => { let result = 1n; base %= mod; while (exp > 0n) { if (exp & 1n) result = (result * base) % mod; base = (base * base) % mod; exp >>= 1n; } return result; }; const toBytes = (big, size) => { const b = []; while (big > 0n) { b.unshift(Number(big & 0xffn)); big >>= 8n; } while (b.length < size) b.unshift(0); return b; }; const rsaEncrypt = (input) => { const out = []; const BLOCK = 128; for (let off = 0; off < input.length; off += BLOCK - 11) { const slice = input.subarray(off, Math.min(off + BLOCK - 11, input.length)); const padCount = BLOCK - 2 - slice.length; const buf = new Uint8Array(BLOCK); buf[0] = 0; for (let i = 1; i <= padCount; i++) buf[i] = 0x02; buf[padCount + 1] = 0; buf.set(slice, padCount + 2); let msg = 0n; for (const b of buf) msg = (msg << 8n) | BigInt(b); for (const b of toBytes(modpow(msg, RSA_E, RSA_N), BLOCK)) out.push(b); } return new Uint8Array(out); }; const rsaDecrypt = (input) => { const out = []; const BLOCK = 128; for (let off = 0; off < input.length; off += BLOCK) { const slice = input.subarray(off, Math.min(off + BLOCK, input.length)); let msg = 0n; for (const b of slice) msg = (msg << 8n) | BigInt(b); const ret = modpow(msg, RSA_E, RSA_N); const b = toBytes(ret, Math.max(1, Math.ceil(ret.toString(16).length / 2))); let idx = b.findIndex(x => x === 0); if (idx < 0) { for (const x of b) out.push(x); } else { for (let i = idx + 1; i < b.length; i++) out.push(b[i]); } } return new Uint8Array(out); }; const xorTransform = (data, key) => { const dataSize = data.length, keySize = key.length; const mod = dataSize % 4; if (mod > 0) for (let i = 0; i < mod; i++) data[i] ^= key[i % keySize]; for (let i = mod; i < dataSize; i++) data[i] ^= key[(i - mod) % keySize]; }; const reverseBytes = (data) => { for (let i = 0, j = data.length - 1; i < j; i++, j--) { const t = data[i]; data[i] = data[j]; data[j] = t; } }; const rsaGenKey = (randKey, skLen) => { const k = new Uint8Array(skLen); let length = skLen * (skLen - 1); let index = 0; for (let i = 0; i < skLen; i++) { const x = (randKey[i] + G_KTS[index]) & 0xff; k[i] = G_KTS[length] ^ x; length -= skLen; index += skLen; } return k; }; const b64Encode = (u8) => { let s = ''; for (const b of u8) s += String.fromCharCode(b); return btoa(s); }; const b64Decode = (s) => { const bin = atob(s); const u8 = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i); return u8; }; const rsaEncEncode = (inputBytes) => { const tmp = inputBytes.slice(); xorTransform(tmp, RSA_KEY); reverseBytes(tmp); const xt = tmp.slice(); xorTransform(xt, G_KEY_L); const xorData = new Uint8Array(16 + xt.length); xorData.set(xt, 16); return b64Encode(rsaEncrypt(xorData)); }; const rsaEncDecode = (inputB64) => { const data = rsaDecrypt(b64Decode(inputB64)); const randkey = data.subarray(0, 16); const keyL = rsaGenKey(randkey, 12); const body = data.subarray(16).slice(); xorTransform(body, keyL); reverseBytes(body); const out = body.slice(); xorTransform(out, RSA_KEY); return out; }; return { md5Hex, sha1Hex, makeInitPayload, decryptInitResp, rsaEncEncode, rsaEncDecode, ecdhEncodeToken, aesEcbEncrypt, rsaEncryptWithKey, // 测试钩子 _test: { aesCbcEncrypt, aesCbcDecrypt, lz4Decompress, crc32, sha1HexFallback }, }; })(); // ---- 115 API 封装(GM_xmlhttpRequest 跨域 + 自定义 UA;Cookie 由油猴按域名自动携带)---- const ONE15 = { state: { userid: '', userkey: '', ua: P115_API.UA_DISK }, listCache: [], // 拦截的当前目录列表(配合 DOM 勾选读取) gmRequest(method, url, opts = {}) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('当前环境无 GM_xmlhttpRequest')); return; } const headers = Object.assign({ 'User-Agent': this.state.ua, 'Referer': 'https://115.com/' }, opts.headers || {}); GM_xmlhttpRequest({ method, url, headers, data: opts.data, responseType: opts.responseType || 'text', timeout: opts.timeout || 20000, onload: (r) => { if (r.status < 200 || r.status >= 300) reject(new Error(`HTTP ${r.status} @ ${url}`)); else resolve(r); }, onerror: () => reject(new Error(`网络请求失败 @ ${url}`)), ontimeout: () => reject(new Error(`请求超时 @ ${url}`)), }); }); }, async gmText(method, url, body) { const r = await this.gmRequest(method, url, { data: body, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); return r.responseText; }, /** 获取 user_id/userkey(缓存) */ async ensureUploadInfo() { if (this.state.userid && this.state.userkey) return this.state; const text = await this.gmText('POST', P115_API.UPLOADINFO, ''); const j = JSON.parse(text); if (!j.userkey) throw new Error('获取 115 上传凭证失败(请确认已登录 115 网页版)'); this.state.userid = String(j.user_id); this.state.userkey = j.userkey; return this.state; }, /** 文件列表(分页) */ async getFileList(cid, offset = 0, limit = 115) { const qs = new URLSearchParams({ aid: '1', cid: String(cid), offset: String(offset), limit: String(limit), type: '0', show_dir: '1', fc_mix: '0', natsort: '1', count_folders: '1', format: 'json', custom_order: '0', }); const text = await this.gmText('GET', `${P115_API.LIST}?${qs.toString()}`); const j = JSON.parse(text); if (!j.state) throw new Error(j.error || j.errNo || '列表接口错误'); return { list: j.data || [], count: Number(j.count || 0) }; }, /** 建目录;同名已存在时查回既有目录 cid(幂等重导入) */ async mkdir(pid, name) { const body = new URLSearchParams({ pid: String(pid), cname: String(name) }).toString(); const text = await this.gmText('POST', P115_API.DIR_ADD, body); const j = JSON.parse(text); if (!j.state) { if (/已存在|exist/i.test(String(j.error || j.errno || ''))) { const page = await this.getFileList(pid, 0, 115); const hit = (page.list || []).find(f => !f.sha && f.n === String(name)); if (hit && hit.cid) return String(hit.cid); } throw new Error(j.error || j.errno || '创建目录失败'); } return String(j.file_id || ''); }, /** 删除文件 */ async deleteFiles(fids) { const body = new URLSearchParams(); fids.forEach((f, i) => body.append(`fid[${i}]`, String(f))); const text = await this.gmText('POST', P115_API.DELETE, body.toString()); const j = JSON.parse(text); return !!j.state; }, /** 秒传初始化(加密请求体),返回 {k_ec, body, url} */ async prepareInit({ fileid, filesize, filename, target, signKey, signVal }) { await this.ensureUploadInfo(); const payload = { appid: '0', appversion: P115_API.APP_VERSION, userid: this.state.userid, userkey: this.state.userkey, filename, filesize: String(filesize), fileid: String(fileid).toUpperCase(), target, topupload: 'true', }; if (signKey) { payload.sign_key = signKey; payload.sign_val = signVal; } const { k_ec, body } = await P115_CRYPTO.makeInitPayload(payload); return { url: `${P115_API.INIT}?k_ec=${encodeURIComponent(k_ec)}`, body }; }, /** 执行 init 并解密响应 */ async initUpload(args) { const { url, body } = await this.prepareInit(args); const r = await this.gmRequest('POST', url, { data: body, responseType: 'arraybuffer', binary: true, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); const buf = new Uint8Array(r.response); try { return await P115_CRYPTO.decryptInitResp(buf); } catch (e) { if (r.status !== 200) return { status: 4, statuscode: r.status, statusmsg: String(e) }; throw new Error(`秒传响应解密失败: ${e.message}`); } }, /** 获取下载直链(rsa 加密;UA 必须为 115disk 组合,否则 CDN 拒签) */ async getDownloadUrl(pickcode) { await this.ensureUploadInfo(); const enc = new TextEncoder(); const data = P115_CRYPTO.rsaEncEncode(enc.encode(JSON.stringify({ pickcode, user_id: this.state.userid }))); const t = Math.floor(Date.now() / 1000); const r = await this.gmRequest('POST', `${P115_API.DOWNURL}?t=${t}`, { data: 'data=' + encodeURIComponent(data), headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, responseType: 'arraybuffer', binary: true, }); const raw = new Uint8Array(r.response); let j = null; try { j = JSON.parse(new TextDecoder().decode(raw)); } catch (e) { // 响应可能整体 ECDH-AES 加密(不同请求上下文形态不一) j = await P115_CRYPTO.decryptInitResp(raw); } if (!j.state || !j.data) throw new Error(j.msg || j.error || '获取下载直链失败'); const decoded = P115_CRYPTO.rsaEncDecode(j.data); const obj = JSON.parse(new TextDecoder().decode(decoded)); for (const k of Object.keys(obj)) { const info = obj[k]; if (info && info.url && info.url.url) return info.url.url; } throw new Error('下载直链响应结构异常'); }, /** CDN Range 取字节(只依赖 UA,无需 Cookie) */ async fetchRangeBytes(url, start, end) { const r = await this.gmRequest('GET', url, { headers: { 'Range': `bytes=${start}-${end}` }, responseType: 'arraybuffer', binary: true, timeout: 60000, }); return new Uint8Array(r.response); }, }; // ---- 勾选读取:拦截 115 webapi 列表响应 + DOM 复选框 ---- function p115InjectPageHook() { try { if (window.__P115_LIST__) return; } catch (e) { /* ignore */ } const script = document.createElement('script'); script.textContent = `(${p115PageHookSource.toString()})();`; (document.documentElement || document.head || document.body).appendChild(script); script.remove(); } function p115PageHookSource() { const st = window.__P115_LIST__ = window.__P115_LIST__ || { lists: [], cid: '', at: 0 }; const looksList = (u) => /^https:\/\/webapi\.115\.com\/files\?/i.test(String(u)); const push = (j) => { try { if (!j || !j.state || !Array.isArray(j.data)) return; st.cid = String(j.cid != null ? j.cid : st.cid); const items = j.data.map(f => ({ fid: String(f.fid || ''), pid: String(f.pid || ''), cid: String(f.cid || ''), n: String(f.n || ''), s: Number(f.s || 0), sha: String(f.sha || '').toLowerCase(), pc: String(f.pc || ''), t: String(f.t || ''), ico: String(f.ico || ''), })); st.lists = st.lists.concat(items); if (st.lists.length > 30000) st.lists.splice(0, st.lists.length - 30000); st.at = Date.now(); } catch (e) { /* ignore */ } }; const origFetch = window.fetch ? window.fetch.bind(window) : null; if (origFetch) { window.fetch = async function (input, init) { const resp = await origFetch(input, init); try { const url = typeof input === 'string' ? input : (input && (input.url || input.href)) || ''; if (looksList(resp.url || url) && resp.ok && typeof resp.clone === 'function') { resp.clone().json().then(push).catch(() => { }); } } catch (e) { /* ignore */ } return resp; }; } const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this.__p115Url = url; return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function () { this.addEventListener('load', function () { try { if (looksList(this.__p115Url) && this.status >= 200 && this.status < 300 && this.responseText) { push(JSON.parse(this.responseText)); } } catch (e) { /* ignore */ } }); return origSend.apply(this, arguments); }; } function p115GetPageState() { try { const w = (typeof unsafeWindow !== 'undefined' && unsafeWindow) || window; return w.__P115_LIST__ || {}; } catch (e) { return {}; } } /** 读取勾选:从勾选框向上爬祖先,用拦截列表的已知文件名匹配(img[alt]/file-name 类/文本)。 * 不按父类排除(真实文件行勾选框也在 checkbox-area 内);表头/页脚全选框爬升时匹配不到 * 任何已知文件名(无 img[alt]/file-name 文本),天然被排除;全选时所有行勾选框同步 checked, * 按行匹配即覆盖"全选当前页"。 */ function p115GetCheckedItems() { const st = p115GetPageState(); const curCid = p115GetLastCid(); const known = new Set((Array.isArray(st.lists) ? st.lists : []).map(f => f.n)); const names = new Set(); const cbs = document.querySelectorAll('input[type="checkbox"]'); for (const cb of cbs) { if (!(cb.checked || cb.getAttribute('aria-checked') === 'true')) continue; let el = cb; let found = false; for (let i = 0; i < 6 && el && !found; i++, el = el.parentElement) { if (!el.querySelector) continue; // 行边界判定:当前作用域内必须恰好只有这一个勾选框(容器内多个 → 跳过,避免误配其它行) if (el.querySelectorAll('input[type="checkbox"]').length !== 1) continue; const img = el.querySelector('img[alt]'); if (img && img.alt && known.has(img.alt)) { names.add(img.alt); found = true; break; } const cands = el.querySelectorAll('[class*="file-name"], [class*="truncate"], [class*="title"], [class*="name"]'); for (const c of cands) { const t = (c.textContent || '').replace(/\s+/g, ' ').trim(); if (t && known.has(t)) { names.add(t); found = true; break; } } } } const source = Array.isArray(st.lists) ? st.lists : []; // 限定当前目录 + 名字匹配 + 按 fid 去重(拦截列表可能跨目录/跨页累积) const seen = new Set(); const items = []; for (const f of source) { if (curCid && f.cid && f.cid !== curCid) continue; if (!names.has(f.n)) continue; const key = f.fid || (f.n + ':' + f.s); if (seen.has(key)) continue; seen.add(key); items.push(f); } return { items, names, all: items.length > 0 && items.length === source.filter(f => !curCid || !f.cid || f.cid === curCid).length }; } /** 列表数据兜底:拦截缓存缺失/过期/目录不符时,主动拉取当前目录列表(分页)写入缓存 */ async function p115EnsureListData() { const st = p115GetPageState(); const cur = p115GetLastCid(); const fresh = Date.now() - (Number(st.at) || 0) < 15000; const sameDir = !cur || !st.cid || st.cid === cur; if (Array.isArray(st.lists) && st.lists.length && fresh && sameDir) return; try { st.lists = []; st.cid = cur || '0'; let offset = 0; for (;;) { const page = await ONE15.getFileList(cur || '0', offset, 115); st.lists = st.lists.concat(page.list); if (!page.list.length || page.list.length < 115) break; offset += page.list.length; } st.at = Date.now(); } catch (e) { mcLog('p115', '列表兜底拉取失败: ' + e.message); } } /** 当前目录 cid(URL 参数优先,其次拦截列表) */ function p115GetLastCid() { try { const m = location.search.match(/[?&]cid=([0-9]+)/); if (m && m[1]) return m[1]; } catch (e) { /* ignore */ } const st = p115GetPageState(); return st.cid || '0'; } /** 新版 UI 判定:新版网盘路由为 /storage/...(如 /storage/allfiles);旧版为 115.com/?mode=wangpan(pathname=/)或 ?old=1 */ function p115IsNewUi() { try { return /^\/storage\//.test(location.pathname); } catch (e) { return true; } } /** 旧版 UI 顶部提示条:提醒切换到新版 */ function p115ShowOldUiHint() { try { if (document.getElementById('p115-old-ui-hint')) return; const bar = document.createElement('div'); bar.id = 'p115-old-ui-hint'; bar.style.cssText = 'position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:2147483647;background:#fff7ed;border:1px solid #fdba74;color:#9a3412;padding:10px 16px;border-radius:10px;font-size:13px;line-height:1.6;box-shadow:0 6px 20px rgba(0,0,0,.15);font-family:system-ui,sans-serif;display:flex;align-items:center;gap:12px;max-width:92vw;'; bar.innerHTML = ` 🔄 115 秒传助手仅支持新版网盘界面(全部文件 /storage 页面)。当前为旧版界面,请切换后再使用秒传。 立即切换到新版 → `; document.body.appendChild(bar); bar.querySelector('#p115-old-ui-close').onclick = () => bar.remove(); } catch (e) { /* ignore */ } } /** 平台门禁:新版返回 true(继续初始化);旧版显示提示并返回 false(不挂秒传功能) */ function p115PlatformGate() { if (p115IsNewUi()) return true; setTimeout(p115ShowOldUiHint, 800); return false; } // ---- 导出(生成秒传 JSON,files 内嵌 pc 供 115→115 二次验证中转)---- async function p115GenerateJson() { // 列表兜底:页面钩子可能落后于 SPA 首次列表请求,主动确保当前目录列表数据存在 await p115EnsureListData(); const sel = p115GetCheckedItems(); if (!sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; } const modal = openModal({ title: '生成秒传 JSON(115 网盘)', icon: ICONS.generate, width: 620, content(el) { el.innerHTML = `
准备中...
`; }, footer() { return ''; }, }); const statusEl = modal.contentEl.querySelector('#p115-gen-status'); const barEl = modal.contentEl.querySelector('.gy-ms-bar'); const files = []; const warnMissing = []; let done = 0; const total = sel.items.length; const collect = async (item, prefix) => { if (!item.sha) { warnMissing.push(prefix + item.n + '(未取到 SHA1)'); return; } files.push({ path: prefix + item.n, etag: item.sha, size: item.s, pc: item.pc || '', }); }; const walkFolder = async (cid, prefix) => { let offset = 0; for (;;) { let page; try { page = await ONE15.getFileList(cid, offset); } catch (e) { setStatus(statusEl, `获取目录失败:${prefix} ${e.message}`, 'err'); return; } for (const it of page.list) { if (!it.sha) { await walkFolder(String(it.cid || it.fid), prefix + it.n + '/'); } else { await collect({ n: it.n, s: it.s, sha: it.sha, pc: it.pc }, prefix); done++; updateProgress(barEl, Math.round((done / Math.max(1, total)) * 100), `获取文件信息 ${done}/${total}:${it.n}`); await sleep(CONFIG.md5Delay); } } if (page.list.length < 115) break; offset += page.list.length; await sleep(CONFIG.listPageDelay); } }; try { setStatus(statusEl, sel.all ? `已全选当前目录 ${total} 项,正在获取文件信息...` : `正在获取 ${total} 项的文件信息...`, 'info'); for (const item of sel.items) { if (!item.sha) { await walkFolder(String(item.cid || item.fid), item.n + '/'); } else { await collect(item, ''); done++; updateProgress(barEl, Math.round((done / Math.max(1, total)) * 100), `获取文件信息 ${done}/${total}:${item.n}`); } await sleep(CONFIG.md5Delay); } if (!files.length) { setStatus(statusEl, '未获取到任何文件的 SHA1,无法生成 JSON。', 'err'); modal.footerEl.innerHTML = ``; return; } const totalSize = files.reduce((s, f) => s + (Number(f.size) || 0), 0); const json = { scriptVersion: SCRIPT_VERSION, platform: '115', totalFilesCount: files.length, totalSize, formattedTotalSize: formatSize(totalSize), files, }; const jsonStr = JSON.stringify(json, null, 2); const exportName = '115秒传_Export.json'; modal.contentEl.innerHTML = `
已生成 ${files.length} 项秒传 JSON(115 导出内嵌 pc,可支持大文件同账号二次验证中转)${warnMissing.length ? `;${warnMissing.length} 项未获取到哈希` : ''}
${escapeHtml(jsonStr)}
`; modal.footerEl.innerHTML = ` `; modal.footerEl.querySelector('[data-act="copy"]').onclick = () => { try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); } catch (e) { const ta = document.createElement('textarea'); ta.value = jsonStr; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); showToast('秒传 JSON 已复制到剪贴板'); } }; modal.footerEl.querySelector('[data-act="download"]').onclick = () => { const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = exportName; a.click(); URL.revokeObjectURL(url); }; modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); } catch (e) { setStatus(statusEl, `生成失败:${e.message}`, 'err'); modal.footerEl.innerHTML = ``; } } // ---- 导入(秒传 + 二次验证云端中转)---- function p115NormalizeFile(f) { const sha1 = String(f.sha1 || f.etag || f.hashName || '').toLowerCase(); if (!/^[0-9a-f]{40}$/.test(sha1)) return null; const size = Number(f.size != null ? f.size : f.fileSize || 0); const name = basenameFromPath(f.path) || f.fileName || 'file'; const dir = dirSegmentsFromPath(f.path).join('/'); return { sha1, size, name, dir, pc: String(f.pc || '') }; } function showP115ImportModal() { const modal = openModal({ title: '保存秒传 JSON 到 115 网盘', icon: ICONS.import, width: 640, content(el) { el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:sha1=40 位、hashName=sha1、path、size)。
≥1MB 的文件会触发 115 二次验证(服务器要求随机字节范围的 SHA1):经夸克/UC 中转获取——填写夸克或 UC 的完整 Cookie,脚本先把文件按 sha1 秒传到夸克/UC(宽松秒传),再从云端取验证块;115 同账号导出的 JSON(内嵌 pc)可留空自动中转。path 中的目录会自动创建;全程不产生分享链接。
${IMPORT_FILE_ROW_HTML}
目标目录 cid:
夸克/UC Cookie:
`; }, footer(el) { el.innerHTML = ` `; }, }); const ta = modal.contentEl.querySelector('#p115-import-ta'); const statusEl = modal.contentEl.querySelector('#p115-import-status'); const barEl = modal.contentEl.querySelector('#p115-import-bar'); const barLabelEl = modal.contentEl.querySelector('#p115-import-bar-label'); const detailEl = modal.contentEl.querySelector('#p115-import-detail'); const cookieEl = modal.contentEl.querySelector('#p115-proxy-cookie'); const cidEl = modal.contentEl.querySelector('#p115-target-cid'); const runBtn = modal.footerEl.querySelector('[data-act="run"]'); modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl); modal.contentEl.querySelector('#p115-cid-cur').onclick = () => { cidEl.value = p115GetLastCid(); }; // ========== 一键获取夸克/UC Cookie(跨标签通道,与阿里导入一致) ========== const qkBtn = modal.contentEl.querySelector('#p115-proxy-qk'); const ucBtn = modal.contentEl.querySelector('#p115-proxy-uc'); let pendingTimer = null; function startPendingPoll() { if (pendingTimer) return; pendingTimer = setInterval(() => { let result = ''; try { result = GM_getValue('__pds_cookie_result', ''); } catch (e) { /* ignore */ } if (result) { try { GM_setValue('__pds_cookie_result', ''); } catch (e) { /* ignore */ } cookieEl.value = result; setStatus(statusEl, '已自动填入 ' + (result.indexOf('QK:') === 0 ? '夸克' : 'UC') + ' Cookie,可以开始导入。', 'ok'); clearInterval(pendingTimer); pendingTimer = null; } }, 500); } qkBtn.onclick = () => { try { GM_setValue('__pds_cookie_pending', 'quark'); GM_setValue('__pds_cookie_result', ''); } catch (e) { /* ignore */ } showToast('正在打开夸克网盘获取 Cookie...', 'info'); window.open('https://pan.quark.cn/', '_blank'); startPendingPoll(); }; ucBtn.onclick = () => { try { GM_setValue('__pds_cookie_pending', 'uc'); GM_setValue('__pds_cookie_result', ''); } catch (e) { /* ignore */ } showToast('正在打开 UC 网盘获取 Cookie...', 'info'); window.open('https://drive.uc.cn/', '_blank'); startPendingPoll(); }; runBtn.onclick = async () => { const vr = validateImportJson(ta.value); if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; } const cid = cidEl.value.trim(); let json; try { json = JSON.parse(ta.value); } catch (e) { setStatus(statusEl, `校验失败:${e.message}`, 'err'); return; } runBtn.disabled = true; runBtn.textContent = '导入中...'; barEl.style.display = 'block'; detailEl.style.display = 'none'; let summary; try { summary = await p115ImportJson(json.files || [], cid, (pct, label) => { updateProgress(barEl, pct, label); if (barLabelEl) barLabelEl.textContent = label; setStatus(statusEl, label, 'info'); }, { sourceCookie: cookieEl ? cookieEl.value.trim() : '' }); } catch (e) { setStatus(statusEl, `导入失败:${e.message}`, 'err'); runBtn.disabled = false; runBtn.textContent = '开始导入'; return; } runBtn.disabled = false; runBtn.textContent = '开始导入'; const detailLines = []; if (summary.failures.length) detailLines.push(...summary.failures); if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(未提交接口) =========='); detailLines.push(...summary.skippedDetail); } if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; } const warn = summary.fail > 0 || summary.skipped > 0; setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok'); if (warn) { try { const logBtn = document.createElement('button'); logBtn.className = 'gy-ms-btn gy-ms-btn-outline'; logBtn.textContent = '导出日志'; logBtn.style.marginTop = '8px'; logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); }; statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling); } catch (e) { /* ignore */ } } }; } /** 核心导入:逐文件秒传;≥1MB 触发二次验证时经「PDS 中转站」取范围字节(先 sha1 丢给夸克/UC 宽松秒传 → 临时直链 → Range), * 或 115 同账号 pc 中转。全程不产生分享链接,直链内存瞬存。 */ async function p115ImportJson(files, rootCid, onProgress, opts) { const sourceCookie = (opts && opts.sourceCookie ? String(opts.sourceCookie).trim() : ''); await ONE15.ensureUploadInfo(); const dirCache = new Map(); dirCache.set('', String(rootCid || '0')); const ensureDir = async (dirPath) => { if (!dirPath) return dirCache.get(''); if (dirCache.has(dirPath)) return dirCache.get(dirPath); const segs = dirPath.split('/').filter(Boolean); let cid = dirCache.get(''); let cur = ''; for (const seg of segs) { cur = cur ? cur + '/' + seg : seg; if (dirCache.has(cur)) { cid = dirCache.get(cur); continue; } cid = await ONE15.mkdir(cid, seg); dirCache.set(cur, cid); await sleep(CONFIG.mkdirDelay); } dirCache.set(dirPath, cid); return cid; }; const importOne = async (f) => { const norm = p115NormalizeFile(f); if (!norm) return { name: (f.path || f.fileName || '?'), ok: false, kind: 'skip', reason: '缺少 SHA1(40位hex)' }; const dirId = await ensureDir(norm.dir); const target = `U_1_${dirId}`; let firstResp = await ONE15.initUpload({ fileid: norm.sha1, filesize: norm.size, filename: norm.name, target, }); if (firstResp.status === 2) { return { name: norm.name, ok: true, kind: 'hit', note: '秒传命中(小文件)' }; } if (firstResp.status === 7) { // 二次验证:需读取 sign_check 范围的字节。字节来源优先级: // 1) 填了夸克/UC Cookie(QK:/UC:)→ PDS 中转:先把 sha1 丢给夸克/UC 宽松秒传(内容在全库即登记)→ download 临时直链 → Range 取字节 // (覆盖跨账号 115→115 与跨盘导入;所有底层用阿里云的网盘都可当此中转站) // 2) JSON 内嵌 pc(115 同账号导出)→ 当前账号 chrome/downurl → Range 取字节 // 3) 否则跳过并给出指引 const signKey = firstResp.sign_key; const signCheck = firstResp.sign_check; const m = String(signCheck).match(/^(\d+)-(\d+)$/); if (!m) return { name: norm.name, ok: false, kind: 'skip', reason: 'sign_check 格式异常' }; const start = Number(m[1]); const end = Number(m[2]); let bytes = null; let via = ''; if (/^QK:/i.test(sourceCookie) || /^UC:/i.test(sourceCookie)) { try { bytes = await pdsProxyFetchRange(sourceCookie, norm.sha1, norm.size, start, end); via = '夸克/UC 中转'; } catch (e) { return { name: norm.name, ok: false, kind: 'skip', reason: '夸克/UC 中转失败(' + e.message + ')——请确认 Cookie 有效且内容在夸克/UC 池中' }; } } else if (norm.pc) { try { const dlUrl = await ONE15.getDownloadUrl(norm.pc); bytes = await ONE15.fetchRangeBytes(dlUrl, start, end); via = '115 同账号中转'; } catch (e) { return { name: norm.name, ok: false, kind: 'skip', reason: '115 同账号中转失败(' + e.message + ')——若为跨账号导入,请填写夸克或 UC 的完整 Cookie 走 PDS 中转' }; } } else { return { name: norm.name, ok: false, kind: 'skip', reason: '≥1MB 触发二次验证且无可用中转:请在导入框填写夸克或 UC 的完整 Cookie(跨盘/跨账号),或使用 115 导出的 JSON(同账号自动中转)' }; } const digest = await crypto.subtle.digest('SHA-1', bytes); const signVal = Array.from(new Uint8Array(digest), b => b.toString(16).padStart(2, '0')).join('').toUpperCase(); let retryResp = await ONE15.initUpload({ fileid: norm.sha1, filesize: norm.size, filename: norm.name, target, signKey, signVal, }); if (retryResp.status === 2) { return { name: norm.name, ok: true, kind: 'hit', note: '秒传命中(二次验证通过,' + via + ')' }; } if (retryResp.status === 1) { return { name: norm.name, ok: false, kind: 'miss', reason: '未命中(该 sha1 内容在 115 全库不存在)' }; } if (retryResp.status === 8 || retryResp.status === 4) { return { name: norm.name, ok: false, kind: 'fail', reason: `二次验证失败(status ${retryResp.status} ${retryResp.statusmsg || ''},中转来源 ${via})` }; } return { name: norm.name, ok: false, kind: 'fail', reason: `异常状态 ${retryResp.status}` }; } if (firstResp.status === 1) { return { name: norm.name, ok: false, kind: 'miss', reason: '未命中(该 sha1 内容在 115 全库不存在)' }; } if (firstResp.status === 4 || firstResp.status === 8) { // 偶发 401/参数问题 → 重试一次 const retry = await ONE15.initUpload({ fileid: norm.sha1, filesize: norm.size, filename: norm.name, target, }); if (retry.status === 2) return { name: norm.name, ok: true, kind: 'hit', note: '秒传命中(重试)' }; return { name: norm.name, ok: false, kind: 'fail', reason: `status ${firstResp.status} ${firstResp.statusmsg || ''}` }; } return { name: norm.name, ok: false, kind: 'fail', reason: `异常状态 ${firstResp.status} ${firstResp.statusmsg || ''}` }; }; const results = []; let done = 0; for (const f of files) { try { results.push(await importOne(f)); } catch (e) { results.push({ name: (f.path || f.fileName || '?'), ok: false, kind: 'fail', reason: e.message }); } done++; if (onProgress) onProgress(Math.round((done / files.length) * 100), `秒传 ${done}/${files.length}:${results[results.length - 1].name} → ${results[results.length - 1].ok ? '命中' : results[results.length - 1].reason}`); await sleep(CONFIG.transferDelay); } const failures = results.filter(r => !r.ok && r.kind !== 'skip').map(r => `${r.name}:${r.reason}`); const skippedDetail = results.filter(r => !r.ok && r.kind === 'skip').map(r => `${r.name}:${r.reason}`); const summary = { total: results.length, success: results.filter(r => r.ok).length, fail: results.filter(r => !r.ok && r.kind !== 'skip').length, skipped: results.filter(r => !r.ok && r.kind === 'skip').length, failures, skippedDetail, }; showToast(`115 秒传完成:${summary.success}/${summary.total} 命中`, summary.fail || summary.skipped ? 'warning' : 'ok'); mcLog('p115', `导入完成 ${summary.success}/${summary.total}`); return summary; } // ============================== 天翼云盘平台(v1.9:189.cn 秒传,个人+家庭云,接口已实测 2026-10) ============================== // 协议要点(已实测验证): // - 导出:GET cloud.189.cn/api/open/file/listFiles.action(Cookie 鉴权)返回 name/size/md5(id),**不返回 sliceMd5**。 // ≤10MB 时 sliceMd5=md5(服务端单分片规则,实测 fileDataExists:1 命中);>10MB 必须正确 sliceMd5 // (= MD5(各 10MB 分片 MD5 大写 hex 以 "\n" 连接),openlist 规范实现),只能下载文件计算 → 导出弹窗提供"计算分片哈希"复选(默认勾选,大文件耗时)。 // - 导入:upload.cloud.189.cn 上传接口全程 header 鉴权(SessionKey + EncryptionText(RSA PKCS1v15) + PkId + Signature(HMAC-SHA1) // + X-Request-Date + X-Request-ID),参数 AES-128-ECB/PKCS7 加密(本地实现,零 CDN 依赖)。流程 generateRsaKey → // initMultiUpload(fileMd5+sliceMd5) → data.fileDataExists==1 命中 → commitMultiUploadFile 提交即秒传(实测命中)。 // - 家庭云:列表/建目录走 api.cloud.189.cn/open/family/ 带 AccessToken + Browser-Id + Signature(md5 排序参数) 签名 // (Browser-Id/AccessToken 由页面请求头捕获钩子持久化);上传走 upload.cloud.189.cn 的 /family/ 前缀接口(params 带 familyId), // 同一套 AES/RSA/HMAC 加密。 // - JSON 字段:{path, md5, slice_md5, size};md5 可被百度/夸克/UC/小飞机/光鸭等支持 md5 的平台导入。 const TIANYI = { WEB: 'https://cloud.189.cn', API: 'https://api.cloud.189.cn', UPLOAD: 'https://upload.cloud.189.cn', UPLOAD_APPKEY: '600100422', FAMILY_CTX_KEY: 'cloud189_family_request_context', SLICE: 10485760, // 10MB 标准分片 _sk: '', _skAt: 0, _rsa: null, _familyId: '', userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', gmRequest(method, url, opts = {}) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('当前环境无 GM_xmlhttpRequest')); return; } GM_xmlhttpRequest({ method, url, headers: Object.assign({ 'User-Agent': TIANYI.userAgent, 'Accept': 'application/json, text/plain, */*' }, opts.headers || {}), data: opts.data, responseType: opts.responseType || 'text', timeout: opts.timeout || 60000, onload: (r) => resolve(r), onerror: () => reject(new Error('网络请求失败 @ ' + url)), ontimeout: () => reject(new Error('请求超时 @ ' + url)), }); }); }, isFamilySpace() { return /\/web\/family\//.test(location.pathname); }, getCurrentFolderId() { if (this.isFamilySpace()) { const m = location.pathname.match(/\/web\/family\/file\/folder\/([^/?#]+)/); const v = m ? m[1] : ''; return (v && v !== 'home') ? v : ''; } const m = location.pathname.match(/\/folder\/([^/?#]+)/); const v = m ? m[1] : ''; return (!v || v === 'home') ? '-11' : v; }, strToU8(str) { const u8 = new Uint8Array(String(str).length); for (let i = 0; i < String(str).length; i++) u8[i] = String(str).charCodeAt(i) & 0xff; return u8; }, /** UTF-8 编码的 md5(中文文件名/目录签名必须 UTF-8 字节;BAIDU.md5Hex 是 Latin-1 字节语义) */ md5Utf8(str) { const u8 = new TextEncoder().encode(String(str)); let bin = ''; for (let i = 0; i < u8.length; i += 0x8000) bin += String.fromCharCode.apply(null, u8.subarray(i, i + 0x8000)); return BAIDU.md5Hex(bin); }, uuid4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); }, async getSessionKey(force = false) { if (!force && this._sk && Date.now() - this._skAt < 30 * 60 * 1000) return this._sk; // 1) getUserBriefInfo 返回当前账号权威 sessionKey(已实测) try { const resp = await this.gmRequest('GET', this.WEB + '/v2/getUserBriefInfo.action', { headers: { 'Accept': 'application/json;charset=UTF-8' } }); const j = JSON.parse(resp.responseText); if (j && j.sessionKey) { this._sk = String(j.sessionKey); this._skAt = Date.now(); return this._sk; } } catch (e) { /* ignore */ } // 2) sessionStorage / localStorage / cookie 兜底 try { const s = sessionStorage.getItem('sessionKey'); if (s) { this._sk = String(s); this._skAt = Date.now(); return this._sk; } } catch (e) { /* ignore */ } try { for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); const v = localStorage.getItem(k); if (v && String(k).toLowerCase().includes('sessionkey')) { this._sk = String(v); this._skAt = Date.now(); return this._sk; } } } catch (e) { /* ignore */ } try { for (const c of document.cookie.split(';')) { const p = c.trim().split('='); if (/^SESSION_KEY$/i.test(p[0])) { this._sk = p.slice(1).join('='); this._skAt = Date.now(); return this._sk; } } } catch (e) { /* ignore */ } return this._sk || ''; }, async getRsaKey(force = false) { if (!force && this._rsa && this._rsa.expire > Date.now() + 60000) return this._rsa; const sk = await this.getSessionKey(); if (!sk) throw new Error('无法获取 SessionKey,请刷新天翼云盘页面'); const ts = String(Date.now()); const sign = BAIDU.md5Hex('AppKey=' + this.UPLOAD_APPKEY + '&Timestamp=' + ts); const resp = await this.gmRequest('GET', this.WEB + '/api/security/generateRsaKey.action?sessionKey=' + encodeURIComponent(sk), { headers: { 'Sign-Type': '1', 'Signature': sign, 'Timestamp': ts, 'AppKey': this.UPLOAD_APPKEY, 'SessionKey': sk, 'Accept': 'application/json' }, }); const text = resp.responseText; if (resp.status < 200 || resp.status >= 300) throw new Error('generateRsaKey HTTP ' + resp.status); let pubKey = '', pkId = '', expire = 0; try { const j = JSON.parse(text); pubKey = j.pubKey || ''; pkId = j.pkId || ''; expire = parseInt(j.expire) || 0; } catch (e) { /* ignore */ } if (!pubKey) { pubKey = (text.match(/([^<]+)([^<]+)([^<]+) 64) { const h = fallback(keyStr); k = new Uint8Array(20); for (let i = 0; i < 20; i++) k[i] = parseInt(h.substr(i * 2, 2), 16); } const ipad = new Uint8Array(64), opad = new Uint8Array(64); for (let i = 0; i < 64; i++) { const b = i < k.length ? k[i] : 0; ipad[i] = b ^ 0x36; opad[i] = b ^ 0x5c; } const bin = (u8) => { let s = ''; for (let i = 0; i < u8.length; i += 0x8000) s += String.fromCharCode.apply(null, u8.subarray(i, i + 0x8000)); return s; }; const innerHex = fallback(bin(ipad) + msgStr); let innerBin = ''; for (let i = 0; i < innerHex.length; i += 2) innerBin += String.fromCharCode(parseInt(innerHex.substr(i, 2), 16)); return fallback(bin(opad) + innerBin).toUpperCase(); }, /** upload.cloud.189.cn 加密请求(AES 参数 + RSA 会话钥 + HMAC 签名;纯 header 鉴权) */ async uploadRequest(uri, paramsObj) { const sk = await this.getSessionKey(); if (!sk) throw new Error('无法获取 SessionKey,请刷新天翼云盘页面'); const rsa = await this.getRsaKey(); const uuid = Array.from({ length: 16 }, () => '0123456789abcdef'.charAt(Math.floor(Math.random() * 16))).join(''); const paramStr = Object.entries(paramsObj).map(([k, v]) => k + '=' + v).join('&'); const enc = this.aesEcbHex(paramStr, uuid); const ts = String(Date.now()); const signature = this.hmacSha1Hex(uuid, 'SessionKey=' + sk + '&Operate=GET&RequestURI=' + uri + '&Date=' + ts + '¶ms=' + enc); const encryptionText = P115_CRYPTO.rsaEncryptWithKey(rsa.pubKey, this.strToU8(uuid)); const resp = await this.gmRequest('GET', this.UPLOAD + uri + '?params=' + enc, { headers: { 'X-Request-Date': ts, 'X-Request-ID': this.uuid4(), 'SessionKey': sk, 'EncryptionText': encryptionText, 'PkId': rsa.pkId, 'Signature': signature, 'Accept': 'application/json;charset=UTF-8', }, }); let j; try { j = JSON.parse(resp.responseText); } catch (e) { throw new Error('上传接口响应解析失败: ' + String(resp.responseText).slice(0, 150)); } if (j.errorCode) throw new Error(j.errorCode === 'InvalidSessionKey' ? '登录已过期,请刷新天翼云盘页面' : (j.errorMsg || j.errorCode)); if (j.code !== 'SUCCESS') throw new Error(j.msg || j.errorMsg || j.error || ('上传接口失败 ' + (j.code || '') + ' ' + String(resp.responseText).slice(0, 120))); return j; }, /** 个人文件列表(Cookie 鉴权),返回 fileListAO */ async listFilesPersonal(folderId, pageNum = 1, pageSize = 60) { const qs = new URLSearchParams({ noCache: String(Math.random()), pageSize: String(pageSize), pageNum: String(pageNum), mediaType: '0', folderId: String(folderId), iconOption: '5', orderBy: 'lastOpTime', descending: 'true', }); const resp = await this.gmRequest('GET', this.WEB + '/api/open/file/listFiles.action?' + qs.toString(), { headers: { 'Accept': 'application/json;charset=UTF-8', 'Sign-Type': '1' } }); const j = JSON.parse(resp.responseText); if (j.res_code !== 0) throw new Error(j.res_message || '获取文件列表失败'); return j.fileListAO || { fileList: [], folderList: [], count: 0 }; }, /** 家庭文件列表(AccessToken 签名),返回 fileListAO */ async listFilesFamily(folderId, pageNum = 1, pageSize = 60) { const familyId = await this.getCurrentFamilyId(); if (!familyId) throw new Error('无法获取 familyId'); const url = this.API + '/open/family/file/listFiles.action?pageSize=' + pageSize + '&pageNum=' + pageNum + '&mediaType=0&familyId=' + encodeURIComponent(familyId) + '&folderId=' + encodeURIComponent(folderId || '') + '&iconOption=5&orderBy=3&descending=true'; const j = await this.familyFetchJson(url); return j.fileListAO || { fileList: [], folderList: [], count: 0 }; }, async listFiles(folderId, pageNum, pageSize) { return this.isFamilySpace() ? this.listFilesFamily(folderId, pageNum, pageSize) : this.listFilesPersonal(folderId, pageNum, pageSize); }, // ---- 家庭云签名 ---- getFamilyContext() { try { const raw = sessionStorage.getItem(this.FAMILY_CTX_KEY) || localStorage.getItem(this.FAMILY_CTX_KEY); if (raw) { const j = JSON.parse(raw); return { browserId: String(j.browserId || ''), accessToken: String(j.accessToken || '') }; } } catch (e) { /* ignore */ } return { browserId: '', accessToken: '' }; }, getAccessToken() { const ctx = this.getFamilyContext(); if (ctx.accessToken) return ctx.accessToken; try { const t = localStorage.getItem('accessToken'); if (t) return t; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); const v = localStorage.getItem(k); if (k && String(k).toLowerCase().includes('accesstoken') && v) return v; } } catch (e) { /* ignore */ } return ''; }, buildFamilySignature(url, extraParams) { try { const entries = []; try { const u = new URL(url); for (const [k, v] of u.searchParams.entries()) entries.push([k, v]); } catch (e) { /* ignore */ } if (extraParams && typeof extraParams === 'object') { for (const [k, v] of Object.entries(extraParams)) entries.push([k, v == null ? '' : String(v)]); } entries.sort((a, b) => a[0].localeCompare(b[0])); const timestamp = String(Date.now()); const parts = ['AccessToken=' + this.getAccessToken(), 'Timestamp=' + timestamp]; for (const [k, v] of entries) parts.push(k + '=' + v); return { timestamp, signature: this.md5Utf8(parts.join('&')) }; } catch (e) { return { timestamp: '', signature: '' }; } }, familyHeaders(url, extraParams) { const ctx = this.getFamilyContext(); const h = { 'Accept': 'application/json;charset=UTF-8', 'Sign-Type': '1' }; const at = this.getAccessToken(); if (at) h.AccessToken = at; if (ctx.browserId) h['Browser-Id'] = ctx.browserId; const sig = this.buildFamilySignature(url, extraParams); if (sig && sig.signature) { h.Signature = sig.signature; h.Timestamp = sig.timestamp; } return h; }, async familyFetchJson(url, extraParams, method = 'GET', body) { const headers = this.familyHeaders(url, extraParams); const resp = await this.gmRequest(method, url, { headers, data: body }); const j = JSON.parse(resp.responseText); if ((j.res_code != null && j.res_code !== 0) || j.errorCode) { throw new Error(j.errorMsg || j.res_message || '家庭接口失败'); } return j; }, async getCurrentFamilyId() { if (this._familyId) return this._familyId; const j = await this.familyFetchJson(this.API + '/open/family/manage/getFamilyList.action'); const list = j.familyInfoResp || []; const cur = list.find(x => Number(x.useFlag) === 1) || list[0]; this._familyId = cur && cur.familyId ? String(cur.familyId) : ''; return this._familyId; }, // ---- 目录 ---- async getFolderIdByNamePersonal(parentFolderId, folderName) { const ao = await this.listFilesPersonal(parentFolderId, 1, 60); const f = (ao.folderList || []).find(x => x.name === folderName); return f ? String(f.id) : null; }, async createFolderPersonal(parentFolderId, folderName) { const resp = await this.gmRequest('POST', this.WEB + '/api/open/file/createFolder.action?noCache=' + Math.random(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json;charset=UTF-8', 'Sign-Type': '1' }, data: 'parentFolderId=' + encodeURIComponent(String(parentFolderId == null ? '' : parentFolderId)) + '&folderName=' + encodeURIComponent(folderName), }); let j; const t = resp.responseText; try { j = JSON.parse(t); } catch (e) { j = {}; } if (j.res_code === 0 && (j.id != null)) return String(j.id); if (String(j.res_message || '').includes('已存在')) { const id = await this.getFolderIdByNamePersonal(parentFolderId, folderName); if (id) return id; } if (j.id != null) return String(j.id); const id = await this.getFolderIdByNamePersonal(parentFolderId, folderName); if (id) return id; throw new Error('创建目录失败(文件夹名: ' + folderName + ')'); }, async getFamilyFolderIdByName(parentFolderId, folderName) { try { const familyId = await this.getCurrentFamilyId(); if (!familyId) return null; const url = this.API + '/open/family/file/listFiles.action?pageSize=60&pageNum=1&mediaType=0&familyId=' + encodeURIComponent(familyId) + '&folderId=' + encodeURIComponent(parentFolderId || '') + '&iconOption=5&orderBy=3&descending=true'; const j = await this.familyFetchJson(url); const f = ((j.fileListAO && j.fileListAO.folderList) || []).find(x => x.name === folderName); return f ? String(f.id || f.fileId) : null; } catch (e) { return null; } }, async createFolderFamily(parentFolderId, folderName) { const familyId = await this.getCurrentFamilyId(); if (!familyId) throw new Error('无法获取 familyId'); const url = this.API + '/open/family/file/createFolder.action?noCache=' + Math.random(); const params = { folderName, familyId: String(familyId), parentId: String(parentFolderId == null ? '' : parentFolderId) }; const headers = this.familyHeaders(url, params); headers['Content-Type'] = 'application/x-www-form-urlencoded'; const body = Object.entries(params).map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v)).join('&'); const resp = await this.gmRequest('POST', url, { headers, data: body }); const j = JSON.parse(resp.responseText); if (j.id != null) return String(j.id); if ((j.res_code != null && j.res_code !== 0) || j.errorCode) { if (String(j.res_message || '').includes('已存在')) { const id = await this.getFamilyFolderIdByName(parentFolderId, folderName); if (id) return id; } throw new Error(j.errorMsg || j.res_message || '家庭目录创建失败'); } const id = await this.getFamilyFolderIdByName(parentFolderId, folderName); if (id) return id; throw new Error('创建家庭目录失败(文件夹名: ' + folderName + ')'); }, async createFolder(parentFolderId, folderName) { return this.isFamilySpace() ? this.createFolderFamily(parentFolderId, folderName) : this.createFolderPersonal(parentFolderId, folderName); }, // ---- 下载链接 + Range(分片哈希计算用)---- async getFileDownloadUrl(fileId) { try { if (this.isFamilySpace()) { const familyId = await this.getCurrentFamilyId(); if (!familyId) return ''; const url = this.API + '/open/family/file/getFileDownloadUrl.action?fileId=' + encodeURIComponent(String(fileId)) + '&familyId=' + encodeURIComponent(familyId) + '&type=1'; const j = await this.familyFetchJson(url); return j.fileDownloadUrl ? String(j.fileDownloadUrl).replace(/&/g, '&') : ''; } const resp = await this.gmRequest('GET', this.WEB + '/api/open/file/getFileDownloadUrl.action?fileId=' + encodeURIComponent(String(fileId)), { headers: { 'Accept': 'application/json;charset=UTF-8', 'Sign-Type': '1' } }); const j = JSON.parse(resp.responseText); return j.fileDownloadUrl || ''; } catch (e) { return ''; } }, async downloadRange(url, offset, length) { const resp = await this.gmRequest('GET', url, { headers: { 'Range': 'bytes=' + offset + '-' + (offset + length - 1), 'Referer': location.href }, responseType: 'arraybuffer', timeout: 120000, }); if (resp.status < 200 || resp.status >= 300) throw new Error('下载片段失败 HTTP ' + resp.status); try { return new Uint8Array(resp.response); } catch (e) { return new Uint8Array(resp.responseText.split('').map(ch => ch.charCodeAt(0) & 0xff)); } }, // ---- 秒传(2026 实测:必须 init(lazyCheck) → checkTransSecond 命中 → commit 三步流程; // 直接带哈希 init+commit 会被 InfoSecurityError「md5 in black list, security check not pass」拒绝)---- async rapidUploadFile(fileName, fileSize, fileMd5, sliceMd5, parentFolderId) { const family = this.isFamilySpace(); const md5u = String(fileMd5 || '').toUpperCase(); const smu = String(sliceMd5 || '').toUpperCase(); const initParams = { parentFolderId: String((family && (parentFolderId == null || parentFolderId === '')) ? '0' : (parentFolderId == null ? '' : parentFolderId)), fileName: encodeURIComponent(String(fileName || '')), fileSize: String(Number(fileSize) || 0), sliceSize: String(this.SLICE), lazyCheck: '1', }; if (family) { const famId = await this.getCurrentFamilyId(); if (famId) initParams.familyId = String(famId); } const initUri = family ? '/family/initMultiUpload' : '/person/initMultiUpload'; const checkUri = family ? '/family/checkTransSecond' : '/person/checkTransSecond'; const commitUri = family ? '/family/commitMultiUploadFile' : '/person/commitMultiUploadFile'; const init = await this.uploadRequest(initUri, initParams); const uploadFileId = (init.data && init.data.uploadFileId) || ''; if (!uploadFileId) return { hit: false, reason: '初始化未返回 uploadFileId' }; const chk = await this.uploadRequest(checkUri, { fileMd5: md5u, sliceMd5: smu, uploadFileId: String(uploadFileId) }); const exists = Number((chk.data && chk.data.fileDataExists) || 0); if (exists !== 1) return { hit: false, reason: '未命中秒传池(服务器上不存在该内容哈希)' }; const commit = await this.uploadRequest(commitUri, { uploadFileId: String(uploadFileId), fileMd5: md5u, sliceMd5: smu, lazyCheck: '1', opertype: '3', }); const fid = (commit.file && (commit.file.userFileId || commit.file.fileId)) || (commit.data && commit.data.fileId) || ''; return { hit: true, fileId: String(fid || '') }; }, }; /** 安装家庭接口请求头捕获钩子(Browser-Id/AccessToken → sessionStorage/localStorage) */ function tianyiInstallFamilyHook() { const persist = (detail = {}) => { const headers = detail.headers || {}; const context = { browserId: headers['Browser-Id'] || headers['browser-id'] || headers.browserId || '', accessToken: headers.accesstoken || headers.AccessToken || '', }; if (!context.browserId && !context.accessToken) return; try { const serialized = JSON.stringify({ ...context, url: detail.url || '', updatedAt: Date.now() }); sessionStorage.setItem(TIANYI.FAMILY_CTX_KEY, serialized); localStorage.setItem(TIANYI.FAMILY_CTX_KEY, serialized); } catch (e) { /* ignore */ } }; document.addEventListener('cloud189-family-request', (event) => persist(event.detail || {})); if (window.__gyMS189FamilyHookInstalled) return; window.__gyMS189FamilyHookInstalled = true; const script = document.createElement('script'); script.textContent = `(${function () { if (window.__cloud189FamilyHookInstalled) return; window.__cloud189FamilyHookInstalled = true; function shouldCapture(url) { return typeof url === 'string' && url.indexOf('/open/family/') !== -1; } function normalizeHeaders(input) { const result = {}; if (!input) return result; try { if (typeof Headers !== 'undefined' && input instanceof Headers) { input.forEach(function (value, key) { result[key] = value; }); return result; } } catch (e) { /* ignore */ } if (Array.isArray(input)) { input.forEach(function (entry) { if (Array.isArray(entry) && entry.length >= 2) result[String(entry[0])] = entry[1]; }); return result; } if (typeof input === 'object') { Object.keys(input).forEach(function (key) { result[key] = input[key]; }); return result; } return result; } function emit(url, headers) { try { document.dispatchEvent(new CustomEvent('cloud189-family-request', { detail: { url: url, headers: normalizeHeaders(headers) } })); } catch (e) { /* ignore */ } } if (window.fetch) { const originalFetch = window.fetch; window.fetch = function (input, init) { const url = typeof input === 'string' ? input : (input && input.url) || ''; if (shouldCapture(url)) { const requestHeaders = {}; if (input && input.headers) Object.assign(requestHeaders, normalizeHeaders(input.headers)); if (init && init.headers) Object.assign(requestHeaders, normalizeHeaders(init.headers)); emit(url, requestHeaders); } return originalFetch.apply(this, arguments); }; } const originalOpen = XMLHttpRequest.prototype.open; const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this.__gyMs189Url = url; this.__gyMs189Headers = {}; return originalOpen.apply(this, arguments); }; XMLHttpRequest.prototype.setRequestHeader = function (key, value) { try { if (this.__gyMs189Headers) this.__gyMs189Headers[key] = value; } catch (e) { /* ignore */ } return originalSetRequestHeader.apply(this, arguments); }; XMLHttpRequest.prototype.send = function () { if (shouldCapture(this.__gyMs189Url)) emit(this.__gyMs189Url, this.__gyMs189Headers || {}); return originalSend.apply(this, arguments); }; }})();`; try { (document.documentElement || document.head || document.body).appendChild(script); script.remove(); } catch (e) { /* ignore */ } } /** 勾选读取:.c-file-item 行内 ant-checkbox-input(悬停显示)+ 列表头部全选框 */ function tianyiGetCheckedItems() { const items = []; const rows = Array.from(document.querySelectorAll('.c-file-item')); let headChecked = false; try { const headCb = Array.from(document.querySelectorAll('input.ant-checkbox-input, input[type="checkbox"]')).find(cb => !cb.closest('.c-file-item')); if (headCb && headCb.checked && headCb.offsetParent !== null) headChecked = true; } catch (e) { /* ignore */ } for (const row of rows) { try { const cb = row.querySelector('input.ant-checkbox-input, input[type="checkbox"]'); const fileId = row.getAttribute('data-fileid') || ''; const isFolder = row.getAttribute('data-isfolder') === 'true'; const nameEl = row.querySelector('.file-item-name-div, .file-item-name'); const name = nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : ''; if (!name) continue; if (headChecked || (cb && cb.checked)) items.push({ fileId: String(fileId), name, isFolder }); } catch (e) { /* ignore */ } } if (!items.length) { // 兜底:Vue 组件内可能已把勾选状态渲染成 ant-checkbox checked(防 hover 未触发) try { for (const row of rows) { const cb = row.querySelector('.ant-checkbox, .ant-checkbox-input'); if (cb && (cb.classList.contains('ant-checkbox-checked') || cb.checked)) { const fileId = row.getAttribute('data-fileid') || ''; const isFolder = row.getAttribute('data-isfolder') === 'true'; const nameEl = row.querySelector('.file-item-name-div, .file-item-name'); const name = nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : ''; if (name) items.push({ fileId: String(fileId), name, isFolder }); } } } catch (e) { /* ignore */ } } return { all: headChecked, items }; } /** 拉取目录全部页,建 id→信息 map */ async function tianyiListFolderMap(folderId) { const filesMap = new Map(); const foldersMap = new Map(); let pageNum = 1; let seen = 0; while (true) { const ao = await TIANYI.listFiles(folderId, pageNum, 60); const fl = ao.fileList || []; const dl = ao.folderList || []; for (const f of fl) filesMap.set(String(f.id), { name: String(f.name || ''), size: Number(f.size || 0), md5: String(f.md5 || '') }); for (const d of dl) foldersMap.set(String(d.id), String(d.name || '')); seen += fl.length + dl.length; const count = Number(ao.count || 0); if (!fl.length && !dl.length) break; if (count > 0 && seen >= count) break; pageNum++; await sleep(CONFIG.listPageDelay); } return { filesMap, foldersMap }; } /** 递归导出目录(勾选文件夹=递归) */ async function tianyiCollectFolder(folderId, prefix, files, warnMissing, statusEl, barEl, counter) { const map = await tianyiListFolderMap(folderId); for (const [id, d] of map.filesMap) { const md5 = String(d.md5 || '').toUpperCase(); if (!/^[0-9A-F]{32}$/.test(md5)) { warnMissing.push(prefix + d.name + ':无 md5(可能仍在处理中)'); continue; } const size = Number(d.size) || 0; const f = { path: prefix + d.name, fileId: String(id), size, md5, sliceMd5: '' }; if (size <= TIANYI.SLICE) f.sliceMd5 = md5; files.push(f); counter.done++; if (statusEl) setStatus(statusEl, `收集 ${counter.done}/${counter.total}:${f.path}`, 'info'); if (barEl) updateProgress(barEl, Math.round(counter.done / Math.max(1, counter.total) * 100), `收集 ${counter.done}/${counter.total}`); await sleep(CONFIG.md5Delay); } for (const id of map.foldersMap.keys()) { const fname = map.foldersMap.get(id); await tianyiCollectFolder(id, prefix + fname + '/', files, warnMissing, statusEl, barEl, counter); } } /** 勾选项 → 收集文件(当前目录文件查 map;文件夹递归) */ async function tianyiExportCollect(items, curFilesMap, files, warnMissing, statusEl, barEl, counter) { for (const item of items) { const name = String(item.name || ''); if (!name) { warnMissing.push('(未命名条目)'); continue; } if (item.isFolder && item.fileId) { if (statusEl) setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info'); await tianyiCollectFolder(item.fileId, name + '/', files, warnMissing, statusEl, barEl, counter); } else if (item.fileId) { const d = curFilesMap.get(String(item.fileId)); if (!d) { warnMissing.push(name + ':未能匹配到列表信息'); continue; } const md5 = String(d.md5 || '').toUpperCase(); if (!/^[0-9A-F]{32}$/.test(md5)) { warnMissing.push(name + ':无 md5(可能仍在处理中)'); continue; } const size = Number(d.size) || 0; const f = { path: d.name || name, fileId: String(item.fileId), size, md5, sliceMd5: '' }; if (size <= TIANYI.SLICE) f.sliceMd5 = md5; files.push(f); counter.done++; if (statusEl) setStatus(statusEl, `收集 ${counter.done}/${counter.total}:${f.path}`, 'info'); if (barEl) updateProgress(barEl, Math.round(counter.done / Math.max(1, counter.total) * 100), `收集 ${counter.done}/${counter.total}`); await sleep(CONFIG.md5Delay); } else { warnMissing.push(name + ':缺少文件 ID'); } } } /** 下载文件计算天翼分片哈希(10MB 分片 md5 → sliceMd5;内存恒定,每片瞬存) */ async function tianyiComputeSliceMd5(file, onProgress) { try { const size = Number(file.size); if (size <= TIANYI.SLICE) return file.md5 || ''; const url = await TIANYI.getFileDownloadUrl(file.fileId); if (!url) return ''; const md5s = []; let offset = 0; while (offset < size) { const len = Math.min(TIANYI.SLICE, size - offset); const buf = await TIANYI.downloadRange(url, offset, len); if (!buf || !buf.length) return ''; md5s.push(baiduBytesToMd5Hex(buf).toUpperCase()); offset += len; if (onProgress) onProgress(Math.min(99, Math.round(offset / size * 100))); } if (onProgress) onProgress(100); return BAIDU.md5Hex(md5s.join('\n')).toUpperCase(); } catch (e) { mcLog('tianyi', '分片哈希计算失败: ' + e.message); return ''; } } /** 天翼导出:勾选 → 收集(md5 零下载)→ 可选下载计算 sliceMd5 → JSON */ async function tianyiGenerateJson() { let sel; try { sel = tianyiGetCheckedItems(); } catch (e) { showToast('读取勾选失败:' + e.message, 'error'); return; } if (!sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; } const modal = openModal({ title: '生成秒传 JSON(天翼云盘)', icon: ICONS.generate, width: 640, content(el) { el.innerHTML = `
准备中...
`; }, footer() { return ''; }, }); const statusEl = modal.contentEl.querySelector('#gy-ms-189-gen-status'); const barEl = modal.contentEl.querySelector('.gy-ms-bar'); const sliceCheck = modal.contentEl.querySelector('#gy-ms-189-gen-slice'); const wantSlice = () => sliceCheck && sliceCheck.checked; const files = []; const warnMissing = []; const counter = { done: 0, total: sel.items.length }; try { setStatus(statusEl, sel.all ? `已全选当前目录 ${sel.items.length} 项,正在收集信息...` : `正在收集 ${sel.items.length} 项...`, 'info'); const curFolderId = TIANYI.getCurrentFolderId(); const curMap = await tianyiListFolderMap(curFolderId); await tianyiExportCollect(sel.items, curMap.filesMap, files, warnMissing, statusEl, barEl, counter); if (!files.length) { setStatus(statusEl, '未获取到任何文件的 md5,无法生成 JSON。' + (warnMissing.length ? '\n' + warnMissing.slice(0, 5).join('\n') : ''), 'err'); modal.footerEl.innerHTML = ``; return; } const need = files.filter(f => !f.sliceMd5 && Number(f.size) > TIANYI.SLICE); if (wantSlice() && need.length) { setStatus(statusEl, `正在下载计算天翼分片哈希(${need.length} 个文件)...`, 'info'); for (let i = 0; i < need.length; i++) { const f = need[i]; setStatus(statusEl, `计算分片 ${i + 1}/${need.length}:${f.path}(${formatSize(f.size)})`, 'info'); const sm = await tianyiComputeSliceMd5(f, (pct) => updateProgress(barEl, Math.round(pct), `分片 ${i + 1}/${need.length}:${f.path} ${pct}%`)); if (sm) f.sliceMd5 = sm; else warnMissing.push(f.path + ':分片哈希计算失败(下载受限),该条目仅有 md5'); } } else if (need.length) { for (const f of need) warnMissing.push(f.path + ':>10MB 未计算分片哈希(未勾选/取消计算),天翼秒传不可用,md5 可导入其他盘'); } const finalFiles = files.filter(f => f.md5); if (!finalFiles.length) { setStatus(statusEl, '未获取到任何可用条目,无法生成 JSON。', 'err'); modal.footerEl.innerHTML = ``; return; } const totalSize = finalFiles.reduce((s, f) => s + (Number(f.size) || 0), 0); const json = { scriptVersion: SCRIPT_VERSION, platform: '天翼云盘', totalFilesCount: finalFiles.length, totalSize, formattedTotalSize: formatSize(totalSize), files: finalFiles.map(f => ({ path: f.path, md5: f.md5 || '', slice_md5: f.sliceMd5 || '', size: Number(f.size) || 0, })), }; const jsonStr = JSON.stringify(json, null, 2); const exportName = CONFIG.defaultExportName + '_天翼.json'; const withSlice = finalFiles.filter(f => f.sliceMd5).length; modal.contentEl.innerHTML = `
已生成 ${finalFiles.length} 项秒传 JSON${withSlice ? `(${withSlice} 项含分片哈希)` : ''}${warnMissing.length ? `;${warnMissing.length} 项未完整` : ''}
含 slice_md5 的 JSON 可被任意天翼账号秒传导入(>10MB 需分片哈希,≤10MB 自动=md5);md5 字段可导入百度/夸克/UC/小飞机等支持 md5 的平台。
${escapeHtml(jsonStr)}
`; modal.footerEl.innerHTML = ` `; modal.footerEl.querySelector('[data-act="copy"]').onclick = () => { try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); } catch (e) { showToast('复制失败:' + e.message, 'error'); } }; modal.footerEl.querySelector('[data-act="download"]').onclick = () => { try { const blob = new Blob([jsonStr], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = exportName; a.click(); URL.revokeObjectURL(a.href); showToast('JSON 已下载'); } catch (e) { showToast('下载失败:' + e.message, 'error'); } }; modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); mcLog('tianyi', `导出完成 ${finalFiles.length} 项`); } catch (e) { setStatus(statusEl, '导出失败:' + e.message, 'err'); modal.footerEl.innerHTML = ``; mcLog('tianyi', '导出失败: ' + e.message); } } /** JSON 条目解析(天翼导入专用;≤10MB 缺 slice_md5 时用 md5 顶替) */ function tianyiParseImportRows(text) { const obj = JSON.parse(text); const list = Array.isArray(obj.files) ? obj.files : []; const rows = []; const skipped = []; for (const f of list) { const path = String(f.path || f.name || '').trim() || 'file'; const size = Number(f.size != null ? f.size : 0); const md5 = normalizeMd5(f.etag || f.md5 || f.fileMd5 || ''); let sliceMd5 = normalizeMd5(f.slice_md5 || f.sliceMd5 || ''); if (!path || !md5) { skipped.push(path + ':缺少 md5(32 位 hex),无法天翼秒传'); continue; } if (!sliceMd5) { if (size <= TIANYI.SLICE) sliceMd5 = md5; else { skipped.push(path + ':>10MB 缺少 slice_md5(天翼秒传必需;请由天翼云盘导出并勾选"计算分片哈希",或该 JSON 仅用于其他盘导入)'); continue; } } rows.push({ path, md5, sliceMd5, size }); } return { rows, skipped }; } /** 天翼导入:目录自动创建 + initMultiUpload/commitMultiUploadFile 秒传 */ async function tianyiImportJson(rows, onProgress) { const rootId = TIANYI.getCurrentFolderId(); const dirCache = new Map(); dirCache.set('', String(rootId == null ? '' : rootId)); const ensureDir = async (dirPath) => { if (!dirPath) return dirCache.get(''); if (dirCache.has(dirPath)) return dirCache.get(dirPath); const segs = dirPath.split('/').filter(Boolean); let cur = ''; let parent = dirCache.get(''); for (const seg of segs) { cur = cur ? cur + '/' + seg : seg; if (dirCache.has(cur)) { parent = dirCache.get(cur); continue; } const id = await TIANYI.createFolder(parent || '', seg); if (!id) throw new Error('创建目录失败:' + seg); dirCache.set(cur, id); parent = id; await sleep(CONFIG.mkdirDelay); } dirCache.set(dirPath, parent); return parent; }; const results = []; let done = 0; for (const row of rows) { try { const segs = dirSegmentsFromPath(row.path); const fileName = basenameFromPath(row.path); const parent = await ensureDir(segs.join('/')); const r = await TIANYI.rapidUploadFile(fileName, row.size, row.md5, row.sliceMd5, parent); results.push({ name: row.path, ok: r.hit, kind: r.hit ? 'hit' : 'miss', note: r.reason || '秒传命中' }); } catch (e) { results.push({ name: row.path, ok: false, kind: 'fail', reason: e.message }); } done++; const last = results[results.length - 1]; if (onProgress) onProgress(Math.round((done / rows.length) * 100), `秒传 ${done}/${rows.length}:${last.name} → ${last.ok ? '命中' : (last.reason || last.note)}`); await sleep(CONFIG.transferDelay); } const failures = results.filter(r => !r.ok && r.kind !== 'skip').map(r => `${r.name}:${r.reason || r.note}`); const skippedDetail = results.filter(r => !r.ok && r.kind === 'skip').map(r => `${r.name}:${r.reason}`); const summary = { total: results.length, success: results.filter(r => r.ok).length, fail: results.filter(r => !r.ok && r.kind !== 'skip').length, skipped: results.filter(r => !r.ok && r.kind === 'skip').length, failures, skippedDetail, }; showToast(`天翼秒传完成:${summary.success}/${summary.total} 命中`, summary.fail || summary.skipped ? 'warning' : 'ok'); mcLog('tianyi', `导入完成 ${summary.success}/${summary.total}`); return summary; } /** 天翼导入模态框 */ function showTianyiImportModal() { const modal = openModal({ title: '保存秒传 JSON 到天翼云盘(当前目录)', icon: ICONS.import, width: 640, content(el) { el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:md5=32 位 hex、slice_md5=天翼分片哈希、path、size)。
天翼秒传需要 md5 + slice_md5:≤10MB 文件 slice_md5 自动 = md5(其他盘带 md5 的 JSON 即可秒传);>10MB 缺少 slice_md5 的条目将跳过(需由天翼导出时勾选"计算分片哈希")。path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`; }, footer(el) { el.innerHTML = ` `; }, }); const ta = modal.contentEl.querySelector('#gy-ms-189-import-ta'); const fileNameEl = modal.contentEl.querySelector('#gy-ms-189-import-filename'); const statusEl = modal.contentEl.querySelector('#gy-ms-189-import-status'); const barEl = modal.contentEl.querySelector('#gy-ms-189-import-bar'); const detailEl = modal.contentEl.querySelector('#gy-ms-189-import-detail'); const runBtn = modal.footerEl.querySelector('[data-act="run"]'); modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); try { attachImportFilePicker(modal, ta, fileNameEl, statusEl); } catch (e) { /* ignore */ } runBtn.onclick = async () => { let rows = [], skipped = []; try { const r = tianyiParseImportRows(ta.value); rows = r.rows; skipped = r.skipped; } catch (e) { setStatus(statusEl, 'JSON 解析失败:' + e.message, 'err'); return; } if (!rows.length) { setStatus(statusEl, '没有可导入的条目(需要 md5;>10MB 还需 slice_md5)' + (skipped.length ? '\n示例提示:' + skipped.slice(0, 3).join('\n') : ''), 'err'); return; } runBtn.disabled = true; runBtn.textContent = '导入中...'; barEl.style.display = 'block'; detailEl.style.display = 'none'; let summary; try { summary = await tianyiImportJson(rows, (pct, label) => { updateProgress(barEl, pct, label); setStatus(statusEl, label, 'info'); }); } catch (e) { setStatus(statusEl, `导入失败:${e.message}`, 'err'); runBtn.disabled = false; runBtn.textContent = '开始导入'; return; } runBtn.disabled = false; runBtn.textContent = '开始导入'; const detailLines = []; if (summary.failures.length) detailLines.push(...summary.failures); if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(未提交接口) =========='); detailLines.push(...summary.skippedDetail); } if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; } const warn = summary.fail > 0 || summary.skipped > 0; setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok'); if (warn) { try { const logBtn = document.createElement('button'); logBtn.className = 'gy-ms-btn gy-ms-btn-outline'; logBtn.textContent = '导出日志'; logBtn.style.marginTop = '8px'; logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); }; statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling); } catch (e) { /* ignore */ } } }; } // ============================== 联通云盘(pan.wo.cn / 沃盘)v1.10:零下载导出 md5 ============================== // 协议来源:2026-10 实测(wohome dispatcher:AES-CBC(accessToken[:16] + 固定IV) + header md5 签名 + Accesstoken 头) // 哈希来源:GetDownloadUrlV2 → 下载 URL → Range: bytes=0-0 读响应头 ETag(单分片文件 ≤~10MB 时 = 完整内容 MD5;多分片大文件为 OSS 分片格式 md5hex-N,原样记录 etag 字段) // 边界:联通无哈希秒传接口(上传 upload2C 必须携带文件本体、无预检;列表 fileUniqueValue/sha256 恒空),故仅支持导出,导入提示不支持 const WOPAN = { BASE: 'https://panservice.mail.wo.cn', CLIENT_ID: '1001000021', IV: 'wNSOYIB1k1DjY5lA', // AES-CBC 固定 IV UA: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0', _token: '', _tokenAt: 0, /** 从 localStorage.wocloudinfo(base64 → JSON字符串 → JSON)取 accessToken,缓存 30 分钟 */ getToken() { if (this._token && Date.now() - this._tokenAt < 30 * 60 * 1000) return this._token; try { const raw = localStorage.getItem('wocloudinfo'); if (!raw) return ''; const decoded = decodeURIComponent(escape(atob(raw))); const obj = JSON.parse(JSON.parse(decoded)); this._token = (obj && obj.user && obj.user.token) || ''; this._tokenAt = Date.now(); } catch (e) { this._token = ''; } return this._token; }, gmReq(method, url, opts = {}) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('当前环境无 GM_xmlhttpRequest')); return; } GM_xmlhttpRequest({ method, url, headers: Object.assign({ 'User-Agent': WOPAN.UA, 'Origin': 'https://pan.wo.cn', 'Referer': 'https://pan.wo.cn/', 'Accept': 'application/json, text/plain, */*', }, opts.headers || {}), data: opts.data, responseType: opts.responseType || 'text', timeout: opts.timeout || 60000, onload: (r) => resolve(r), onerror: () => reject(new Error('网络请求失败 @ ' + url)), ontimeout: () => reject(new Error('请求超时 @ ' + url)), }); }); }, /** 标准 PKCS7 填充(无论是否对齐都补 1..16) */ pkcs7(data) { const mod = data.length % 16; const pad = 16 - mod; const out = new Uint8Array(data.length + pad); out.set(data, 0); for (let i = data.length; i < out.length; i++) out[i] = pad; return out; }, unpad(data) { if (!data || !data.length) return data; const p = data[data.length - 1]; if (p >= 1 && p <= 16) { let ok = true; for (let i = data.length - p; i < data.length; i++) { if (data[i] !== p) { ok = false; break; } } if (ok) return data.slice(0, data.length - p); } return data; }, keyBytes() { const tok = this.getToken(); const u8 = new Uint8Array(16); for (let i = 0; i < 16; i++) u8[i] = (tok.charCodeAt(i) || 0) & 0xff; return u8; }, ivBytes() { return new TextEncoder().encode(WOPAN.IV); }, b64ToBytes(b64) { const bin = atob(b64); const u8 = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i); return u8; }, /** 加密请求参数:JSON → UTF-8 → PKCS7 → AES-CBC → base64 */ encryptParam(json) { const data = this.pkcs7(new TextEncoder().encode(JSON.stringify(json))); const enc = P115_CRYPTO._test.aesCbcEncrypt(data, this.keyBytes(), this.ivBytes()); let s = ''; for (const b of enc) s += String.fromCharCode(b); return btoa(s); }, /** 解密响应 DATA:base64 → AES-CBC → 去 PKCS7 → UTF-8 */ decryptData(b64) { const plain = P115_CRYPTO._test.aesCbcDecrypt(this.b64ToBytes(b64), this.keyBytes(), this.ivBytes()); return new TextDecoder('utf-8').decode(this.unpad(plain)); }, /** wohome dispatcher 通用调用(本地实现加密+签名,同浏览器前端协议) */ async dispatcher(key, param) { const token = this.getToken(); if (!token) throw new Error('未获取到联通云盘令牌(请先登录并刷新页面)'); const resTime = Date.now(); const reqSeq = Math.floor(Math.random() * 8999) + 1e5; const sign = BAIDU.md5Hex(String(key) + resTime + reqSeq + 'wohome' + ''); const bodyObj = { header: { key, resTime, reqSeq, channel: 'wohome', version: '', sign }, body: { param: param ? this.encryptParam(param) : '', secret: true }, }; if (!param) delete bodyObj.body.param; const r = await this.gmReq('POST', this.BASE + '/wohome/dispatcher', { headers: { 'Accesstoken': token, 'Content-Type': 'application/json;charset=UTF-8' }, data: JSON.stringify(bodyObj), }); let j; try { j = JSON.parse(r.responseText); } catch (e) { throw new Error('联通接口响应解析失败: ' + String(r.responseText).slice(0, 150)); } if (j.STATUS !== '200') throw new Error('联通接口状态异常: ' + (j.MSG || j.STATUS)); const rsp = j.RSP || {}; if (rsp.RSP_CODE !== '0000') { if (rsp.RSP_CODE === '9999') throw new Error('令牌已过期,请刷新联通云盘页面后重试'); throw new Error('联通接口失败: ' + (rsp.RSP_DESC || rsp.RSP_CODE)); } const data = rsp.DATA; if (data === undefined || data === null || data === '') return data; if (typeof data === 'string') { const b64 = (data.startsWith('"') && data.endsWith('"')) ? data.slice(1, -1) : data; try { return JSON.parse(this.decryptData(b64)); } catch (e) { /* 明文 DATA */ } try { return JSON.parse(data); } catch (e2) { return data; } } return data; }, /** 文件列表(pageNum 从 0 开始,100/页) */ async listFiles(dirId, spaceType, familyId, pageNum) { const param = { spaceType: String(spaceType || '0'), parentDirectoryId: String(dirId || '0'), pageNum: pageNum || 0, pageSize: 100, sortRule: 0, clientId: WOPAN.CLIENT_ID, }; if (String(spaceType) === '1') param.familyId = String(familyId || ''); const data = await this.dispatcher('QueryAllFiles', param); return (data && data.files) || []; }, /** 批量取下载链接:fidList → { fid: downloadUrl } */ async getDownloadUrls(fidList) { const data = await this.dispatcher('GetDownloadUrlV2', { type: '1', fidList: fidList, clientId: WOPAN.CLIENT_ID, }); const list = (data && data.list) || (Array.isArray(data) ? data : []); const map = {}; for (const it of list) if (it && it.fid && it.downloadUrl) map[it.fid] = it.downloadUrl; return map; }, /** 对下载 URL 发 Range: bytes=0-0,读响应头 ETag(零下载) * - 单分片文件(≤~10MB):ETag = 完整内容 MD5(32 hex) * - 多分片大文件:ETag = OSS 分片格式 "md5hex-N"(非内容 MD5,原样记录到 etag 字段供兼容) */ async getEtag(url) { const r = await this.gmReq('GET', url, { headers: { 'Range': 'bytes=0-0', 'Accept': '*/*' }, timeout: 30000, }); const hdrs = String(r.responseHeaders || ''); const m = hdrs.match(/etag:\s*"?([0-9a-fA-F-]+)"?/i); if (m) return m[1].toLowerCase(); return ''; }, }; /** 找到文件列表页 Vue 组件(含 tableData/multipleSelection/cloudType/familyId) */ function wopanFindPageVm() { const walk = (seed) => { let vm = seed; for (let i = 0; i < 12 && vm; i++) { if (vm.$data && Array.isArray(vm.$data.tableData)) return vm; vm = vm.$parent; } return null; }; try { const tr = document.querySelector('tr.el-table__row'); if (tr) { const v = walk(tr.__vue__ || (tr.firstElementChild && tr.firstElementChild.__vue__)); if (v) return v; } } catch (e) { /* ignore */ } try { const table = document.querySelector('.el-table'); if (table) { const v = walk(table.__vue__ || (table.firstElementChild && table.firstElementChild.__vue__)); if (v) return v; } } catch (e) { /* ignore */ } try { const cont = document.querySelector('[class*="file-list-container"], [class*="operation-container"]'); if (cont) { const v = walk(cont.__vue__ || (cont.firstElementChild && cont.firstElementChild.__vue__)); if (v) return v; } } catch (e) { /* ignore */ } return null; } /** 勾选读取:页面 Vue 组件的 multipleSelection(el-table 选中行,含表头全选)+ allChecked/selectALlFileModel */ function wopanGetCheckedItems() { const vm = wopanFindPageVm(); if (!vm) return { all: false, items: [], state: null }; const d = vm.$data; let items = []; try { items = (d.multipleSelection || []).filter(x => x && x.id); } catch (e) { /* ignore */ } if (!items.length) { try { const tr = document.querySelector('tr.el-table__row'); let b = tr && tr.__vue__ && tr.__vue__.$parent; if (b && b.store && b.store.states && b.store.states.selection && b.store.states.selection.length) { items = b.store.states.selection.filter(x => x && x.id); } } catch (e) { /* ignore */ } } let all = false; try { all = !!(d.allChecked || d.selectALlFileModel); } catch (e) { /* ignore */ } if (!all && !items.length) { try { const headInput = document.querySelector('.el-table__header input[type="checkbox"]'); if (headInput && headInput.checked) all = true; } catch (e) { /* ignore */ } } let state = { spaceType: '0', familyId: '', currentDirId: '0' }; try { state.spaceType = String(d.cloudType != null ? d.cloudType : '0'); state.familyId = String(d.familyId || ''); const first = (d.tableData || []).find(x => x && x.parentDirectoryId); state.currentDirId = first ? String(first.parentDirectoryId) : '0'; } catch (e) { /* ignore */ } return { all, items, state }; } /** 拉取目录全部页(文件+文件夹) */ async function wopanListDirAll(dirId, state) { const out = []; let pageNum = 0; for (;;) { const list = await WOPAN.listFiles(dirId, state.spaceType, state.familyId, pageNum); out.push(...list); if (list.length < 100) break; pageNum++; await sleep(CONFIG.listPageDelay); } return out; } /** 导出收集:顶层条目(文件→记录 fid;文件夹→递归扫描) */ async function wopanExportCollect(items, files, warnMissing, state, statusEl, barEl, counter, prefix) { for (const item of items) { const name = String(item.name || ''); if (!name) { warnMissing.push('(未命名条目)'); counter.done++; continue; } const path = (prefix || '') + name; if (Number(item.type) === 0) { if (statusEl) setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info'); try { const sub = await wopanListDirAll(item.id, state); await wopanExportCollect(sub, files, warnMissing, state, statusEl, barEl, counter, path + '/'); } catch (e) { warnMissing.push(path + ':扫描文件夹失败 ' + e.message); } } else if (item.fid) { files.push({ path, fid: String(item.fid), size: Number(item.size) || 0 }); counter.done++; if (statusEl) setStatus(statusEl, `收集 ${counter.done}/${counter.total}:${path}`, 'info'); if (barEl) updateProgress(barEl, Math.round(counter.done / Math.max(1, counter.total) * 100), `收集 ${counter.done}/${counter.total}`); await sleep(CONFIG.md5Delay); } else { warnMissing.push(path + ':缺少 fid'); counter.done++; } } } /** 联通导出:勾选/全选 → 收集(目录递归)→ 批量取下载链接 → Range 读 ETag 取 md5(零下载)→ JSON */ async function wopanGenerateJson() { const sel = wopanGetCheckedItems(); if (!sel.all && !sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹(可勾选列表左上角全选框)', 'warning'); return; } const modal = openModal({ title: '生成秒传 JSON(联通云盘)', icon: ICONS.generate, width: 640, content(el) { el.innerHTML = `
准备中...
零下载取哈希:文件列表 → 下载链接 → 响应头 ETag。单分片文件(≤~10MB)ETag = 完整内容 MD5;多分片大文件 ETag 为 OSS 分片格式,原样记录到 JSON 的 etag 字段。
注意:联通云盘无哈希秒传接口(上传必须携带文件本体、列表不返回哈希),本 JSON 的 md5 可用于百度/夸克/UC/小飞机等支持 md5 的平台导入。
`; }, footer() { return ''; }, }); const statusEl = modal.contentEl.querySelector('#gy-ms-wo-gen-status'); const barEl = modal.contentEl.querySelector('.gy-ms-bar'); const files = []; const warnMissing = []; try { let topItems = []; if (sel.all) { setStatus(statusEl, `已全选当前目录,正在拉取全部文件列表(${sel.state.currentDirId === '0' ? '根目录' : '当前目录'})...`, 'info'); topItems = await wopanListDirAll(sel.state.currentDirId, sel.state); setStatus(statusEl, `当前目录共 ${topItems.length} 项,正在收集...`, 'info'); } else { topItems = sel.items; setStatus(statusEl, `正在收集 ${sel.items.length} 项...`, 'info'); } if (!topItems.length) { setStatus(statusEl, '当前目录没有可导出的内容。', 'err'); modal.footerEl.innerHTML = ``; return; } const counter = { done: 0, total: topItems.length }; await wopanExportCollect(topItems, files, warnMissing, sel.state, statusEl, barEl, counter); if (!files.length) { setStatus(statusEl, '未获取到任何文件(可能勾选的全是空文件夹)。' + (warnMissing.length ? '\n' + warnMissing.slice(0, 5).join('\n') : ''), 'err'); modal.footerEl.innerHTML = ``; return; } setStatus(statusEl, `已收集 ${files.length} 个文件,正在批量获取下载链接并读取 ETag(零下载)...`, 'info'); updateProgress(barEl, 0, '取哈希 0/' + files.length); const BATCH = 20; const withMd5 = []; const withEtag = []; for (let i = 0; i < files.length; i += BATCH) { const batch = files.slice(i, i + BATCH); let urlMap = {}; try { urlMap = await WOPAN.getDownloadUrls(batch.map(f => f.fid)); } catch (e) { for (const f of batch) warnMissing.push(f.path + ':获取下载链接失败 ' + e.message); continue; } for (const f of batch) { const url = urlMap[f.fid]; if (!url) { warnMissing.push(f.path + ':无下载链接'); continue; } try { const etag = await WOPAN.getEtag(url); if (/^[0-9a-f]{32}$/.test(etag)) { f.md5 = etag; f.etag = ''; withMd5.push(f); setStatus(statusEl, `取 md5 ${withMd5.length}/${files.length}:${f.path}(零下载)`, 'info'); updateProgress(barEl, Math.round((withMd5.length + withEtag.length) / files.length * 100), `取哈希 ${withMd5.length + withEtag.length}/${files.length}`); } else if (/^[0-9a-f]{32}-[0-9]+$/.test(etag)) { // 多分片大文件:ETag 为 OSS 分片格式,非内容 MD5 → 原样记录 etag 字段 f.md5 = ''; f.etag = etag; withEtag.push(f); warnMissing.push(f.path + ':>10MB 分片文件,ETag 为分片格式(' + etag + ')非内容 MD5,已记录 etag 字段;如需跨盘秒传请先在支持下载计算的平台导出'); setStatus(statusEl, `取 etag ${withMd5.length + withEtag.length}/${files.length}:${f.path}(分片格式)`, 'info'); updateProgress(barEl, Math.round((withMd5.length + withEtag.length) / files.length * 100), `取哈希 ${withMd5.length + withEtag.length}/${files.length}`); } else { warnMissing.push(f.path + ':响应头无有效 ETag'); } } catch (e) { warnMissing.push(f.path + ':取哈希失败 ' + e.message); } await sleep(CONFIG.md5Delay); } } if (!withMd5.length && !withEtag.length) { setStatus(statusEl, '未获取到任何文件的哈希,无法生成 JSON。' + (warnMissing.length ? '\n' + warnMissing.slice(0, 5).join('\n') : ''), 'err'); modal.footerEl.innerHTML = ``; return; } const totalSize = (withMd5.concat(withEtag)).reduce((s, f) => s + (Number(f.size) || 0), 0); const json = { scriptVersion: SCRIPT_VERSION, platform: '联通云盘', totalFilesCount: withMd5.length + withEtag.length, totalSize, formattedTotalSize: formatSize(totalSize), files: (withMd5.concat(withEtag)).map(f => { const o = { path: f.path, md5: f.md5 || '', size: Number(f.size) || 0, }; if (f.etag) o.etag = f.etag; // 大文件分片 ETag(OSS 格式 md5hex-N,非内容 MD5,仅供兼容尝试) return o; }), }; const jsonStr = JSON.stringify(json, null, 2); const exportName = CONFIG.defaultExportName + '_联通.json'; modal.contentEl.innerHTML = `
已生成 ${withMd5.length + withEtag.length} 项秒传 JSON:${withMd5.length} 项含内容 MD5(零下载)${withEtag.length ? `,${withEtag.length} 项仅记录分片 ETag` : ''}${warnMissing.length ? `;${warnMissing.length} 项未完整` : ''}
md5 = 完整内容 MD5(下载链接响应头 ETag,仅单分片文件 ≤~10MB 可用);大文件(多分片)ETag 为 OSS 分片格式(md5hex-N),已写入 etag 字段供各盘兼容尝试,非内容 MD5。
联通云盘无秒传接口,本 JSON 可在百度/夸克/UC/小飞机等支持 md5 的平台导入(仅 md5 字段条目)。
${escapeHtml(jsonStr)}
`; modal.footerEl.innerHTML = ` `; modal.footerEl.querySelector('[data-act="copy"]').onclick = () => { try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); } catch (e) { showToast('复制失败:' + e.message, 'error'); } }; modal.footerEl.querySelector('[data-act="download"]').onclick = () => { try { const blob = new Blob([jsonStr], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = exportName; a.click(); URL.revokeObjectURL(a.href); showToast('JSON 已下载'); } catch (e) { showToast('下载失败:' + e.message, 'error'); } }; modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); mcLog('wopan', `导出完成 ${withMd5.length + withEtag.length} 项`); } catch (e) { setStatus(statusEl, '导出失败:' + e.message, 'err'); modal.footerEl.innerHTML = ``; mcLog('wopan', '导出失败: ' + e.message); } } // ---- 样式注入 ---- function insertStyle() { if (document.getElementById('gy-ms-style')) return; const style = document.createElement('style'); style.id = 'gy-ms-style'; style.textContent = STYLES; (document.head || document.documentElement).appendChild(style); style.textContent += ` .gy-ms-dropdown-item:hover{background:#f0fdf4;color:#059669;} .gy-ms-main-btn:hover{background:#059669;} `; } // ============================== 移动云盘平台(v1.11:yun.139.com,SHA256 秒传,协议实测 2026) ============================== // 协议来源:139FastLink(个人版,MIT)移植——/file/list 列表自带 contentHash(SHA256);/file/create 秒传(exist/rapidUpload 命中) // 边界:列表自带 64hex SHA256 零下载导出;导入仅支持 SHA256(32 位 MD5 JSON 无法在移动云盘秒传) // JSON 说明:etag 字段存 64 位 SHA256(仅供移动云盘自身导入;其他平台按 md5 解析会跳过该条目) const Y139 = (() => { // ---- MD5(mcloud-sign 签名,移植自 139FastLink)---- function md5(inputString) { const hc = "0123456789abcdef"; function rh(n) { let j, s = ""; for (j = 0; j <= 3; j++) s += hc.charAt((n >> (j * 8 + 4)) & 0x0F) + hc.charAt((n >> (j * 8)) & 0x0F); return s; } function ad(x, y) { const l = (x & 0xFFFF) + (y & 0xFFFF), m = (x >> 16) + (y >> 16) + (l >> 16); return (m << 16) | (l & 0xFFFF); } function rl(n, c) { return (n << c) | (n >>> (32 - c)); } function cm(q, a, b, x, s, t) { return ad(rl(ad(ad(a, q), ad(x, t)), s), b); } function ff(a, b, c, d, x, s, t) { return cm((b & c) | ((~b) & d), a, b, x, s, t); } function gg(a, b, c, d, x, s, t) { return cm((b & d) | (c & (~d)), a, b, x, s, t); } function hh(a, b, c, d, x, s, t) { return cm(b ^ c ^ d, a, b, x, s, t); } function ii(a, b, c, d, x, s, t) { return cm(c ^ (b | (~d)), a, b, x, s, t); } function sb(x) { const bytes = new TextEncoder().encode(x); let i; const nblk = ((bytes.length + 8) >> 6) + 1; const blks = new Array(nblk * 16); for (i = 0; i < nblk * 16; i++) blks[i] = 0; for (i = 0; i < bytes.length; i++) blks[i >> 2] |= bytes[i] << ((i % 4) * 8); blks[i >> 2] |= 0x80 << ((i % 4) * 8); blks[nblk * 16 - 2] = bytes.length * 8; return blks; } let i, x = sb(inputString), a = 1732584193, b = -271733879, c = -1732584194, d = 271733878, olda, oldb, oldc, oldd; for (i = 0; i < x.length; i += 16) { olda = a; oldb = b; oldc = c; oldd = d; a = ff(a, b, c, d, x[i + 0], 7, -680876936); d = ff(d, a, b, c, x[i + 1], 12, -389564586); c = ff(c, d, a, b, x[i + 2], 17, 606105819); b = ff(b, c, d, a, x[i + 3], 22, -1044525330); a = ff(a, b, c, d, x[i + 4], 7, -176418897); d = ff(d, a, b, c, x[i + 5], 12, 1200080426); c = ff(c, d, a, b, x[i + 6], 17, -1473231341); b = ff(b, c, d, a, x[i + 7], 22, -45705983); a = ff(a, b, c, d, x[i + 8], 7, 1770035416); d = ff(d, a, b, c, x[i + 9], 12, -1958414417); c = ff(c, d, a, b, x[i + 10], 17, -42063); b = ff(b, c, d, a, x[i + 11], 22, -1990404162); a = ff(a, b, c, d, x[i + 12], 7, 1804603682); d = ff(d, a, b, c, x[i + 13], 12, -40341101); c = ff(c, d, a, b, x[i + 14], 17, -1502002290); b = ff(b, c, d, a, x[i + 15], 22, 1236535329); a = gg(a, b, c, d, x[i + 1], 5, -165796510); d = gg(d, a, b, c, x[i + 6], 9, -1069501632); c = gg(c, d, a, b, x[i + 11], 14, 643717713); b = gg(b, c, d, a, x[i + 0], 20, -373897302); a = gg(a, b, c, d, x[i + 5], 5, -701558691); d = gg(d, a, b, c, x[i + 10], 9, 38016083); c = gg(c, d, a, b, x[i + 15], 14, -660478335); b = gg(b, c, d, a, x[i + 4], 20, -405537848); a = gg(a, b, c, d, x[i + 9], 5, 568446438); d = gg(d, a, b, c, x[i + 14], 9, -1019803690); c = gg(c, d, a, b, x[i + 3], 14, -187363961); b = gg(b, c, d, a, x[i + 8], 20, 1163531501); a = gg(a, b, c, d, x[i + 13], 5, -1444681467); d = gg(d, a, b, c, x[i + 2], 9, -51403784); c = gg(c, d, a, b, x[i + 7], 14, 1735328473); b = gg(b, c, d, a, x[i + 12], 20, -1926607734); a = hh(a, b, c, d, x[i + 5], 4, -378558); d = hh(d, a, b, c, x[i + 8], 11, -2022574463); c = hh(c, d, a, b, x[i + 11], 16, 1839030562); b = hh(b, c, d, a, x[i + 14], 23, -35309556); a = hh(a, b, c, d, x[i + 1], 4, -1530992060); d = hh(d, a, b, c, x[i + 4], 11, 1272893353); c = hh(c, d, a, b, x[i + 7], 16, -155497632); b = hh(b, c, d, a, x[i + 10], 23, -1094730640); a = hh(a, b, c, d, x[i + 13], 4, 681279174); d = hh(d, a, b, c, x[i + 0], 11, -358537222); c = hh(c, d, a, b, x[i + 3], 16, -722521979); b = hh(b, c, d, a, x[i + 6], 23, 76029189); a = hh(a, b, c, d, x[i + 9], 4, -640364487); d = hh(d, a, b, c, x[i + 12], 11, -421815835); c = hh(c, d, a, b, x[i + 15], 16, 530742520); b = hh(b, c, d, a, x[i + 2], 23, -995338651); a = ii(a, b, c, d, x[i + 0], 6, -198630844); d = ii(d, a, b, c, x[i + 7], 10, 1126891415); c = ii(c, d, a, b, x[i + 14], 15, -1416354905); b = ii(b, c, d, a, x[i + 5], 21, -57434055); a = ii(a, b, c, d, x[i + 12], 6, 1700485571); d = ii(d, a, b, c, x[i + 3], 10, -1894986606); c = ii(c, d, a, b, x[i + 10], 15, -1051523); b = ii(b, c, d, a, x[i + 1], 21, -2054922799); a = ii(a, b, c, d, x[i + 8], 6, 1873313359); d = ii(d, a, b, c, x[i + 15], 10, -30611744); c = ii(c, d, a, b, x[i + 6], 15, -1560198380); b = ii(b, c, d, a, x[i + 13], 21, 1309151649); a = ii(a, b, c, d, x[i + 4], 6, -145523070); d = ii(d, a, b, c, x[i + 11], 10, -1120210379); c = ii(c, d, a, b, x[i + 2], 15, 718787259); b = ii(b, c, d, a, x[i + 9], 21, -343485551); a = ad(a, olda); b = ad(b, oldb); c = ad(c, oldc); d = ad(d, oldd); } return rh(a) + rh(b) + rh(c) + rh(d); } function encodeURIComponent2(str) { return encodeURIComponent(str) .replace(/\+/g, "%20").replace(/!/g, "%21").replace(/'/g, "%27") .replace(/\(/g, "%28").replace(/\)/g, "%29").replace(/\*/g, "%2A"); } function calSign(body, ts, randStr) { let b = encodeURIComponent2(body); b = b.split("").sort().join(""); b = btoa(unescape(encodeURIComponent(b))); const res = md5(b) + md5(ts + ":" + randStr); return md5(res).toUpperCase(); } function getCookie(name) { const m = document.cookie.split(';').map(s => s.trim()).find(s => s.startsWith(name + '=')); return m ? decodeURIComponent(m.slice(name.length + 1)) : ""; } function getLS(key) { try { return localStorage.getItem(key) || ""; } catch (e) { return ""; } } function parseLS(key) { try { return JSON.parse(getLS(key)); } catch (e) { return null; } } function randomString(len) { const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; let s = ""; for (let i = 0; i < len; i++) s += chars.charAt(Math.floor(Math.random() * chars.length)); return s; } class Api { constructor() { this.init(); } init() { this.authorization = getCookie("authorization"); this.account = this.extractAccount(); const router = parseLS("routerInfo") || []; const personal = router.find(r => r.modName === "personal"); this.host = (personal && personal.httpsUrl) || "https://personal-kd-njs.yun.139.com/hcy"; this.clientInfo = "||9|7.18.0|edge||" + (getLS("TY_DEVICE_N_ID") || "unknown") + "||windows 10||zh-CN|||ZWRnZQ==||"; this.appChannel = "10000034"; } extractAccount() { try { const b64 = this.authorization.replace(/^Basic\s+/i, ""); const parts = atob(b64).split(":"); if (parts.length >= 2) return parts[1]; } catch (e) { /* ignore */ } try { return atob(getLS("encryptAccount") || ""); } catch (e) { return ""; } } getCurrentFolderId() { const id = getLS("currentCatalogID"); return id && id !== "root" ? id : "/"; } async sendRequest(method, path, body) { const bodyStr = body ? JSON.stringify(body) : ""; const now = new Date(); const pad = n => String(n).padStart(2, "0"); const tsStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`; const rand = randomString(16); const sign = calSign(bodyStr, tsStr, rand); const resp = await fetch(this.host + path, { method, headers: { "Content-Type": "application/json;charset=UTF-8", "Authorization": this.authorization, "x-yun-api-version": "v1", "x-yun-client-info": this.clientInfo, "x-yun-app-channel": this.appChannel, "mcloud-sign": `${tsStr},${rand},${sign}`, }, body: bodyStr, credentials: "include", }); let data; try { data = await resp.json(); } catch (e) { throw new Error("响应不是 JSON: " + (await resp.text()).slice(0, 200)); } if (data.success !== true && data.code !== "0000") { throw new Error(`接口错误(${path}): ${data.code} ${data.message || data.resultMsg}`); } return data; } async getOnePageFileList(parentFileId, pageCursor) { const data = await this.sendRequest("POST", "/file/list", { pageInfo: { pageSize: 100, pageCursor: pageCursor || null }, orderBy: "updated_at", orderDirection: "DESC", parentFileId, imageThumbnailStyleList: ["Small", "Large"], }); const items = (data.data && data.data.items) || []; return { items, nextPageCursor: (data.data && data.data.nextPageCursor) || null }; } async getFileList(parentFileId) { const all = []; let cursor = null; do { const page = await this.getOnePageFileList(parentFileId, cursor); all.push(...page.items); cursor = page.nextPageCursor; if (cursor) await sleep(CONFIG.listPageDelay); } while (cursor); return all; } async mkdir(parentFileId, folderName) { try { const data = await this.sendRequest("POST", "/file/create", { fileRenameMode: "force_rename", type: "folder", name: folderName, parentFileId, description: "", commonAccountInfo: { account: this.account, accountType: 1 }, }); const fileId = data.data && (data.data.fileId || data.data.existFileId); return { fileId, folderName, success: !!fileId }; } catch (e) { return { fileId: null, folderName, success: false, error: e.message }; } } // 秒传:按 SHA256 hash 创建文件,exist/rapidUpload 为 true 即成功 async getFile(fileInfo, parentFileId) { try { const data = await this.sendRequest("POST", "/file/create", { fileRenameMode: "auto_rename", contentType: "application/oct-stream", type: "file", name: fileInfo.fileName, size: fileInfo.size, contentHashAlgorithm: "SHA256", contentHash: fileInfo.etag, partInfos: [{ parallelHashCtx: { partOffset: 0 }, partNumber: 1, partSize: fileInfo.size }], parentFileId, commonAccountInfo: { account: this.account, accountType: 1 }, }); const d = data.data || {}; if (d.exist === true || d.rapidUpload === true) { return [true, null, d.fileId || null]; } return [false, "云端无此文件,无法秒传", null]; } catch (e) { return [false, "请求失败: " + e.message, null]; } } } return { Api }; })(); /** 找到移动云盘文件列表 Vue 组件($data.dataList 含 contentID 的真实列表,优先当前目录) */ function y139FindPageVm() { let currentId = ''; try { currentId = localStorage.getItem('currentCatalogID') || ''; } catch (e) { /* ignore */ } const seen = new Set(); let best = null, bestScore = -1; const walk = (obj, depth) => { if (depth > 8 || !obj || seen.has(obj)) return; seen.add(obj); try { const dl = obj.$data && obj.$data.dataList; if (Array.isArray(dl)) { const real = dl.filter(i => i && i.contentID); let score = real.length; if (obj.$data && obj.$data.catalogID === currentId) score += 100000; if (score > bestScore) { bestScore = score; best = obj; } } } catch (e) { /* ignore */ } if (obj.$children) obj.$children.forEach(c => walk(c, depth + 1)); }; try { const container = document.querySelector(".document_main, .body_main"); if (container && container.__vue__) walk(container.__vue__, 0); if (!best) { for (const el of document.querySelectorAll("*")) { if (el.__vue__) { seen.clear(); walk(el.__vue__, 0); if (best) break; } } } } catch (e) { /* ignore */ } return best || null; } /** 移动云盘勾选读取:dataList 中 isSelected===true 的条目(Vue 内存态) */ function y139GetCheckedItems() { const vm = y139FindPageVm(); if (!vm) return { all: false, items: [], state: null }; const dl = (vm.$data && vm.$data.dataList) || []; const items = dl.filter(i => i && i.isSelected === true && i.contentID).map(i => ({ contentID: i.contentID, contentName: i.contentName, contentSize: i.contentSize, contentType: i.contentType, isSelected: true, })); let all = false; try { all = !!(vm.$data && vm.$data.isAllSelected); } catch (e) { /* ignore */ } if (!all && items.length && dl.length && items.length === dl.length) all = true; return { all, items, state: { currentDirId: y139GetCurrentFolderId() } }; } /** 移动云盘当前目录 ID(localStorage.currentCatalogID,根为 '/') */ function y139GetCurrentFolderId() { try { const id = localStorage.getItem('currentCatalogID'); return id && id !== 'root' ? id : '/'; } catch (e) { return '/'; } } /** 拉取移动云盘目录全部页(items 含 name/type/contentHash/fileId/size) */ async function y139ListDirAll(api, parentFileId) { const out = []; let cursor = null; for (;;) { const page = await api.getOnePageFileList(parentFileId, cursor); out.push(...page.items); cursor = page.nextPageCursor; if (!cursor) break; await sleep(CONFIG.listPageDelay); } return out; } /** 移动云盘导出收集:顶层文件→SHA256;文件夹→API 递归扫描 */ async function y139ExportCollect(api, items, files, warnMissing, statusEl, barEl, counter, prefix) { for (const item of items) { const name = String(item.name || ''); if (!name) { warnMissing.push('(未命名条目)'); counter.done++; continue; } const path = (prefix || '') + name; if (item.type === 'folder') { if (statusEl) setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info'); try { const sub = await y139ListDirAll(api, item.fileId); await y139ExportCollect(api, sub, files, warnMissing, statusEl, barEl, counter, path + '/'); } catch (e) { warnMissing.push(path + ':扫描文件夹失败 ' + e.message); } } else { const sha256 = normalizeSha256(item.contentHash); if (sha256) { files.push({ path, etag: sha256, size: Number(item.size) || 0 }); counter.done++; if (statusEl) setStatus(statusEl, `收集 ${counter.done}/${counter.total}:${path}`, 'info'); if (barEl) updateProgress(barEl, Math.round(counter.done / Math.max(1, counter.total) * 100), `收集 ${counter.done}/${counter.total}`); await sleep(CONFIG.md5Delay); } else { warnMissing.push(path + ':缺少 contentHash(SHA256)'); counter.done++; } } } } /** 移动云盘导出:勾选 → 顶层用当前目录 API 列表补齐哈希 → 文件夹递归 → JSON */ async function y139GenerateJson() { const sel = y139GetCheckedItems(); if (!sel.all && !sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; } const modal = openModal({ title: '生成秒传 JSON(移动云盘)', icon: ICONS.generate, width: 640, content(el) { el.innerHTML = `
准备中...
零下载取哈希:文件列表接口自带 contentHash(SHA256,64位hex)。
注意:移动云盘秒传使用 SHA256,本 JSON 的 etag 字段为 64 位 SHA256,仅供移动云盘自身导入;其他平台按 32 位 MD5 解析会跳过这些条目。
`; }, footer() { return ''; }, }); const statusEl = modal.contentEl.querySelector('#gy-ms-y139-gen-status'); const barEl = modal.contentEl.querySelector('.gy-ms-bar'); const files = []; const warnMissing = []; try { const api = new Y139.Api(); let topItems = []; if (sel.all) { setStatus(statusEl, '已全选当前目录,正在拉取全部文件列表...', 'info'); topItems = await y139ListDirAll(api, y139GetCurrentFolderId()); setStatus(statusEl, `当前目录共 ${topItems.length} 项,正在收集...`, 'info'); } else { // 顶层条目:用当前目录 API 列表按 contentID 补齐 type/contentHash(页面 dataList 无哈希) setStatus(statusEl, '正在获取当前目录文件信息(补齐 SHA256)...', 'info'); const curDirId = y139GetCurrentFolderId(); let apiMap = {}; try { const items = await y139ListDirAll(api, curDirId); apiMap = {}; items.forEach(i => { if (i && i.fileId) apiMap[i.fileId] = i; }); } catch (e) { warnMissing.push('获取当前目录列表失败:' + e.message); } topItems = sel.items.map(s => { const meta = apiMap[s.contentID]; if (meta) return meta; const isFolder = String(s.contentType) === '1'; return isFolder ? { name: s.contentName, type: 'folder', fileId: s.contentID, contentHash: '', size: Number(s.contentSize) || 0 } : { name: s.contentName, type: 'file', fileId: s.contentID, contentHash: '', size: Number(s.contentSize) || 0 }; }); setStatus(statusEl, `正在收集 ${topItems.length} 项...`, 'info'); } if (!topItems.length) { setStatus(statusEl, '当前目录没有可导出的内容。', 'err'); modal.footerEl.innerHTML = ``; return; } const counter = { done: 0, total: topItems.length }; await y139ExportCollect(api, topItems, files, warnMissing, statusEl, barEl, counter); if (!files.length) { setStatus(statusEl, '未获取到任何文件(可能勾选的全是空文件夹)。' + (warnMissing.length ? '\n' + warnMissing.slice(0, 5).join('\n') : ''), 'err'); modal.footerEl.innerHTML = ``; return; } const totalSize = files.reduce((s, f) => s + (Number(f.size) || 0), 0); const json = { scriptVersion: SCRIPT_VERSION, platform: '移动云盘', totalFilesCount: files.length, totalSize, formattedTotalSize: formatSize(totalSize), files: files.map(f => ({ path: f.path, etag: f.etag, gcid: '', size: Number(f.size) || 0 })), }; const jsonStr = JSON.stringify(json, null, 2); const exportName = CONFIG.defaultExportName + '_移动云盘.json'; modal.contentEl.innerHTML = `
已生成 ${files.length} 项秒传 JSON${warnMissing.length ? `;${warnMissing.length} 项未完整` : ''}
etag = 64 位 SHA256(列表接口 contentHash,零下载)。
移动云盘秒传使用 SHA256,本 JSON 仅供移动云盘自身导入(其他平台按 32 位 MD5 解析会跳过)。
${escapeHtml(jsonStr)}
`; modal.footerEl.innerHTML = ` `; modal.footerEl.querySelector('[data-act="copy"]').onclick = () => { try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); } catch (e) { showToast('复制失败:' + e.message, 'error'); } }; modal.footerEl.querySelector('[data-act="download"]').onclick = () => { try { const blob = new Blob([jsonStr], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = exportName; a.click(); URL.revokeObjectURL(a.href); showToast('JSON 已下载'); } catch (e) { showToast('下载失败:' + e.message, 'error'); } }; modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); mcLog('y139', `导出完成 ${files.length} 项`); } catch (e) { setStatus(statusEl, '导出失败:' + e.message, 'err'); modal.footerEl.innerHTML = ``; mcLog('y139', '导出失败: ' + e.message); } } /** 移动云盘导入:JSON(files 的 etag=64位SHA256)→ 建目录 → /file/create 秒传 */ async function y139ImportJson(rawJson, onProgress) { let obj; try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); } const rows = []; const skippedDetail = []; let skipped = 0; for (const f of (obj.files || [])) { const path = String(f.path || f.name || '').trim(); const sha256 = normalizeSha256(f.etag || f.sha256); const size = Number(f.size != null ? f.size : 0); if (!path) continue; if (!sha256) { skippedDetail.push(`${path}:无 64 位 SHA256(移动云盘秒传仅支持 SHA256;百度等导出的 32 位 MD5 无法在此导入)`); skipped++; continue; } rows.push({ path, sha256, size }); } if (!rows.length) throw new Error('JSON 中没有可导入的条目(需要 etag=64 位 SHA256)'); const api = new Y139.Api(); const rootFolder = api.getCurrentFolderId(); let success = 0, fail = 0; const failures = []; const total = rows.length; const dirCache = new Map(); for (let i = 0; i < rows.length; i++) { const row = rows[i]; onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `移动云盘秒传 ${i + 1}/${total}:${row.path}` }); try { const dirSegments = dirSegmentsFromPath(row.path); const fileName = basenameFromPath(row.path); let parent = rootFolder; let mkdirOk = true; for (const dirName of dirSegments) { const key = `${parent}::${dirName}`; if (dirCache.has(key)) { parent = dirCache.get(key); continue; } try { const dir = await api.mkdir(parent, dirName); if (!dir.fileId) throw new Error(dir.error || '创建目录失败'); dirCache.set(key, dir.fileId); parent = dir.fileId; await sleep(CONFIG.mkdirDelay); } catch (e) { mkdirOk = false; failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`); break; } } if (!mkdirOk) { fail++; continue; } const r = await api.getFile({ etag: row.sha256, fileName, size: row.size }, parent); if (r[0]) { success++; } else { fail++; failures.push(`${row.path}:${r[1] || '未命中秒传库存'}`); } } catch (e) { fail++; failures.push(`${row.path}:秒传失败(${e.message})`); } await sleep(CONFIG.transferDelay); } onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` }); mcLog('y139', '导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped); if (failures.length) mcLog('y139', '失败明细:' + failures.slice(0, 8).join(' | ')); return { total, success, fail, skipped, failures, skippedDetail }; } /** 移动云盘导入模态框 */ function showY139ImportModal() { const modal = openModal({ title: '保存秒传 JSON 到移动云盘', icon: ICONS.import, width: 640, content(el) { el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:etag=64 位 SHA256(移动云盘列表 contentHash)、path、size)。
移动云盘秒传使用 SHA256:仅本平台导出的 JSON(etag 为 64 位 hex)可秒传;百度/123 等导出的 32 位 MD5 条目会跳过。
path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`; }, footer(el) { el.innerHTML = ` `; }, }); const ta = modal.contentEl.querySelector('#gy-ms-y139-import-ta'); const statusEl = modal.contentEl.querySelector('#gy-ms-y139-import-status'); const barEl = modal.contentEl.querySelector('#gy-ms-y139-import-bar'); const detailEl = modal.contentEl.querySelector('#gy-ms-y139-import-detail'); const runBtn = modal.footerEl.querySelector('[data-act="run"]'); modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl); runBtn.onclick = async () => { const vr = validateImportJson(ta.value); if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; } runBtn.disabled = true; runBtn.textContent = '导入中...'; barEl.style.display = 'block'; detailEl.style.display = 'none'; let summary; try { summary = await y139ImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); }); } catch (e) { setStatus(statusEl, `导入失败:${e.message}`, 'err'); runBtn.disabled = false; runBtn.textContent = '开始导入'; return; } runBtn.disabled = false; runBtn.textContent = '开始导入'; const detailLines = []; if (summary.failures.length) detailLines.push(...summary.failures); if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(无 64 位 SHA256) =========='); detailLines.push(...summary.skippedDetail); } if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; } const warn = summary.fail > 0 || summary.skipped > 0; setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok'); }; } // ============================== 123云盘平台(v1.11:123pan.com,MD5 秒传,协议实测 2026) ============================== // 协议来源:123FastLink(Baoqing/Chaofan/lipkiat/JiangKaslana,MIT)移植——/b/api/file/list/new 自带 Etag(MD5);/b/api/file/upload_request 秒传(Reuse 命中) // 边界:列表自带 32hex MD5 零下载导出;导入走 upload_request(etag+size 命中即秒传) // JSON 说明:etag 存 32 位 MD5——123 导出的 JSON 可在百度/夸克/UC/小飞机等支持 md5 的平台导入 const P123 = (() => { class Api { constructor() { this.host = 'https://' + location.host; try { this.authToken = localStorage['authorToken'] || ''; } catch (e) { this.authToken = ''; } try { this.loginUuid = localStorage['LoginUuid'] || ''; } catch (e) { this.loginUuid = ''; } } buildURL(path, queryParams) { const qs = new URLSearchParams(queryParams || {}).toString(); return `${this.host}${path}?${qs}`; } async sendRequest(method, path, queryParams, body) { const headers = { 'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer ' + this.authToken, 'platform': 'web', 'App-Version': '3', 'LoginUuid': this.loginUuid, 'Origin': this.host, 'Referer': document.location.href, }; const response = await fetch(this.buildURL(path, queryParams), { method, headers, body, credentials: 'include' }); let data; try { data = await response.json(); } catch (e) { throw new Error('123接口响应不是 JSON'); } if (data.code !== 0) throw new Error(data.message || ('123接口错误 code=' + data.code)); return data; } async getOnePageFileList(parentFileId, page) { const urlParams = { driveId: '0', limit: '100', next: '0', orderBy: 'file_name', orderDirection: 'asc', parentFileId: String(parentFileId || 0), trashed: 'false', SearchData: '', Page: String(page || 1), OnlyLookAbnormalFile: '0', event: 'homeListFile', operateType: '1', inDirectSpace: 'false', }; const data = await this.sendRequest('GET', '/b/api/file/list/new', urlParams); return { items: (data.data && data.data.InfoList) || [], total: (data.data && data.data.Total) || 0 }; } /** 秒传:etag+size 命中 Reuse 即成功 */ async fastUpload(fileInfo) { try { const response = await this.sendRequest('POST', '/b/api/file/upload_request', {}, JSON.stringify({ ...fileInfo, RequestSource: null })); const d = response.data || {}; if (!d.Reuse) return [false, '云端无此文件,无法秒传', null]; return [true, null, (d.Info && d.Info.FileId) || null]; } catch (error) { return [false, '请求失败: ' + error.message, null]; } } async mkdir(parentFileId, folderName) { 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, })); const folderFileId = response.data && response.data.Info && response.data.Info.FileId; return { folderFileId, folderName, success: !!folderFileId }; } catch (e) { return { folderFileId: null, folderName, success: false, error: e.message }; } } /** 当前父目录 ID:优先表格 dataSource 的 ParentFileId(yun.123pan.cn 新站无 sessionStorage.filePath),兜底 URL/sessionStorage 旧方案 */ getParentFileId() { // 1. 新站:表格 dataSource 行自带 ParentFileId(当前目录 = 任一行 ParentFileId;根目录为 0) try { const fiber = p123FindTableFiber(); if (fiber) { const ds = fiber.memoizedProps && fiber.memoizedProps.dataSource; if (Array.isArray(ds) && ds.length) { const pid = ds[0] && ds[0].ParentFileId; if (pid != null) return String(pid); } } } catch (e) { /* ignore */ } // 2. URL homeFilePath 参数(旧方案兜底) try { const urlHomeFilePath = new URLSearchParams(window.location.search).get('homeFilePath'); if (urlHomeFilePath) { const arr = urlHomeFilePath.split(',').filter(Boolean); if (arr.length) return String(arr[arr.length - 1]); } } catch (e) { /* ignore */ } // 3. sessionStorage.filePath(更旧方案兜底) try { const rawFilePath = sessionStorage.getItem('filePath') || sessionStorage['filePath']; if (rawFilePath) { const parsed = typeof rawFilePath === 'string' ? JSON.parse(rawFilePath) : rawFilePath; if (parsed && Array.isArray(parsed.homeFilePath) && parsed.homeFilePath.length) { return String(parsed.homeFilePath[parsed.homeFilePath.length - 1]); } } } catch (e) { /* ignore */ } return '0'; } } return { Api }; })(); /** 定位 123 云盘 antd 表格 fiber(memoizedProps 含 dataSource/rowSelection/columns) */ function p123FindTableFiber() { const isTableFiber = (node) => { const p = node && node.memoizedProps; return !!(p && Array.isArray(p.dataSource) && p.rowSelection && Array.isArray(p.columns)); }; const findInElement = (el) => { const fiberKey = Object.keys(el).find(k => k.startsWith('__reactFiber$')); if (!fiberKey) return null; let node = el[fiberKey] && el[fiberKey].return; for (let i = 0; i < 12 && node; i++, node = node.return) { if (isTableFiber(node)) return node; } return null; }; // 1. 语义化容器快速定位 const containerSelectors = [ '.ant-table-wrapper', '.file-table-list-wrapper', '[class*="table-list-wrapper"]', '[class*="table-module"]', '[class*="ant-table"]', ]; for (const selector of containerSelectors) { for (const el of document.querySelectorAll(selector)) { const fiber = findInElement(el); if (fiber) return fiber; } } // 2. 兜底:从 React 根容器沿 fiber 树遍历(与 class 名完全解耦) const roots = []; const pushRoot = (el) => { if (!el) return; const containerKey = Object.keys(el).find(k => k.startsWith('__reactContainer$')); if (containerKey && el[containerKey]) roots.push(el[containerKey]); }; for (const selector of ['#app', '#root', '#__next', '#main', '#content']) pushRoot(document.querySelector(selector)); if (!roots.length) { for (const el of document.querySelectorAll('*')) { pushRoot(el); if (roots.length >= 3) break; } } for (const root of roots) { const stack = [root]; let visited = 0; while (stack.length && visited < 100000) { const node = stack.pop(); visited++; if (isTableFiber(node)) return node; if (node.child) stack.push(node.child); if (node.sibling) stack.push(node.sibling); } } return null; } /** 123 勾选读取:表格内存态 dataSource + selectedRowKeys(antd rowSelection,含虚拟列表未渲染行) */ function p123GetCheckedItems() { const fiber = p123FindTableFiber(); if (!fiber) return { all: false, items: [] }; const p = fiber.memoizedProps; const dataSource = Array.isArray(p.dataSource) ? p.dataSource : []; const selectedRowKeys = (p.rowSelection && Array.isArray(p.rowSelection.selectedRowKeys)) ? p.rowSelection.selectedRowKeys : []; const rowKey = p.rowKey; const keyOf = (item) => { if (typeof rowKey === 'string') return item[rowKey]; if (typeof rowKey === 'function') return rowKey(item); return item.FileId != null ? item.FileId : (item.keys != null ? item.keys : item.id); }; let items = []; if (selectedRowKeys.length && dataSource.length) { const keySet = new Set(selectedRowKeys); items = dataSource.filter(it => keySet.has(keyOf(it))); } else { items = dataSource.filter(it => it.checked); } items = items.filter(it => it && it.FileId != null).map(it => ({ FileId: it.FileId, FileName: it.FileName, Size: it.Size, Type: it.Type, Etag: it.Etag, })); return { all: false, items }; } /** 123 当前父目录 ID(同步,sessionStorage/URL) */ function p123GetCurrentParentId() { try { return new P123.Api().getParentFileId(); } catch (e) { return '0'; } } /** 拉取 123 目录全部页(InfoList 含 FileName/FileId/Type/Size/Etag) */ async function p123ListDirAll(api, parentFileId) { const out = []; const first = await api.getOnePageFileList(parentFileId, 1); out.push(...first.items); const total = first.total; if (total > 100) { const times = Math.ceil(total / 100); for (let page = 2; page <= times; page++) { const p = await api.getOnePageFileList(parentFileId, page); out.push(...p.items); await sleep(CONFIG.listPageDelay); } } return out; } /** 123 导出收集:顶层文件→Etag(MD5);文件夹→API 递归扫描 */ async function p123ExportCollect(api, items, files, warnMissing, statusEl, barEl, counter, prefix) { for (const item of items) { const name = String(item.FileName || item.name || ''); if (!name) { warnMissing.push('(未命名条目)'); counter.done++; continue; } const path = (prefix || '') + name; if (Number(item.Type) === 1) { if (statusEl) setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info'); try { const sub = await p123ListDirAll(api, item.FileId); await p123ExportCollect(api, sub, files, warnMissing, statusEl, barEl, counter, path + '/'); } catch (e) { warnMissing.push(path + ':扫描文件夹失败 ' + e.message); } } else { const md5 = normalizeMd5(item.Etag); if (md5) { files.push({ path, etag: md5, size: Number(item.Size) || 0 }); counter.done++; if (statusEl) setStatus(statusEl, `收集 ${counter.done}/${counter.total}:${path}`, 'info'); if (barEl) updateProgress(barEl, Math.round(counter.done / Math.max(1, counter.total) * 100), `收集 ${counter.done}/${counter.total}`); await sleep(CONFIG.md5Delay); } else { warnMissing.push(path + ':缺少 Etag(MD5)'); counter.done++; } } } } /** 123 导出:勾选(页面 dataSource 自带 Etag)→ 文件夹递归 → JSON */ async function p123GenerateJson() { const sel = p123GetCheckedItems(); if (!sel.all && !sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; } const modal = openModal({ title: '生成秒传 JSON(123云盘)', icon: ICONS.generate, width: 640, content(el) { el.innerHTML = `
准备中...
零下载取哈希:文件列表自带 Etag(MD5,32位hex)。
跨盘秒传:本 JSON 的 etag 为 32 位 MD5,可在百度/夸克/UC/小飞机等支持 md5 的平台导入。
`; }, footer() { return ''; }, }); const statusEl = modal.contentEl.querySelector('#gy-ms-p123-gen-status'); const barEl = modal.contentEl.querySelector('.gy-ms-bar'); const files = []; const warnMissing = []; try { const api = new P123.Api(); setStatus(statusEl, `正在收集 ${sel.items.length} 项...`, 'info'); const counter = { done: 0, total: sel.items.length }; await p123ExportCollect(api, sel.items, files, warnMissing, statusEl, barEl, counter); if (!files.length) { setStatus(statusEl, '未获取到任何文件(可能勾选的全是空文件夹)。' + (warnMissing.length ? '\n' + warnMissing.slice(0, 5).join('\n') : ''), 'err'); modal.footerEl.innerHTML = ``; return; } const totalSize = files.reduce((s, f) => s + (Number(f.size) || 0), 0); const json = { scriptVersion: SCRIPT_VERSION, platform: '123云盘', totalFilesCount: files.length, totalSize, formattedTotalSize: formatSize(totalSize), files: files.map(f => ({ path: f.path, etag: f.etag, gcid: '', size: Number(f.size) || 0 })), }; const jsonStr = JSON.stringify(json, null, 2); const exportName = CONFIG.defaultExportName + '_123.json'; modal.contentEl.innerHTML = `
已生成 ${files.length} 项秒传 JSON${warnMissing.length ? `;${warnMissing.length} 项未完整` : ''}
etag = 32 位 MD5(列表 Etag,零下载)。
123 导出的 JSON 可在百度/夸克/UC/小飞机等支持 md5 的平台导入。
${escapeHtml(jsonStr)}
`; modal.footerEl.innerHTML = ` `; modal.footerEl.querySelector('[data-act="copy"]').onclick = () => { try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); } catch (e) { showToast('复制失败:' + e.message, 'error'); } }; modal.footerEl.querySelector('[data-act="download"]').onclick = () => { try { const blob = new Blob([jsonStr], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = exportName; a.click(); URL.revokeObjectURL(a.href); showToast('JSON 已下载'); } catch (e) { showToast('下载失败:' + e.message, 'error'); } }; modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); mcLog('p123', `导出完成 ${files.length} 项`); } catch (e) { setStatus(statusEl, '导出失败:' + e.message, 'err'); modal.footerEl.innerHTML = ``; mcLog('p123', '导出失败: ' + e.message); } } /** 123 导入:JSON(files 的 etag/md5=32位MD5)→ 建目录 → upload_request 秒传 */ async function p123ImportJson(rawJson, onProgress) { let obj; try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); } const rows = []; const skippedDetail = []; let skipped = 0; for (const f of (obj.files || [])) { const path = String(f.path || f.name || '').trim(); const md5 = normalizeMd5(f.etag || f.md5); const size = Number(f.size != null ? f.size : 0); if (!path) continue; if (!md5) { skippedDetail.push(`${path}:无 32 位 MD5(123 秒传需要 MD5;阿里导出的仅含 sha1 无法导入)`); skipped++; continue; } rows.push({ path, md5, size }); } if (!rows.length) throw new Error('JSON 中没有可导入的条目(需要 etag/md5=32 位 MD5)'); const api = new P123.Api(); const rootFolder = api.getParentFileId(); let success = 0, fail = 0; const failures = []; const total = rows.length; const dirCache = new Map(); for (let i = 0; i < rows.length; i++) { const row = rows[i]; onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `123秒传 ${i + 1}/${total}:${row.path}` }); try { const dirSegments = dirSegmentsFromPath(row.path); const fileName = basenameFromPath(row.path); let parent = rootFolder; let mkdirOk = true; for (const dirName of dirSegments) { const key = `${parent}::${dirName}`; if (dirCache.has(key)) { parent = dirCache.get(key); continue; } try { const dir = await api.mkdir(parent, dirName); if (!dir.success || !dir.folderFileId) throw new Error(dir.error || '创建目录失败'); dirCache.set(key, dir.folderFileId); parent = dir.folderFileId; await sleep(CONFIG.mkdirDelay); } catch (e) { mkdirOk = false; failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`); break; } } if (!mkdirOk) { fail++; continue; } const r = await api.fastUpload({ driveId: 0, etag: row.md5, fileName, parentFileId: parent, size: row.size, type: 0, duplicate: 1, }); if (r[0]) { success++; } else { fail++; failures.push(`${row.path}:${r[1] || '未命中秒传库存'}`); } } catch (e) { fail++; failures.push(`${row.path}:秒传失败(${e.message})`); } await sleep(CONFIG.transferDelay); } onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` }); mcLog('p123', '导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped); if (failures.length) mcLog('p123', '失败明细:' + failures.slice(0, 8).join(' | ')); return { total, success, fail, skipped, failures, skippedDetail }; } /** 123 导入模态框 */ function showP123ImportModal() { const modal = openModal({ title: '保存秒传 JSON 到 123云盘', icon: ICONS.import, width: 640, content(el) { el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:etag/md5=32 位 MD5、path、size)。
123 秒传使用 MD5:本平台导出的 JSON(etag=32 位 MD5)、以及百度/夸克/UC/小飞机/城通等导出的含 md5 的 JSON 均可导入;仅含 sha1 的条目(如阿里导出)会跳过。
path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`; }, footer(el) { el.innerHTML = ` `; }, }); const ta = modal.contentEl.querySelector('#gy-ms-p123-import-ta'); const statusEl = modal.contentEl.querySelector('#gy-ms-p123-import-status'); const barEl = modal.contentEl.querySelector('#gy-ms-p123-import-bar'); const detailEl = modal.contentEl.querySelector('#gy-ms-p123-import-detail'); const runBtn = modal.footerEl.querySelector('[data-act="run"]'); modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove(); attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl); runBtn.onclick = async () => { const vr = validateImportJson(ta.value); if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; } runBtn.disabled = true; runBtn.textContent = '导入中...'; barEl.style.display = 'block'; detailEl.style.display = 'none'; let summary; try { summary = await p123ImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); }); } catch (e) { setStatus(statusEl, `导入失败:${e.message}`, 'err'); runBtn.disabled = false; runBtn.textContent = '开始导入'; return; } runBtn.disabled = false; runBtn.textContent = '开始导入'; const detailLines = []; if (summary.failures.length) detailLines.push(...summary.failures); if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(无 32 位 MD5) =========='); detailLines.push(...summary.skippedDetail); } if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; } const warn = summary.fail > 0 || summary.skipped > 0; setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok'); }; } // ============================== 初始化 ============================== function init() { // openapi 授权页:OAuth 自动处理(不依赖 PLATFORM) try { if (typeof BAIDU !== 'undefined') BAIDU.oauthPageHook(); } catch (e) { /* ignore */ } if (!PLATFORM) return; // 非目标平台不运行 loadSettings(); insertStyle(); // 115:新版注入页面钩子拦截 webapi 列表响应(勾选匹配数据源);旧版显示提示切新版 if (isP115()) { if (p115PlatformGate()) p115InjectPageHook(); } // 天翼:安装家庭接口请求头捕获钩子(Browser-Id/AccessToken 持久化,家庭云签名用) if (isTianyi()) { try { tianyiInstallFamilyHook(); } catch (e) { /* ignore */ } } // 勾选状态追踪(用户脚本层,与 pageHook 解耦) document.addEventListener('change', function (e) { try { if (isGuangya()) { const input = e.target; if (!input || input.tagName !== 'INPUT' || !/swangpan-checkbox__input/.test(input.className || '')) return; const row = input.closest('.swangpan-file-list-table__row'); const checked = !!(input.checked || input.getAttribute('aria-checked') === 'true'); const st = guangyaGetPageState(); if (!st) return; if (!row) { st.selectAll = checked; st.selectedNames = {}; } else { const nameEl = row.querySelector('.swangpan-file-list-table__titleCell'); const name = nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : ''; if (name) { st.selectedNames = st.selectedNames || {}; st.selectedNames[name] = checked ? 1 : 0; } } st.selectionVersion = (st.selectionVersion || 0) + 1; } else if (isXunlei()) { // 迅雷勾选记录到本地 state(DOM 兜底由 xunleiGetCheckedItems 直接读 checkbox) const input = e.target; const st = xunleiGetPageState(); if (input && input.tagName === 'INPUT' && input.type === 'checkbox') { st.selectionVersion = (st.selectionVersion || 0) + 1; } } } catch (err) { /* ignore */ } }, true); // 幂等挂载按钮(蓝奏云走独立 iframe 注入流程,其余平台走通用工具栏注入) if (isLanzou()) { lanzouSetupIframe(); setTimeout(lanzouSetupIframe, 1200); setTimeout(lanzouSetupIframe, 3000); } else { const tryAdd = () => { if (document.querySelector('button img[alt="upload"], [class*="file-list-table"], .ant-dropdown-trigger, [class*="SourceListItem"], [class*="file-list-toolbar"], [class*="wp-s-pan-table"], [class*="node-card"], .adrive-create-button, [class*="actions--"], [class*="actions-"], button.upload-btn, button.btn-file, .file-list, [class*="mod-action-wrap"], [class*="layout-toolbar"], .title-bar, [class*="title-bar"], iframe[name="mainframe"], iframe[src*="item=files"], iframe[src*="mydisk.php"], [class*="file-name-responsive"], .ct-toolbar, .ct-row, .ct-list, .c-file-item, [class*="file-head"], [class*="operation-container"], .el-table, .document_main, .document_top_upload_button, .home-operator-button-group')) { addToolbarButton(); return; } setTimeout(tryAdd, 600); }; let obsTimer = null; const obs = new MutationObserver(() => { if (obsTimer) return; obsTimer = setTimeout(() => { obsTimer = null; addToolbarButton(); }, 500); }); try { obs.observe(document.body || document.documentElement, { childList: true, subtree: true }); } catch (e) { /* ignore */ } setTimeout(tryAdd, 800); setTimeout(tryAdd, 2500); setTimeout(tryAdd, 5000); } // 暴露调试对象 try { const w = (typeof unsafeWindow !== 'undefined' && unsafeWindow) || window; w.__GYMS = { CONFIG, generateJson, showImportModal, getCheckedItems, getLastParentId, getPageState: isGuangya() ? guangyaGetPageState : (isBaidu() ? (() => ({})) : xunleiGetPageState), SCRIPT_VERSION, PLATFORM, }; if (isXunlei()) { w.__GYMS.xunleiGetCheckedItems = xunleiGetCheckedItems; w.__GYMS.xunleiGetFileList = xunleiGetFileList; } if (isBaidu()) { w.__GYMS.BAIDU = BAIDU; w.__GYMS.baiduGenerateJson = baiduGenerateJson; w.__GYMS.baiduImportJson = baiduImportJson; w.__GYMS.getBdstoken = () => BAIDU.getBdstoken(); } if (isAliyun()) { w.__GYMS.ALIYUN = ALIYUN; w.__GYMS.aliyunGenerateJson = aliyunGenerateJson; w.__GYMS.aliyunImportJson = aliyunImportJson; w.__GYMS.aliyunWalkFolder = aliyunWalkFolder; } if (isQuark()) { w.__GYMS.QUARK = QUARK; w.__GYMS.quarkGenerateJson = quarkGenerateJson; w.__GYMS.quarkImportJson = quarkImportJson; w.__GYMS.quarkWalkFolder = quarkWalkFolder; } if (isUc()) { w.__GYMS.UC = UC; w.__GYMS.ucGenerateJson = ucGenerateJson; w.__GYMS.ucImportJson = ucImportJson; w.__GYMS.ucWalkFolder = ucWalkFolder; } if (isWeiyun()) { w.__GYMS.WEIYUN = WEIYUN; w.__GYMS.weiyunGenerateJson = weiyunGenerateJson; w.__GYMS.weiyunImportJson = weiyunImportJson; w.__GYMS.weiyunWalkFolder = weiyunWalkFolder; } if (isFeejii()) { w.__GYMS.FEEJII = FEEJII; w.__GYMS.feejiiGenerateJson = feejiiGenerateJson; w.__GYMS.feejiiImportJson = feejiiImportJson; w.__GYMS.feejiiWalkFolder = feejiiWalkFolder; w.__GYMS.showFeejiiImportModal = showFeejiiImportModal; } if (isIlanzou()) { w.__GYMS.ILANZOU = ILANZOU; w.__GYMS.ilanzouGenerateJson = ilanzouGenerateJson; w.__GYMS.ilanzouImportJson = ilanzouImportJson; w.__GYMS.ilanzouWalkFolder = ilanzouWalkFolder; w.__GYMS.showIlanzouImportModal = showIlanzouImportModal; } if (isLanzou()) { w.__GYMS.LANZOU = LANZOU; w.__GYMS.lanzouGenerateJson = lanzouGenerateJson; w.__GYMS.lanzouRunExport = lanzouRunExport; w.__GYMS.parseLanzouSizeStr = parseLanzouSizeStr; w.__GYMS.lanzouSetupIframe = lanzouSetupIframe; w.__GYMS.lanzouGetCheckedRows = lanzouGetCheckedRows; w.__GYMS.lanzouInjectCheckboxes = lanzouInjectCheckboxes; } if (isP115()) { w.__GYMS.ONE15 = ONE15; w.__GYMS.P115_CRYPTO = P115_CRYPTO; w.__GYMS.p115GenerateJson = p115GenerateJson; w.__GYMS.showP115ImportModal = showP115ImportModal; w.__GYMS.p115ImportJson = p115ImportJson; w.__GYMS.p115GetCheckedItems = p115GetCheckedItems; w.__GYMS.p115GetLastCid = p115GetLastCid; } if (isCtfile()) { w.__GYMS.CTFILE = CTFILE; w.__GYMS.ctfileGenerateJson = ctfileGenerateJson; w.__GYMS.ctfileImportJson = ctfileImportJson; w.__GYMS.ctfileGetCheckedItems = ctfileGetCheckedItems; w.__GYMS.showCtfileImportModal = showCtfileImportModal; } if (isTianyi()) { w.__GYMS.TIANYI = TIANYI; w.__GYMS.tianyiGenerateJson = tianyiGenerateJson; w.__GYMS.tianyiImportJson = tianyiImportJson; w.__GYMS.tianyiGetCheckedItems = tianyiGetCheckedItems; w.__GYMS.showTianyiImportModal = showTianyiImportModal; } if (isWopan()) { w.__GYMS.WOPAN = WOPAN; w.__GYMS.wopanGenerateJson = wopanGenerateJson; w.__GYMS.wopanGetCheckedItems = wopanGetCheckedItems; w.__GYMS.wopanFindPageVm = wopanFindPageVm; } if (isY139()) { w.__GYMS.Y139 = Y139; w.__GYMS.y139GenerateJson = y139GenerateJson; w.__GYMS.y139GetCheckedItems = y139GetCheckedItems; w.__GYMS.y139FindPageVm = y139FindPageVm; w.__GYMS.y139ImportJson = y139ImportJson; } if (isP123()) { w.__GYMS.P123 = P123; w.__GYMS.p123GenerateJson = p123GenerateJson; w.__GYMS.p123GetCheckedItems = p123GetCheckedItems; w.__GYMS.p123FindTableFiber = p123FindTableFiber; w.__GYMS.p123ImportJson = p123ImportJson; } w.__GYMS.pdsProxyFetchProof = pdsProxyFetchProof; // 阿里导入夸克/UC 中转取 proof 块(调试/复用) w.__GYMS.pdsCookieAutoFetch = pdsCookieAutoFetch; // 夸克/UC 一键取 Cookie 回填(调试/复用) w.__GYMS.showAliyunImportModal = showAliyunImportModal; // 调试/复用 } catch (e) { /* ignore */ } // 夸克/UC 页:检测"阿里导入一键获取 Cookie"通道(在目标站取登录态后回填) try { setTimeout(pdsCookieAutoFetch, 600); } catch (e) { /* ignore */ } // 首启云令牌授权提示(百度网盘页,未选择过才弹) if (isBaidu() && BAIDU.needsConsentPrompt()) { try { setTimeout(() => showCloudConsentModal(), 1200); } catch (e) { /* ignore */ } } } // 尽早注入页面拦截脚本(仅光鸭需要) if (isGuangya()) { if (document.documentElement) { guangyaInjectPageHook(); } else { document.addEventListener('DOMContentLoaded', guangyaInjectPageHook, { once: true }); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init, { once: true }); } else { init(); } })();