吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 261|回复: 4
上一主题 下一主题
收起左侧

[其他转载] 夸克分享链接目录树导出工具V1.0

[复制链接]
跳转到指定楼层
楼主
cxs808 发表于 2026-9-12 16:26 回帖奖励
本帖最后由 cxs808 于 2026-9-12 18:24 编辑

借助于万能的AI,做了一个程序,用于导出夸克分享链接目录树,做了油猴和Nodejs CLi2个版本。
主要用途:拿到别人分享的夸克链接后,在未登录状态下,本程序可以看到“里面有什么、多大、几层”,并将目录结构保存为Txt文档。
成品在最后。

程序效果演示:
1.油猴脚本演示





2.Nodejs cli演示




3.导出的文本文件内容



这个程序我做了2个版本:


1.油猴版
点击浏览器上的油猴图标,选择添加新脚本。


先把下面的编辑框内容清空,复制附件中quark-tree-export.user.js里的全部代码,粘贴进来,按Ctrl+S保存。



随便打开一个夸克云盘的分享链接,比如https://pan.quark.cn/s/5dc373f0a50a#/list/share,在浏览器页面的右上角点击文件夹图标,即可获取全部内容。



这个脚本未上架各个脚本站,主要是网络不好,打不开。


quark-tree-export.user.js的代码如下:



