// ==UserScript== // @name 夸克网盘网页端管家 // @namespace https://scriptcat.org/users/pwhscr // @version 1.0.0 // @description 给夸克网盘网页端加一批实用能力:文件批量勾选、批量下载(走官方网页端下载通道)、文件清单导出为 CSV、自动清理「请打开客户端」类引导弹窗。 // @author pwhscr // @license MIT // @homepageURL https://userscripts-toolkit.app.workbuddy.host/ // @supportURL https://userscripts-toolkit.app.workbuddy.host/ // @match https://pan.quark.cn/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @run-at document-idle // @noframes // ==/UserScript== /* * 边界说明(重要) * 本脚本只做「操作自动化」——帮你点官方网页端本来就有的下载按钮、批量选中、导出清单。 * 它不做直链提取,也不绕开服务端的下载限速,原因见文件末尾注释。 */ (function () { 'use strict'; const BTN_TEXT_RE = /^(下载|下载文件|保存到本地|Download)$/i; const MORE_RE = /^(更多|\.\.\.|···|更多操作)$/i; const CLIENT_POPUP_RE = /客户端|夸克APP|扫码登录|下载APP/; // ---------- 状态 ---------- const state = { selection: new Map(), // fileName -> {el, size, time} busy: false, }; // ---------- 工具 ---------- function visible(el) { if (!el || el.nodeType !== 1) return false; const r = el.getBoundingClientRect(); return r.width > 4 && r.height > 4; } function clickableText(el) { return (el.getAttribute('title') || el.getAttribute('aria-label') || el.textContent || '') .replace(/\s+/g, '') .trim(); } /** 从「下载」按钮往上找它所属的文件行 */ function rowOf(el) { let cur = el; for (let i = 0; i < 8 && cur && cur.parentElement; i++) { cur = cur.parentElement; const txt = (cur.innerText || '').trim(); // 行容器应该有一段不太长的文本,并且含文件大小/日期的痕迹 if (txt.length > 0 && txt.length < 400 && /[\d.]+\s*(B|KB|MB|GB|TB)/i.test(txt)) { return cur; } } return el.parentElement; } /** 提取文件名:优先行内的 title / 单行截断文本 */ function fileNameOf(row) { if (!row) return ''; const cand = row.querySelector('[title]'); if (cand) { const t = (cand.getAttribute('title') || '').trim(); if (t && !/^(下载|更多|分享|重命名|删除)$/.test(t)) return t; } const firstLine = (row.innerText || '') .split('\n') .map((s) => s.trim()) .filter(Boolean)[0]; return firstLine || '(未知文件)'; } function sizeOf(row) { const m = (row.innerText || '').match(/[\d.]+\s*(?:B|KB|MB|GB|TB)/i); return m ? m[0] : ''; } function timeOf(row) { const m = (row.innerText || '').match(/\d{4}[-/]\d{1,2}[-/]\d{1,2}(?:\s+\d{1,2}:\d{2})?/); return m ? m[0] : ''; } // ---------- 找官方下载入口 ---------- function findDownloadButton(row) { const btns = row.querySelectorAll('button, a, div[role="button"], span[role="button"], [class*="btn"]'); for (const b of btns) { if (BTN_TEXT_RE.test(clickableText(b)) && visible(b)) return b; } return null; } function findMoreButton(row) { const btns = row.querySelectorAll('button, a, div[role="button"], span[role="button"]'); for (const b of btns) { if (MORE_RE.test(clickableText(b)) && visible(b)) return b; } return null; } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } /** 从文件行触发一次官方下载 */ async function triggerDownload(row) { let btn = findDownloadButton(row); if (!btn) { // 下载藏在「更多」菜单里 const more = findMoreButton(row); if (!more) return false; more.click(); await sleep(420); const items = document.querySelectorAll( 'li, div[role="menuitem"], [class*="menu"] [class*="item"], [class*="dropdown"] [class*="item"]' ); for (const it of items) { if (BTN_TEXT_RE.test(clickableText(it)) && visible(it)) { it.click(); await sleep(260); closeClientPopup(); return true; } } document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); return false; } btn.click(); await sleep(260); closeClientPopup(); return true; } /** 关掉「请打开客户端」引导弹窗(不改变下载通道,只是别挡路) */ function closeClientPopup() { const nodes = document.querySelectorAll('[class*="modal"], [class*="dialog"], [class*="popup"], [role="dialog"]'); for (const d of nodes) { if (!visible(d)) continue; const txt = d.innerText || ''; if (!CLIENT_POPUP_RE.test(txt)) continue; const btns = d.querySelectorAll('button, [class*="close"], [class*="cancel"], span[role="button"]'); for (const b of btns) { const t = clickableText(b); if (/取消|关闭|以后再说|暂不|继续使用网页|不再提示/.test(t)) { b.click(); return true; } } const x = d.querySelector('[class*="close"]'); if (x) { x.click(); return true; } } return false; } // ---------- 勾选增强 ---------- /** 收集当前可见的文件行 */ function collectRows() { const rows = new Set(); const all = document.querySelectorAll('button, a, div[role="button"], span[role="button"], [class*="btn"]'); for (const b of all) { if (!visible(b) || !BTN_TEXT_RE.test(clickableText(b))) continue; const row = rowOf(b); if (row) rows.add(row); } // 兜底:有些列表行本身就带 class 特征 if (!rows.size) { document .querySelectorAll('[class*="file-item"], [class*="fileItem"], [class*="list-item"]') .forEach((r) => { if (visible(r)) rows.add(r); }); } return [...rows]; } function injectCheckboxes() { const rows = collectRows(); let added = 0; rows.forEach((row) => { if (row.querySelector('.qk-toolkit-pick')) return; const name = fileNameOf(row); const box = document.createElement('span'); box.className = 'qk-toolkit-pick'; box.title = '加入批量下载清单'; box.textContent = '+'; box.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); if (state.selection.has(name)) { state.selection.delete(name); box.textContent = '+'; box.classList.remove('on'); } else { state.selection.set(name, { el: row, size: sizeOf(row), time: timeOf(row) }); box.textContent = 'v'; box.classList.add('on'); } paintPanel(); }); const cs = getComputedStyle(row); if (cs.position === 'static') row.style.position = 'relative'; row.appendChild(box); added++; }); return added; } // ---------- 清单导出 ---------- function exportCSV() { const rows = collectRows(); const list = rows .map((row) => ({ name: fileNameOf(row), size: sizeOf(row), time: timeOf(row), })) .filter((x) => x.name && x.name !== '(未知文件)'); if (!list.length) { toast('没找到文件行,先在网盘里打开一个文件夹'); return; } const head = '文件名,大小,修改时间\n'; const body = list .map((x) => [x.name, x.size, x.time].map((v) => '"' + String(v).replace(/"/g, '""') + '"').join(',')) .join('\n'); const blob = new Blob(['\ufeff' + head + body], { type: 'text/csv;charset=utf-8' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = '夸克文件清单_' + new Date().toISOString().slice(0, 10) + '.csv'; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 4000); toast('已导出 ' + list.length + ' 条到 CSV'); } // ---------- 批量下载 ---------- async function batchDownload() { if (state.busy) return; const items = [...state.selection.values()].filter((v) => v.el && v.el.isConnected); if (!items.length) { toast('先用行首的 + 勾选几个文件'); return; } state.busy = true; paintPanel(); let ok = 0; let fail = 0; for (let i = 0; i < items.length; i++) { const it = items[i]; paintPanel('下载中 ' + (i + 1) + '/' + items.length); try { const done = await triggerDownload(it.el); done ? ok++ : fail++; } catch (e) { fail++; } // 随机间隔,别让浏览器把它当成脚本刷量 await sleep(1200 + Math.random() * 1300); } state.busy = false; // 已触发的从清单里移除 for (const [k, v] of [...state.selection.entries()]) { if (v.el && v.el.isConnected) state.selection.delete(k); } document.querySelectorAll('.qk-toolkit-pick.on').forEach((b) => { b.textContent = '+'; b.classList.remove('on'); }); paintPanel(); toast('批量下载完成:成功 ' + ok + ',跳过 ' + fail); } // ---------- 面板 ---------- let panel = null; function buildPanel() { if (panel || !document.body) return; const css = document.createElement('style'); css.textContent = ` .qk-toolkit-pick{position:absolute;left:6px;top:50%;transform:translateY(-50%);width:18px;height:18px; display:flex;align-items:center;justify-content:center;border-radius:5px;cursor:pointer;z-index:20; background:rgba(0,0,0,.05);border:1px solid rgba(0,0,0,.16);color:#8a8f99;font:12px/1 sans-serif;user-select:none} .qk-toolkit-pick:hover{background:rgba(0,0,0,.1)} .qk-toolkit-pick.on{background:#3f7dff;border-color:#3f7dff;color:#fff} #qk-toolkit-panel{position:fixed;right:16px;bottom:16px;z-index:2147483000;width:216px;padding:12px; border-radius:12px;background:#fff;border:1px solid rgba(0,0,0,.08);box-shadow:0 8px 28px rgba(0,0,0,.14); font:12px/1.6 -apple-system,"Segoe UI","Microsoft YaHei",sans-serif;color:#1d2129;user-select:none} #qk-toolkit-panel .qk-t{font-weight:700;font-size:12px;margin-bottom:8px;display:flex; align-items:center;justify-content:space-between} #qk-toolkit-panel .qk-n{color:#3f7dff;font-weight:700} #qk-toolkit-panel button{width:100%;margin-top:6px;padding:7px 0;border-radius:8px;cursor:pointer; border:1px solid rgba(0,0,0,.1);background:#f6f7f9;color:#1d2129;font-size:12px} #qk-toolkit-panel button:hover{background:#eceef2} #qk-toolkit-panel button.qk-main{background:#3f7dff;border-color:#3f7dff;color:#fff} #qk-toolkit-panel button.qk-main:hover{filter:brightness(1.06)} #qk-toolkit-panel button[disabled]{opacity:.5;cursor:not-allowed} #qk-toolkit-panel .qk-hint{margin-top:8px;font-size:11px;color:#8a8f99;line-height:1.5} #qk-toolkit-toast{position:fixed;left:50%;bottom:76px;transform:translateX(-50%);z-index:2147483000; padding:9px 16px;border-radius:999px;background:rgba(24,28,36,.9);color:#fff; font:12px/1.4 -apple-system,"Segoe UI","Microsoft YaHei",sans-serif;pointer-events:none;opacity:0; transition:opacity .2s} #qk-toolkit-toast.show{opacity:1}`; document.head.appendChild(css); panel = document.createElement('div'); panel.id = 'qk-toolkit-panel'; panel.innerHTML = [ '