[Asm] 纯文本查看 复制代码
// ==UserScript==
// @name         夸克云盘目录树导出工具
// @namespace    quark-tree-export
// @version      1.0.0
// @description  免登录导出夸克分享目录树,支持层级展开、文件大小、TXT/Markdown 导出
// @author       wallechfox
// @match        https://pan.quark.cn/s/*
// @match        https://pan.quark.cn/*
// @grant        GM_setClipboard
// @run-at       document-idle
// @license      MIT
// ==/UserScript==
(function () {
    'use strict';

    // ============ 配置 ============
    const API = "https://drive-pc.quark.cn/1/clouddrive/share/sharepage/detail";
    const CONCURRENCY = 8;
    const RETRY = 4;
    const PAGE_SIZE = 50;

    // ============ 状态 ============
    let shareInfo = null;
    let shareFolderName = "";
    let isScanning = false;
    let lastItems = [];
    let lastTree = null;
    let lastResultText = "";
    let pageCache = new Map();

    const sleep = t => new Promise(r => setTimeout(r, t));

    // ============ 分享信息 ============
    function getShareInfo() {
        let pwd_id = null, stoken = "", title = "";
        try {
            const raw = sessionStorage.getItem("_share_args");
            if (raw) {
                const parsed = JSON.parse(raw);
                const val = parsed.value || parsed;
                if (val && val.pwd_id) {
                    pwd_id = val.pwd_id;
                    stoken = val.stoken || "";
                    title = val.title || val.share_title || val.name || val.share_name || val.pwd_name || "";
                }
            }
        } catch (e) { /* ignore */ }
        if (!pwd_id) {
            const m = location.pathname.match(/\/s\/([a-zA-Z0-9]+)/);
            if (m) pwd_id = m[1];
        }
        if (!title && document.title) {
            title = document.title.replace(/[-_—|]\s*夸克[^\s]*.*$/, "").trim()
                                  .replace(/^【.*?】/, "").trim();
        }
        if (!pwd_id) return null;
        return { pwd_id, stoken, title: title || "" };
    }

    function ensureShareInfo() {
        shareInfo = getShareInfo();
        return !!shareInfo;
    }

    // ============ API 请求 ============
    async function fetchPage(fid, page) {
        if (page === 1 && pageCache.has(fid)) return pageCache.get(fid);

        const params = new URLSearchParams({
            pr: "ucpro", fr: "pc",
            pwd_id: shareInfo.pwd_id,
            stoken: shareInfo.stoken || "",
            pdir_fid: fid, force: "0",
            _page: String(page), _size: String(PAGE_SIZE)
        });
        for (let i = 0; i < RETRY; i++) {
            try {
                const r = await fetch(`${API}?${params}`, { credentials: "include" });
                if (r.status === 400) throw new Error("参数失效(可能需要输入提取码)");
                if (!r.ok) throw new Error(`HTTP ${r.status}`);
                const j = await r.json();
                extractShareName(j);
                if (page === 1) pageCache.set(fid, j);
                return j;
            } catch (e) {
                if (i === RETRY - 1) { console.error("[quark-tree] fetchPage failed:", e); return null; }
                await sleep(800 + Math.random() * 1200);
            }
        }
        return null;
    }

    function extractShareName(j) {
        if (!j || !j.data || shareFolderName) return;
        const d = j.data;
        const s = d.share || {};
        shareFolderName =
            d.share_title || s.share_title || s.title || s.share_name || s.pwd_name ||
            d.title || d.folder_name || d.share_folder_name || s.folder_name || d.name || "";
    }

    async function fetchShareMeta() {
        pageCache.clear();
        await fetchPage("0", 1);
    }

    async function listAll(fid) {
        const all = [];
        let page = 1;
        while (true) {
            const j = await fetchPage(fid, page);
            if (!j || !j.data || !Array.isArray(j.data.list) || j.data.list.length === 0) break;
            all.push(...j.data.list);
            const total = j.data.total || 0;
            if (all.length >= total || j.data.list.length < PAGE_SIZE) break;
            page++;
            await sleep(80 + Math.random() * 120);
        }
        return all;
    }

    // ============ 扫描 ============
    async function scanRoot(depth) {
        const items = [];
        const visited = new Set(["0"]);
        const queue = [{ fid: "0", path: "", depth: 0, isFolder: true, size: 0 }];
        async function worker() {
            while (queue.length > 0) {
                const task = queue.shift();
                if (!task) break;
                if (task.depth > depth) continue;
                const list = await listAll(task.fid);
                for (const it of list) {
                    const full = (task.path ? task.path : "") + "/" + it.file_name;
                    items.push({ path: full, size: it.size || 0, isFolder: !!it.dir });
                    if (it.dir && !visited.has(it.fid)) {
                        visited.add(it.fid);
                        queue.push({ fid: it.fid, path: full, depth: task.depth + 1, isFolder: true, size: 0 });
                    }
                }
            }
        }
        await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
        return items;
    }

    // ============ 构建树 ============
    function buildTree(items) {
        const root = { name: "ROOT", isFolder: true, size: 0, children: {} };
        for (const item of items) {
            const parts = item.path.split("/").filter(Boolean);
            let node = root;
            for (let i = 0; i < parts.length; i++) {
                const part = parts[i];
                const isLast = i === parts.length - 1;
                if (!node.children[part]) {
                    node.children[part] = { name: part, isFolder: false, size: 0, children: {} };
                }
                const child = node.children[part];
                if (isLast) {
                    child.isFolder = item.isFolder;
                    child.size = item.size || 0;
                } else {
                    child.isFolder = true;
                }
                node = child;
            }
        }
        computeFolderSizes(root);
        return root;
    }

    function computeFolderSizes(node) {
        let total = node.isFolder ? 0 : (node.size || 0);
        for (const k of Object.keys(node.children)) {
            total += computeFolderSizes(node.children[k]);
        }
        if (node.isFolder) node.size = total;
        return total;
    }

    function countChildren(node) {
        const keys = Object.keys(node.children);
        let folders = 0, files = 0;
        for (const k of keys) node.children[k].isFolder ? folders++ : files++;
        return { folders, files, total: keys.length };
    }

    function formatSize(bytes) {
        if (!bytes) return "";
        const units = ["B", "KB", "MB", "GB", "TB"];
        let v = bytes, i = 0;
        while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
        return v.toFixed(i === 0 ? 0 : 2) + " " + units[i];
    }

    function generateTreeText(root) {
        const lines = [];
        function walk(children, prefix, isTop) {
            for (let i = 0; i < children.length; i++) {
                const child = children[i];
                const isLast = i === children.length - 1;
                let line = prefix + (isTop ? "" : (isLast ? "└── " : "├── "));
                line += child.name + (child.isFolder ? "/" : "");
                if (child.size) line += "  (" + formatSize(child.size) + ")";
                lines.push(line);
                if (child.isFolder) {
                    const childChildren = sortedKeys(child).map(k => child.children[k]);
                    walk(childChildren, prefix + (isLast ? "    " : "│   "), false);
                }
            }
        }
        walk(sortedKeys(root).map(k => root.children[k]), "", true);
        return lines.join("\n");
    }

    // ============ 下载 ============
    function downloadTxt(text, filename) {
        const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url; a.download = filename || "quark目录.txt";
        document.body.appendChild(a); a.click();
        document.body.removeChild(a);
        setTimeout(() => URL.revokeObjectURL(url), 4000);
    }

    function escapeHtml(s) {
        const div = document.createElement("div");
        div.textContent = s;
        return div.innerHTML;
    }

    // ============ 样式(浅色主题) ============
    const STYLE = `
#qte-fab {
  position: fixed; top: 16px; right: 16px; z-index: 2147483647;
  width: 52px; height: 52px; border-radius: 50%;
  background: linear-gradient(135deg, #3b82f6, #2563eb);
  color: #fff; border: none; cursor: pointer;
  box-shadow: 0 4px 16px rgba(0,0,0,.15);
  display: flex; align-items: center; justify-content: center;
  font-size: 24px; transition: transform .2s;
}
#qte-fab:hover { transform: scale(1.08); }

#qte-mask {
  position: fixed; inset: 0; z-index: 2147483646;
  background: rgba(0,0,0,.2); display: none;
}
#qte-mask.show { display: block; }

#qte-drawer {
  position: fixed; top: 0; right: 0;
  width: 75vw; max-width: 1000px; min-width: 400px; height: 100vh;
  background: #ffffff; color: #1e293b;
  z-index: 2147483647;
  transform: translateX(100%);
  transition: transform .28s ease;
  display: grid;
  grid-template-rows: auto auto auto 1fr auto;
  box-shadow: -4px 0 20px rgba(0,0,0,.1);
  overflow: hidden;
  font-family: -apple-system, "Segoe UI", Roboto, "Microsoft YaHei", sans-serif;
}
#qte-drawer.show { transform: translateX(0); }

#qte-head {
  padding: 12px 20px; background: #f8fafc;
  display: flex; align-items: center; justify-content: space-between;
  border-bottom: 1px solid #e2e8f0;
}
#qte-title { font-size: 16px; font-weight: 600; color: #1e293b; }
#qte-close {
  background: none; border: none; color: #64748b; font-size: 26px;
  cursor: pointer; line-height: 1;
}
#qte-close:hover { color: #1e293b; }

#qte-url-bar {
  padding: 8px 20px; background: #f8fafc;
  border-bottom: 1px solid #e2e8f0;
  font-size: 12px; color: #64748b;
  display: flex; align-items: center; gap: 8px;
}
#qte-url-bar a { color: #3b82f6; text-decoration: none; word-break: break-all; }
#qte-url-bar a:hover { text-decoration: underline; }

#qte-controls {
  padding: 12px 20px; background: #f8fafc;
  border-bottom: 1px solid #e2e8f0;
}
.qte-ctrl-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.qte-label { font-size: 13px; color: #64748b; }
.qte-depth {
  width: 64px; padding: 5px 8px; background: #fff; color: #1e293b;
  border: 1px solid #cbd5e1; border-radius: 6px; font-size: 13px;
}
.qte-btn {
  background: #3b82f6; color: #fff; border: none; padding: 6px 14px;
  border-radius: 6px; cursor: pointer; font-size: 13px;
}
.qte-btn:hover { background: #2563eb; }
.qte-btn.ghost { background: #e2e8f0; color: #1e293b; }
.qte-btn.ghost:hover { background: #cbd5e1; }
.qte-btn:disabled { opacity: .5; cursor: not-allowed; }
#qte-progress { font-size: 12px; color: #64748b; }

#qte-tree-wrap {
  min-height: 0; min-width: 0;
  overflow-y: auto; overflow-x: auto;
  overscroll-behavior: contain;
  padding: 16px 20px; background: #ffffff;
}
#qte-tree-wrap::-webkit-scrollbar { width: 8px; height: 8px; }
#qte-tree-wrap::-webkit-scrollbar-track { background: #f1f5f9; }
#qte-tree-wrap::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
#qte-tree-wrap::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
#qte-tree-wrap { scrollbar-width: thin; scrollbar-color: #cbd5e1 #f1f5f9; }
#qte-tree {
  font-family: "Consolas", "Microsoft YaHei", monospace;
  font-size: 13px; line-height: 1.7;
}
#qte-tree ul {
  list-style: none; padding-left: 20px; margin: 0;
  position: relative;
}
#qte-tree > ul { padding-left: 0; }
#qte-tree ul::before {
  content: ''; position: absolute; left: 6px; top: 0; bottom: 8px;
  width: 1px; background: #cbd5e1; opacity: .6;
}
#qte-tree > ul::before { display: none; }
#qte-tree li { position: relative; }
.qte-node {
  display: flex; align-items: center; padding: 1px 6px;
  border-radius: 3px; cursor: pointer; user-select: none;
}
.qte-node:hover { background: #f1f5f9; }
.qte-node::before {
  content: ''; position: absolute; left: -14px; top: 14px;
  width: 14px; height: 1px; background: #cbd5e1; opacity: .6;
}
#qte-tree > ul > li > .qte-node::before { display: none; }
.qte-arrow {
  width: 14px; height: 14px; margin-right: 2px; flex-shrink: 0;
  transition: transform .15s; fill: #64748b;
}
.qte-arrow.expanded { transform: rotate(90deg); }
.qte-arrow.hidden { visibility: hidden; }
.qte-icon { width: 16px; height: 16px; margin-right: 6px; flex-shrink: 0; }
.qte-name { white-space: nowrap; color: #1e293b; }
.qte-meta { color: #94a3b8; font-size: 11px; margin-left: 8px; }
.qte-size { color: #64748b; font-size: 11px; margin-left: 8px; }
.qte-folder-size { color: #d97706; font-size: 11px; margin-left: 8px; }

#qte-foot {
  padding: 10px 20px; background: #f8fafc;
  border-top: 1px solid #e2e8f0;
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
}
`;

    function injectStyle() {
        const style = document.createElement("style");
        style.textContent = STYLE;
        document.head.appendChild(style);
    }

    // ============ 浮动按钮 ============
    function createFab() {
        const fab = document.createElement("button");
        fab.id = "qte-fab";
        fab.title = "夸克目录树导出";
        fab.textContent = "&#128194;";
        fab.addEventListener("click", toggleDrawer);
        document.body.appendChild(fab);
    }

    // ============ 抽屉面板 ============
    function createDrawer() {
        const mask = document.createElement("div");
        mask.id = "qte-mask";
        mask.addEventListener("click", closeDrawer);

        const drawer = document.createElement("div");
        drawer.id = "qte-drawer";
        drawer.innerHTML = `
          <div id="qte-head">
            <span id="qte-title">&#128194; 夸克云盘目录树导出</span>
            <button id="qte-close">×</button>
          </div>
          <div id="qte-url-bar">
            &#128279; 当前分享:<a href="${escapeHtml(location.href)}" target="_blank" id="qte-url-link">${escapeHtml(location.href)}</a>
          </div>
          <div id="qte-controls">
            <div class="qte-ctrl-row">
              <span class="qte-label">扫描层级:</span>
              <input type="number" class="qte-depth" id="qte-scan-depth" min="1" max="20" value="4">
              <button class="qte-btn" id="qte-export">&#128640; 开始导出</button>
              <span id="qte-progress"></span>
            </div>
          </div>
          <div id="qte-tree-wrap">
            <div id="qte-tree"></div>
          </div>
          <div id="qte-foot">
            <button class="qte-btn" id="qte-download">&#11015; 下载 TXT</button>
            <button class="qte-btn ghost" id="qte-copy">&#128203; 复制路径</button>
            <button class="qte-btn ghost" id="qte-expand-all">展开全部</button>
            <button class="qte-btn ghost" id="qte-collapse-all">折叠全部</button>
            <span class="qte-label" id="qte-count" style="margin-left:auto;"></span>
          </div>
        `;
        document.body.appendChild(mask);
        document.body.appendChild(drawer);

        drawer.style.setProperty("position", "fixed", "important");
        drawer.style.setProperty("top", "0", "important");
        drawer.style.setProperty("right", "0", "important");
        drawer.style.setProperty("height", "100vh", "important");
        drawer.style.setProperty("display", "grid", "important");
        drawer.style.setProperty("grid-template-rows", "auto auto auto 1fr auto", "important");
        drawer.style.setProperty("overflow", "hidden", "important");
        drawer.style.setProperty("z-index", "2147483647", "important");
        const treeWrap = drawer.querySelector("#qte-tree-wrap");
        treeWrap.style.setProperty("min-height", "0", "important");
        treeWrap.style.setProperty("height", "auto", "important");
        treeWrap.style.setProperty("overflow-y", "scroll", "important");
        treeWrap.style.setProperty("overflow-x", "auto", "important");
        treeWrap.style.setProperty("overscroll-behavior", "contain", "important");
        const treeEl = drawer.querySelector("#qte-tree");
        treeEl.style.setProperty("display", "block", "important");
        treeEl.style.setProperty("height", "auto", "important");
        treeEl.style.setProperty("max-height", "none", "important");

        drawer.querySelector("#qte-close").addEventListener("click", closeDrawer);
        drawer.querySelector("#qte-export").addEventListener("click", doExport);
        drawer.querySelector("#qte-download").addEventListener("click", () => {
            if (!lastResultText) { alert("暂无导出内容,请先导出。"); return; }
            downloadTxt(lastResultText, `quark目录_${shareInfo ? shareInfo.pwd_id : ""}.txt`);
        });
        drawer.querySelector("#qte-copy").addEventListener("click", async () => {
            if (!lastResultText) { alert("暂无导出内容"); return; }
            try { await navigator.clipboard.writeText(lastResultText); alert("已复制到剪贴板"); }
            catch (e) {
                if (typeof GM_setClipboard !== "undefined") { GM_setClipboard(lastResultText); alert("已复制到剪贴板"); }
                else alert("复制失败:" + e.message);
            }
        });
        drawer.querySelector("#qte-expand-all").addEventListener("click", expandAll);
        drawer.querySelector("#qte-collapse-all").addEventListener("click", collapseAll);

        return drawer;
    }

    function toggleDrawer() {
        const drawer = document.getElementById("qte-drawer") || createDrawer();
        const mask = document.getElementById("qte-mask");
        if (drawer.classList.contains("show")) closeDrawer();
        else openDrawer();
    }

    function openDrawer() {
        let drawer = document.getElementById("qte-drawer");
        if (!drawer) drawer = createDrawer();
        const mask = document.getElementById("qte-mask");
        drawer.classList.add("show");
        mask.classList.add("show");
        const link = document.getElementById("qte-url-link");
        if (link) {
            link.href = location.href;
            link.textContent = location.href;
        }
    }

    function closeDrawer() {
        const drawer = document.getElementById("qte-drawer");
        const mask = document.getElementById("qte-mask");
        if (drawer) drawer.classList.remove("show");
        if (mask) mask.classList.remove("show");
    }

    // ============ 树形渲染 ============
    function renderTree(root) {
        const container = document.getElementById("qte-tree");
        container.innerHTML = "";
        const ul = document.createElement("ul");
        renderChildren(ul, root);
        container.appendChild(ul);
    }

    function sortedKeys(node) {
        return Object.keys(node.children).sort((a, b) => {
            const af = node.children[a].isFolder;
            const bf = node.children[b].isFolder;
            if (af !== bf) return af ? -1 : 1;
            return a.localeCompare(b, "zh-CN");
        });
    }

    function renderChildren(ul, parentNode) {
        for (const key of sortedKeys(parentNode)) {
            ul.appendChild(createNodeEl(parentNode.children[key]));
        }
    }

    function createNodeEl(node) {
        const li = document.createElement("li");
        li.className = node.isFolder ? "qte-folder" : "qte-file";
        li._node = node;

        const row = document.createElement("div");
        row.className = "qte-node";

        const arrow = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        arrow.setAttribute("class", "qte-arrow" + (node.isFolder ? "" : " hidden"));
        arrow.setAttribute("viewBox", "0 0 24 24");
        arrow.innerHTML = '<path d="M10 17l5-5-5-5v10z"/>';

        const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        icon.setAttribute("class", "qte-icon");
        icon.setAttribute("viewBox", "0 0 24 24");
        if (node.isFolder) {
            icon.setAttribute("fill", "#f59e0b");
            icon.innerHTML = '<path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>';
        } else {
            icon.setAttribute("fill", "#64748b");
            icon.innerHTML = '<path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6z"/>';
        }

        const name = document.createElement("span");
        name.className = "qte-name";
        name.textContent = node.name;

        row.appendChild(arrow);
        row.appendChild(icon);
        row.appendChild(name);

        if (node.isFolder) {
            const c = countChildren(node);
            const meta = document.createElement("span");
            meta.className = "qte-meta";
            meta.textContent = c.total > 0 ? `(${c.folders} 文件夹 / ${c.files} 文件)` : "(空)";
            row.appendChild(meta);
            const sz = formatSize(node.size);
            if (sz) {
                const sizeEl = document.createElement("span");
                sizeEl.className = "qte-folder-size";
                sizeEl.textContent = sz;
                row.appendChild(sizeEl);
            }
        } else if (node.size) {
            const sz = document.createElement("span");
            sz.className = "qte-size";
            sz.textContent = formatSize(node.size);
            row.appendChild(sz);
        }

        li.appendChild(row);

        if (node.isFolder) {
            const childUl = document.createElement("ul");
            childUl.style.display = "none";
            li.appendChild(childUl);

            row.addEventListener("click", () => {
                const hidden = childUl.style.display === "none";
                if (hidden) {
                    if (childUl.children.length === 0) renderChildren(childUl, node);
                    childUl.style.display = "block";
                    arrow.classList.add("expanded");
                } else {
                    childUl.style.display = "none";
                    arrow.classList.remove("expanded");
                }
            });
        }
        return li;
    }

    function expandAll() {
        if (!lastTree) return;
        const container = document.getElementById("qte-tree");
        container.innerHTML = "";
        const ul = document.createElement("ul");
        renderAll(ul, lastTree);
        container.appendChild(ul);
    }

    function renderAll(ul, node) {
        for (const key of sortedKeys(node)) {
            const child = node.children[key];
            const li = createNodeEl(child);
            ul.appendChild(li);
            if (child.isFolder && Object.keys(child.children).length > 0) {
                const childUl = li.querySelector("ul");
                renderAll(childUl, child);
                childUl.style.display = "block";
                li.querySelector(".qte-arrow").classList.add("expanded");
            }
        }
    }

    function collapseAll() {
        if (!lastTree) return;
        renderTree(lastTree);
    }

    // ============ 导出主流程 ============
    async function doExport() {
        if (isScanning) return;
        if (!ensureShareInfo()) {
            alert("未检测到分享信息,请确认当前在夸克分享页面且已输入提取码。");
            return;
        }
        const scanDepth = parseInt(document.getElementById("qte-scan-depth").value, 10);
        if (!scanDepth || scanDepth < 1) { alert("请输入有效的扫描层级(≥1)"); return; }

        const exportBtn = document.getElementById("qte-export");
        const progressEl = document.getElementById("qte-progress");
        const treeEl = document.getElementById("qte-tree");
        const countEl = document.getElementById("qte-count");

        isScanning = true;
        exportBtn.disabled = true;
        exportBtn.textContent = "&#9203; 扫描中…";
        progressEl.textContent = "准备扫描…";
        treeEl.innerHTML = '<div style="color:#64748b;padding:10px;">扫描中,请稍候…</div>';
        shareFolderName = "";

        try {
            progressEl.textContent = "获取分享信息…";
            await fetchShareMeta();

            progressEl.textContent = `扫描全部根目录,扫描深度 ${scanDepth}…`;
            const items = await scanRoot(scanDepth);

            items.sort((a, b) => a.path.localeCompare(b.path, "zh-CN"));
            lastItems = items;
            lastTree = buildTree(items);

            const treeText = generateTreeText(lastTree);
            lastResultText = location.href + "\n" + "=================================\n" + treeText;

            renderTree(lastTree);

                        expandToDepth(scanDepth); 

            const folderCount = items.filter(i => i.isFolder).length;
            const fileCount = items.length - folderCount;
            const totalSize = formatSize(lastTree.size || 0);
            countEl.textContent = `共 ${items.length} 项(文件夹 ${folderCount} / 文件 ${fileCount})` + (totalSize ? `,总计 ${totalSize}` : "");
            progressEl.textContent = `&#9989; 扫描完成(扫描 ${scanDepth} 层),已下载 TXT。`;

            downloadTxt(lastResultText, `quark目录_${shareInfo.pwd_id}.txt`);
        } catch (e) {
            console.error(e);
            progressEl.textContent = "&#10060; 导出失败:" + e.message;
            alert("导出失败:" + e.message);
        } finally {
            isScanning = false;
            exportBtn.disabled = false;
            exportBtn.textContent = "&#128640; 开始导出";
        }
    }


function expandToDepth(depth) {
    const container = document.getElementById("qte-tree");
    if (!container) return;

    function walk(ul, currentDepth) {
        if (currentDepth > depth) return;
        const lis = ul.querySelectorAll(":scope > li");
        for (const li of lis) {
            const row = li.querySelector(":scope > .qte-node");
            const childUl = li.querySelector(":scope > ul");
            if (!row || !childUl) continue;

            const node = li._node;
            if (node && node.isFolder) {
                if (childUl.children.length === 0) {
                    renderChildren(childUl, node);
                }
                childUl.style.display = "block";
                const arrow = row.querySelector(".qte-arrow");
                if (arrow) arrow.classList.add("expanded");
                walk(childUl, currentDepth + 1);
            }
        }
    }

    const rootUl = container.querySelector(":scope > ul");
    if (rootUl) walk(rootUl, 1);
}



    // ============ 启动 ============
    function init() {
        injectStyle();
        createFab();
        let lastUrl = location.href;
        new MutationObserver(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                shareInfo = null;
                pageCache.clear();
                const link = document.getElementById("qte-url-link");
                if (link) { link.href = location.href; link.textContent = location.href; }
            }
        }).observe(document, { subtree: true, childList: true });
    }

    if (document.body) init();
    else window.addEventListener("DOMContentLoaded", init);
})();

2.Nodejs cli版。


电脑上需要先安装Nodejs,打开cmd窗口,切换到cli文件夹,输入nodeindex.js https://pan.quark.cn/s/5dc373f0a50a#/list/share,回车后即可下载。






程序用法:
[Asm] 纯文本查看 复制代码
# 基本用法(自动换 stoken,展开全部层级,自动导出 txt)
node index.js "https://pan.quark.cn/s/xxxxxx"
# 控制展开层级
node index.js "https://pan.quark.cn/s/xxxxxx" --depth 3
# 折叠模式(只显示第一层)
node index.js "https://pan.quark.cn/s/xxxxxx" --no-expand
# 有提取码
node index.js "https://pan.quark.cn/s/xxxxxx" --passcode abcd
# 手动指定 stoken(跳过自动换取,最稳的兜底)
node index.js "https://pan.quark.cn/s/xxxxxx" --stoken "xxx"
# JSON 输出(不导出 txt,便于管道处理)
node index.js "https://pan.quark.cn/s/xxxxxx" --json
# 指定导出文件路径
node index.js "https://pan.quark.cn/s/xxxxxx" --output "D:\我的目录树\result.txt"





程序核心代码:
[Asm] 纯文本查看 复制代码
#!/usr/bin/env node
/* eslint-disable no-console */
'use strict';

/**
 * 夸克云盘分享目录树 CLI(免登录,仅查看公开分享)
 *
 * 用法:
 *   node index.js <url> [选项]
 *   node index.js                 # 显示帮助
 *
 * 示例:
 *   node index.js "https://pan.quark.cn/s/xxxxxx"
 *   node index.js "https://pan.quark.cn/s/xxxxxx" --depth 3
 *   node index.js "https://pan.quark.cn/s/xxxxxx" --no-expand
 *   node index.js "https://pan.quark.cn/s/xxxxxx" --passcode abcd
 *   node index.js "https://pan.quark.cn/s/xxxxxx" --stoken "xxx"   # 跳过自动换取
 *   node index.js "https://pan.quark.cn/s/xxxxxx" --json
 *   node index.js "https://pan.quark.cn/s/xxxxxx" --output result.txt
 *
 * 接口说明:
 *   - sharepage/token  -> POST {pwd_id, passcode}  换取 stoken
 *   - sharepage/detail -> GET  ?pwd_id=&stoken=&pdir_fid=&...  拉取文件列表
 */

const https = require('https');
const fs = require('fs');
const path = require('path');
const { URL } = require('url');

// ============ 配置 ============
const API_HOST = 'drive-pc.quark.cn';
const API_PATH = '/1/clouddrive/share/sharepage/detail';
const PAGE_SIZE = 50;
const RETRY = 3;

// ============ Windows 中文乱码修复 ============
if (process.platform === 'win32') {
  require('child_process').execSync('chcp 65001 >nul 2>&1');
  process.stdout.setDefaultEncoding('utf-8');
  process.stderr.setDefaultEncoding('utf-8');
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// ============ 参数解析 ============
function parseArgs(argv) {
  const args = {
    _: [],
    depth: Infinity,
    expand: true,
    json: false,
    size: PAGE_SIZE,
    stoken: '',
    passcode: '',
    output: '',
  };
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    switch (a) {
      case '-h':
      case '--help':
        args.help = true;
        break;
      case '-d':
      case '--depth':
        args.depth = parseInt(argv[++i], 10) || 1;
        break;
      case '--no-expand':
        args.expand = false;
        break;
      case '-j':
      case '--json':
        args.json = true;
        break;
      case '-s':
      case '--size':
        args.size = parseInt(argv[++i], 10) || PAGE_SIZE;
        break;
      case '--stoken':
        args.stoken = argv[++i] || '';
        break;
      case '--passcode':
      case '-p':
        args.passcode = argv[++i] || '';
        break;
      case '-o':
      case '--output':
        args.output = argv[++i] || '';
        break;
      default:
        if (a.startsWith('-')) {
          console.error(`未知参数: ${a}`);
        } else {
          args._.push(a);
        }
    }
  }
  return args;
}

function printHelp() {
  console.log(`
夸克云盘分享目录树 CLI(免登录,查看公开分享)

用法:
  quark-tree <url> [选项]

选项:
  -h, --help            显示帮助
  -d, --depth <n>       展开层级,默认全部 (Infinity)
      --no-expand       折叠模式,只显示第一层
  -j, --json            以 JSON 输出目录树(便于程序处理,不导出 txt)
  -s, --size <n>        每页数量 (默认 50)
      --stoken <token>  直接指定 stoken,跳过自动换取
  -p, --passcode <code> 分享提取码
  -o, --output <file>   导出 txt 路径 (默认自动生成)

示例:
  node index.js "https://pan.quark.cn/s/xxxxxx"
  node index.js "https://pan.quark.cn/s/xxxxxx" --depth 3
  node index.js "https://pan.quark.cn/s/xxxxxx" --passcode abcd
  node index.js "https://pan.quark.cn/s/xxxxxx" --stoken "xxx" --json
  node index.js "https://pan.quark.cn/s/xxxxxx" --output result.txt
`.trim());
}

// ============ URL / 分享信息 ============
function parseUrl(raw) {
  try {
    const u = new URL(raw);
    const m = u.pathname.match(/\/s\/([a-zA-Z0-9]+)/);
    if (!m) return null;
    return {
      pwd_id: m[1],
      stoken: u.searchParams.get('stoken') || '',
    };
  } catch (e) {
    return null;
  }
}

// ============ HTTP ============
function getJSON(url) {
  return new Promise((resolve, reject) => {
    https
      .get(url, (res) => {
        let buf = '';
        res.on('data', (c) => (buf += c));
        res.on('end', () => {
          try {
            resolve(JSON.parse(buf));
          } catch (e) {
            reject(new Error('响应解析失败: ' + buf.slice(0, 200)));
          }
        });
      })
      .on('error', reject);
  });
}

function postJSON(urlPath, bodyObj) {
  return new Promise((resolve, reject) => {
    const data = JSON.stringify(bodyObj);
    const options = {
      hostname: API_HOST,
      path: urlPath,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(data),
        'User-Agent':
          'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36',
        Referer: 'https://pan.quark.cn/',
        Origin: 'https://pan.quark.cn',
      },
    };
    const req = https.request(options, (res) => {
      let buf = '';
      res.on('data', (c) => (buf += c));
      res.on('end', () => {
        try {
          resolve(JSON.parse(buf));
        } catch (e) {
          reject(new Error('响应解析失败: ' + buf.slice(0, 200)));
        }
      });
    });
    req.on('error', reject);
    req.write(data);
    req.end();
  });
}

// ============ 夸克接口 ============
async function getStoken(pwd_id, passcode) {
  const p = '/1/clouddrive/share/sharepage/token';
  const body = { pwd_id, passcode: passcode || '' };

  let lastErr;
  for (let i = 0; i < RETRY; i++) {
    try {
      const j = await postJSON(p, body);
      if (j && j.data && j.data.stoken) {
        return { stoken: j.data.stoken, title: j.data.title || '' };
      }
      if (j && (j.error_msg || j.message)) {
        throw new Error(j.error_msg || j.message);
      }
      throw new Error('换取 stoken 失败:返回结构异常');
    } catch (e) {
      lastErr = e;
      if (i === RETRY - 1) break;
      await sleep(300 * (i + 1));
    }
  }
  throw lastErr;
}

async function fetchPage(pwd_id, stoken, pdir_fid, page, size) {
  const qs = new URLSearchParams({
    pr: 'ucpro',
    fr: 'pc',
    pwd_id,
    stoken: stoken || '',
    pdir_fid,
    force: '0',
    _page: String(page),
    _size: String(size),
  });
  const url = `https://${API_HOST}${API_PATH}?${qs.toString()}`;

  let lastErr;
  for (let i = 0; i < RETRY; i++) {
    try {
      const j = await getJSON(url);
      if (j && j.data) return j;
      lastErr = new Error(j?.error_msg || j?.message || '接口返回为空');
    } catch (e) {
      lastErr = e;
    }
    await sleep(300 * (i + 1));
  }
  throw lastErr;
}

async function listAll(pwd_id, stoken, pdir_fid, size) {
  const all = [];
  let page = 1;
  while (true) {
    const j = await fetchPage(pwd_id, stoken, pdir_fid, page, size);
    const list = j.data.list || [];
    if (list.length === 0) break;
    all.push(...list);
    const total = j.data.total || 0;
    if (all.length >= total || list.length < size) break;
    page++;
    await sleep(50);
  }
  return all;
}

async function scan(pwd_id, stoken, depth, size) {
  const items = [];
  const visited = new Set(['0']);
  const queue = [{ fid: '0', path: '', depth: 0 }];

  async function worker() {
    while (queue.length) {
      const task = queue.shift();
      if (!task || task.depth > depth) continue;
      const list = await listAll(pwd_id, stoken, task.fid, size);
      for (const it of list) {
        const full = (task.path ? task.path : '') + '/' + it.file_name;
        items.push({
          path: full,
          name: it.file_name,
          size: it.size || 0,
          isFolder: !!it.dir,
          fid: it.fid,
        });
        if (it.dir && !visited.has(it.fid)) {
          visited.add(it.fid);
          queue.push({ fid: it.fid, path: full, depth: task.depth + 1 });
        }
      }
    }
  }

  const concurrency = 8;
  await Promise.all(Array.from({ length: concurrency }, worker));
  return items;
}

// ============ 构建树 ============
function buildTree(items, rootName) {
  const root = { name: rootName || '/', isFolder: true, size: 0, children: {} };
  for (const item of items) {
    const parts = item.path.split('/').filter(Boolean);
    let node = root;
    for (let i = 0; i < parts.length; i++) {
      const part = parts[i];
      const isLast = i === parts.length - 1;
      if (!node.children[part]) {
        node.children[part] = { name: part, isFolder: false, size: 0, children: {} };
      }
      const child = node.children[part];
      if (isLast) {
        child.isFolder = item.isFolder;
        child.size = item.size || 0;
      } else {
        child.isFolder = true;
      }
      node = child;
    }
  }
  function sum(node) {
    if (!node.isFolder) return node.size || 0;
    let t = 0;
    for (const k of Object.keys(node.children)) t += sum(node.children[k]);
    node.size = t;
    return t;
  }
  sum(root);
  return root;
}

function sortChildren(node) {
  return Object.keys(node.children)
    .sort((a, b) => {
      const af = node.children[a].isFolder;
      const bf = node.children[b].isFolder;
      if (af !== bf) return af ? -1 : 1;
      return a.localeCompare(b, 'zh-CN');
    })
    .map((k) => node.children[k]);
}

// ============ 格式化 ============
function formatSize(bytes) {
  if (!bytes) return '';
  const units = ['B', 'KB', 'MB', 'GB', 'TB'];
  let v = bytes;
  let i = 0;
  while (v >= 1024 && i < units.length - 1) {
    v /= 1024;
    i++;
  }
  return `${v.toFixed(i === 0 ? 0 : 2)} ${units[i]}`;
}

// ============ 终端颜色 ============
const RED = '\x1b[31m';
const c = {
  reset: '\x1b[0m',
  bold: '\x1b[1m',
  dim: '\x1b[2m',
  blue: '\x1b[34m',
  green: '\x1b[32m',
  yellow: '\x1b[33m',
  gray: '\x1b[90m',
  cyan: '\x1b[36m',
};
function supportsColor() {
  return process.stdout.isTTY && process.env.TERM !== 'dumb';
}
function paint(str, color) {
  return supportsColor() ? `${color}${str}${c.reset}` : str;
}

function renderColor(node, { expandAll, depthLimit }) {
  const lines = [];
  function walk(children, prefix, isTop, currentDepth) {
    for (let i = 0; i < children.length; i++) {
      const child = children[i];
      const isLast = i === children.length - 1;
      const branch = isTop ? '' : isLast ? '└── ' : '├── ';
      let name = child.name;
      if (child.isFolder) name = paint(name + '/', c.yellow);
      else name = paint(name, c.reset);
      const sz = formatSize(child.size);
      const sizeStr = sz ? paint(`  (${sz})`, c.gray) : '';
      lines.push(prefix + branch + name + sizeStr);

      if (child.isFolder && Object.keys(child.children).length > 0) {
        const shouldExpand = expandAll || currentDepth < depthLimit;
        if (shouldExpand) {
          walk(
            sortChildren(child),
            prefix + (isTop ? '' : isLast ? '    ' : '│   '),
            false,
            currentDepth + 1
          );
        } else {
          const hidden = Object.keys(child.children).length;
          lines.push(
            prefix +
              (isTop ? '' : isLast ? '    ' : '│   ') +
              (isLast ? '└── ' : '├── ') +
              paint(`… (${hidden} 项,用 --depth 展开)`, c.dim)
          );
        }
      }
    }
  }
  walk(sortChildren(node), '', true, 1);
  return lines.join('\n');
}

function renderText(node, { expandAll, depthLimit }) {
  const lines = [];
  function walk(children, prefix, isTop, currentDepth) {
    for (let i = 0; i < children.length; i++) {
      const child = children[i];
      const isLast = i === children.length - 1;
      const branch = isTop ? '' : isLast ? '└── ' : '├── ';
      let line = prefix + branch + child.name + (child.isFolder ? '/' : '');
      const sz = formatSize(child.size);
      if (sz) line += `  (${sz})`;
      lines.push(line);

      if (child.isFolder && Object.keys(child.children).length > 0) {
        const shouldExpand = expandAll || currentDepth < depthLimit;
        if (shouldExpand) {
          walk(
            sortChildren(child),
            prefix + (isTop ? '' : isLast ? '    ' : '│   '),
            false,
            currentDepth + 1
          );
        }
      }
    }
  }
  walk(sortChildren(node), '', true, 1);
  return lines.join('\n');
}

// ============ 导出文件 ============
function saveToFile(text, args, pwd_id, rootTitle) {
  let filePath;
  if (args.output) {
    filePath = path.resolve(args.output);
  } else {
    const safeTitle = (rootTitle || pwd_id).replace(/[\\/:*?"<>|]/g, '_').slice(0, 50);
    const date = new Date().toISOString().slice(0, 10);
    const fileName = `quark目录_${safeTitle}_${date}.txt`;
    filePath = path.resolve(process.cwd(), fileName);
  }
  fs.writeFileSync(filePath, text, 'utf-8');
  return filePath;
}

// ============ 主流程 ============
async function main() {
  const args = parseArgs(process.argv.slice(2));

  if (args.help || args._.length === 0) {
    printHelp();
    process.exit(args.help ? 0 : 1);
  }

  const url = args._[0];
  const info = parseUrl(url);
  if (!info) {
    console.error(paint('&#10007; 无法解析分享链接,请检查是否为 pan.quark.cn/s/xxx 格式', RED));
    process.exit(1);
  }

  const { pwd_id } = info;
  console.log(paint('&#128279; 分享ID: ', c.gray) + pwd_id);

  let stoken = args.stoken || info.stoken;
  let rootTitle = '';

  if (!stoken) {
    console.log(paint('&#128273; 正在换取访问凭证 (stoken)…', c.gray));
    try {
      const result = await getStoken(pwd_id, args.passcode);
      stoken = result.stoken;
      rootTitle = result.title;
      console.log(paint('&#10003; 获取成功', c.gray));
      if (args.passcode) console.log(paint('&#128274; 使用提取码: ' + args.passcode, c.gray));
    } catch (e) {
      const msg = String(e.message || '');
      console.error(paint('&#10007; 换取 stoken 失败: ' + msg, RED));
      if (msg.includes('passcode') || msg.includes('提取') || msg.includes('密码')) {
        console.error(paint('  该分享需要提取码,请用 --passcode <code> 指定。', c.gray));
      } else {
        console.error(
          paint('  可尝试用浏览器打开分享页,F12 复制 stoken 后用 --stoken 指定。', c.gray)
        );
      }
      process.exit(1);
    }
  } else {
    console.log(paint('&#128273; 使用指定 stoken', c.gray));
  }

  const userDepth = Number.isFinite(args.depth) ? args.depth : Infinity;

  console.log(paint('&#9203; 正在扫描目录树,请稍候…\n', c.gray));

  try {
    const items = await scan(pwd_id, stoken, userDepth, args.size);
    const tree = buildTree(items, rootTitle || pwd_id);

    const folderCount = items.filter((i) => i.isFolder).length;
    const fileCount = items.length - folderCount;

    const plainText = renderText(tree, {
      expandAll: args.expand && !Number.isFinite(args.depth),
      depthLimit: args.depth,
    });
    const header = `${url}\n=================================`;
    const summary =
      `\n\n共 ${items.length} 项(文件夹 ${folderCount} / 文件 ${fileCount})` +
      (tree.size ? `\n总计: ${formatSize(tree.size)}` : '');
    const resultText = `${header}\n${plainText}${summary}`;

    if (args.json) {
      console.log(JSON.stringify(tree, null, 2));
    } else {
      const expandAll = args.expand && !Number.isFinite(args.depth);
      const renderer = supportsColor() ? renderColor : renderText;
      console.log(renderer(tree, { expandAll, depthLimit: args.depth }));
      console.log('');
      console.log(
        paint(`共 ${items.length} 项(文件夹 ${folderCount} / 文件 ${fileCount})`, c.gray)
      );
      if (tree.size) console.log(paint(`总计: ${formatSize(tree.size)}`, c.gray));
    }

    // ===== 自动保存 txt(--json 时不导出) =====
    if (!args.json) {
      try {
        const savedPath = saveToFile(resultText, args, pwd_id, rootTitle);
        console.log(paint(`\n&#128196; 已保存: ${savedPath}`, c.green));
      } catch (e) {
        console.error(paint(`\n&#10007; 保存文件失败: ${e.message}`, RED));
      }
    }
  } catch (e) {
    console.error(paint('\n&#10007; 导出失败: ' + e.message, RED));
    process.exit(1);
  }
}

main();



下载链接:

夸克云盘目录树.rar (16.91 KB, 下载次数: 16)

免费评分

参与人数 2吾爱币 +2 热心值 +2 收起 理由
maple185 + 1 + 1 谢谢@Thanks!
Sy6 + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
苏紫方璇 发表于 2026-9-12 17:58
本板块是可以正常上传压缩包的,论坛禁止变相上传附件
3#
 楼主| cxs808 发表于 2026-9-12 18:18 |楼主
苏紫方璇 发表于 2026-9-12 17:58
本板块是可以正常上传压缩包的,论坛禁止变相上传附件

谢谢版主提醒,马上改
4#
laiwen666 发表于 2026-9-13 01:42
5#
梅球王 发表于 2026-9-13 07:38
谢谢分享,这个很实用
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-9-15 13:06

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表