夸克分享链接目录树导出工具V1.0
最近更新 2026年09月14日
资源编号 24037

夸克分享链接目录树导出工具V1.0

郑重承诺丨三色资源网提供安全交易、信息保真!
增值服务:
¥ 免费 元宝
VIP折扣
    折扣详情
  • 体验VIP会员

    免费

  • 月卡VIP会员

    免费

  • 年卡VIP会员

    免费

  • 永久VIP会员

    免费

开通VIP尊享优惠特权
立即获取 升级会员
详情介绍

文章摘要

本文介绍了一款借助AI开发的夸克分享链接目录树导出程序,提供油猴脚本和Node.js CLI两个版本。用户无需登录,即可查看夸克分享链接中的文件内容、大小和层级结构,并导出为TXT文档。油猴版通过浏览器脚本实现,点击图标即可扫描导出;CLI版通过命令行运行,支持指定层级、提取码、stoken及JSON输出等参数。程序核心功能包括分页拉取文件列表、并发扫描目录、构建树形结构、计算文件夹大小及格式化导出。

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

程序效果演示:
1.油猴脚本演示
夸克分享链接目录树导出工具V1.0
2.Nodejs cli演示
夸克分享链接目录树导出工具V1.0
3.导出的文本文件内容
夸克分享链接目录树导出工具V1.0
这个程序我做了2个版本:

1.油猴版
点击浏览器上的油猴图标,选择添加新脚本。
夸克分享链接目录树导出工具V1.0
先把下面的编辑框内容清空,复制附件中quark-tree-export.user.js里的全部代码,粘贴进来,按Ctrl+S保存。
夸克分享链接目录树导出工具V1.0
随便打开一个夸克云盘的分享链接,比如https://pan.quark.cn/s/bb5401ef712a,在浏览器页面的右上角点击文件夹图标,即可获取全部内容。
夸克分享链接目录树导出工具V1.0
这个脚本未上架各个脚本站,主要是网络不好,打不开。

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

  1. // ==UserScript==
  2. // @name         夸克云盘目录树导出工具
  3. // @namespace    quark-tree-export
  4. // @version      1.0.0
  5. // @description  免登录导出夸克分享目录树,支持层级展开、文件大小、TXT/Markdown 导出
  6. // @author       wallechfox
  7. // @match        https://pan.quark.cn/s/*
  8. // @match        https://pan.quark.cn/*
  9. // @grant        GM_setClipboard
  10. // @run-at       document-idle
  11. // @license      MIT
  12. // ==/UserScript==
  13. (function () {
  14.     ‘use strict’;
  15.     // ============ 配置 ============
  16.     const API = “https://drive-pc.quark.cn/1/clouddrive/share/sharepage/detail”;
  17.     const CONCURRENCY = 8;
  18.     const RETRY = 4;
  19.     const PAGE_SIZE = 50;
  20.     // ============ 状态 ============
  21.     let shareInfo = null;
  22.     let shareFolderName = “”;
  23.     let isScanning = false;
  24.     let lastItems = [];
  25.     let lastTree = null;
  26.     let lastResultText = “”;
  27.     let pageCache = new Map();
  28.     const sleep = t => new Promise(r => setTimeout(r, t));
  29.     // ============ 分享信息 ============
  30.     function getShareInfo() {
  31.         let pwd_id = null, stoken = “”, title = “”;
  32.         try {
  33.             const raw = sessionStorage.getItem(“_share_args”);
  34.             if (raw) {
  35.                 const parsed = JSON.parse(raw);
  36.                 const val = parsed.value || parsed;
  37.                 if (val && val.pwd_id) {
  38.                     pwd_id = val.pwd_id;
  39.                     stoken = val.stoken || “”;
  40.                     title = val.title || val.share_title || val.name || val.share_name || val.pwd_name || “”;
  41.                 }
  42.             }
  43.         } catch (e) { /* ignore */ }
  44.         if (!pwd_id) {
  45.             const m = location.pathname.match(/\/s\/([a-zA-Z0-9]+)/);
  46.             if (m) pwd_id = m[1];
  47.         }
  48.         if (!title && document.title) {
  49.             title = document.title.replace(/[-_—|]\s*夸克[^\s]*.*$/, “”).trim()
  50.                                   .replace(/^【.*?】/, “”).trim();
  51.         }
  52.         if (!pwd_id) return null;
  53.         return { pwd_id, stoken, title: title || “” };
  54.     }
  55.     function ensureShareInfo() {
  56.         shareInfo = getShareInfo();
  57.         return !!shareInfo;
  58.     }
  59.     // ============ API 请求 ============
  60.     async function fetchPage(fid, page) {
  61.         if (page === 1 && pageCache.has(fid)) return pageCache.get(fid);
  62.         const params = new URLSearchParams({
  63.             pr: “ucpro”, fr: “pc”,
  64.             pwd_id: shareInfo.pwd_id,
  65.             stoken: shareInfo.stoken || “”,
  66.             pdir_fid: fid, force: “0”,
  67.             _page: String(page), _size: String(PAGE_SIZE)
  68.         });
  69.         for (let i = 0; i < RETRY; i++) {
  70.             try {
  71.                 const r = await fetch(`${API}?${params}`, { credentials: “include” });
  72.                 if (r.status === 400) throw new Error(“参数失效(可能需要输入提取码)”);
  73.                 if (!r.ok) throw new Error(`HTTP ${r.status}`);
  74.                 const j = await r.json();
  75.                 extractShareName(j);
  76.                 if (page === 1) pageCache.set(fid, j);
  77.                 return j;
  78.             } catch (e) {
  79.                 if (i === RETRY – 1) { console.error(“[quark-tree] fetchPage failed:”, e); return null; }
  80.                 await sleep(800 + Math.random() * 1200);
  81.             }
  82.         }
  83.         return null;
  84.     }
  85.     function extractShareName(j) {
  86.         if (!j || !j.data || shareFolderName) return;
  87.         const d = j.data;
  88.         const s = d.share || {};
  89.         shareFolderName =
  90.             d.share_title || s.share_title || s.title || s.share_name || s.pwd_name ||
  91.             d.title || d.folder_name || d.share_folder_name || s.folder_name || d.name || “”;
  92.     }
  93.     async function fetchShareMeta() {
  94.         pageCache.clear();
  95.         await fetchPage(“0”, 1);
  96.     }
  97.     async function listAll(fid) {
  98.         const all = [];
  99.         let page = 1;
  100.         while (true) {
  101.             const j = await fetchPage(fid, page);
  102.             if (!j || !j.data || !Array.isArray(j.data.list) || j.data.list.length === 0) break;
  103.             all.push(…j.data.list);
  104.             const total = j.data.total || 0;
  105.             if (all.length >= total || j.data.list.length < PAGE_SIZE) break;
  106.             page++;
  107.             await sleep(80 + Math.random() * 120);
  108.         }
  109.         return all;
  110.     }
  111.     // ============ 扫描 ============
  112.     async function scanRoot(depth) {
  113.         const items = [];
  114.         const visited = new Set([“0”]);
  115.         const queue = [{ fid: “0”, path: “”, depth: 0, isFolder: true, size: 0 }];
  116.         async function worker() {
  117.             while (queue.length > 0) {
  118.                 const task = queue.shift();
  119.                 if (!task) break;
  120.                 if (task.depth > depth) continue;
  121.                 const list = await listAll(task.fid);
  122.                 for (const it of list) {
  123.                     const full = (task.path ? task.path : “”) + “/” + it.file_name;
  124.                     items.push({ path: full, size: it.size || 0, isFolder: !!it.dir });
  125.                     if (it.dir && !visited.has(it.fid)) {
  126.                         visited.add(it.fid);
  127.                         queue.push({ fid: it.fid, path: full, depth: task.depth + 1, isFolder: true, size: 0 });
  128.                     }
  129.                 }
  130.             }
  131.         }
  132.         await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
  133.         return items;
  134.     }
  135.     // ============ 构建树 ============
  136.     function buildTree(items) {
  137.         const root = { name: “ROOT”, isFolder: true, size: 0, children: {} };
  138.         for (const item of items) {
  139.             const parts = item.path.split(“/”).filter(Boolean);
  140.             let node = root;
  141.             for (let i = 0; i < parts.length; i++) {
  142.                 const part = parts[i];
  143.                 const isLast = i === parts.length – 1;
  144.                 if (!node.children[part]) {
  145.                     node.children[part] = { name: part, isFolder: false, size: 0, children: {} };
  146.                 }
  147.                 const child = node.children[part];
  148.                 if (isLast) {
  149.                     child.isFolder = item.isFolder;
  150.                     child.size = item.size || 0;
  151.                 } else {
  152.                     child.isFolder = true;
  153.                 }
  154.                 node = child;
  155.             }
  156.         }
  157.         computeFolderSizes(root);
  158.         return root;
  159.     }
  160.     function computeFolderSizes(node) {
  161.         let total = node.isFolder ? 0 : (node.size || 0);
  162.         for (const k of Object.keys(node.children)) {
  163.             total += computeFolderSizes(node.children[k]);
  164.         }
  165.         if (node.isFolder) node.size = total;
  166.         return total;
  167.     }
  168.     function countChildren(node) {
  169.         const keys = Object.keys(node.children);
  170.         let folders = 0, files = 0;
  171.         for (const k of keys) node.children[k].isFolder ? folders++ : files++;
  172.         return { folders, files, total: keys.length };
  173.     }
  174.     function formatSize(bytes) {
  175.         if (!bytes) return “”;
  176.         const units = [“B”, “KB”, “MB”, “GB”, “TB”];
  177.         let v = bytes, i = 0;
  178.         while (v >= 1024 && i < units.length – 1) { v /= 1024; i++; }
  179.         return v.toFixed(i === 0 ? 0 : 2) + ” ” + units[i];
  180.     }
  181.     function generateTreeText(root) {
  182.         const lines = [];
  183.         function walk(children, prefix, isTop) {
  184.             for (let i = 0; i < children.length; i++) {
  185.                 const child = children[i];
  186.                 const isLast = i === children.length – 1;
  187.                 let line = prefix + (isTop ? “” : (isLast ? “└── ” : “├── “));
  188.                 line += child.name + (child.isFolder ? “/” : “”);
  189.                 if (child.size) line += ”  (” + formatSize(child.size) + “)”;
  190.                 lines.push(line);
  191.                 if (child.isFolder) {
  192.                     const childChildren = sortedKeys(child).map(k => child.children[k]);
  193.                     walk(childChildren, prefix + (isLast ? ”    ” : “│   “), false);
  194.                 }
  195.             }
  196.         }
  197.         walk(sortedKeys(root).map(k => root.children[k]), “”, true);
  198.         return lines.join(“\n”);
  199.     }
  200.     // ============ 下载 ============
  201.     function downloadTxt(text, filename) {
  202.         const blob = new Blob([text], { type: “text/plain;charset=utf-8” });
  203.         const url = URL.createObjectURL(blob);
  204.         const a = document.createElement(“a”);
  205.         a.href = url; a.download = filename || “quark目录.txt”;
  206.         document.body.appendChild(a); a.click();
  207.         document.body.removeChild(a);
  208.         setTimeout(() => URL.revokeObjectURL(url), 4000);
  209.     }
  210.     function escapeHtml(s) {
  211.         const div = document.createElement(“div”);
  212.         div.textContent = s;
  213.         return div.innerHTML;
  214.     }
  215.     // ============ 样式(浅色主题) ============
  216.     const STYLE = `
  217. #qte-fab {
  218.   position: fixed; top: 16px; right: 16px; z-index: 2147483647;
  219.   width: 52px; height: 52px; border-radius: 50%;
  220.   background: linear-gradient(135deg, #3b82f6, #2563eb);
  221.   color: #fff; border: none; cursor: pointer;
  222.   box-shadow: 0 4px 16px rgba(0,0,0,.15);
  223.   display: flex; align-items: center; justify-content: center;
  224.   font-size: 24px; transition: transform .2s;
  225. }
  226. #qte-fab:hover { transform: scale(1.08); }
  227. #qte-mask {
  228.   position: fixed; inset: 0; z-index: 2147483646;
  229.   background: rgba(0,0,0,.2); display: none;
  230. }
  231. #qte-mask.show { display: block; }
  232. #qte-drawer {
  233.   position: fixed; top: 0; right: 0;
  234.   width: 75vw; max-width: 1000px; min-width: 400px; height: 100vh;
  235.   background: #ffffff; color: #1e293b;
  236.   z-index: 2147483647;
  237.   transform: translateX(100%);
  238.   transition: transform .28s ease;
  239.   display: grid;
  240.   grid-template-rows: auto auto auto 1fr auto;
  241.   box-shadow: -4px 0 20px rgba(0,0,0,.1);
  242.   overflow: hidden;
  243.   font-family: -apple-system, “Segoe UI”, Roboto, “Microsoft YaHei”, sans-serif;
  244. }
  245. #qte-drawer.show { transform: translateX(0); }
  246. #qte-head {
  247.   padding: 12px 20px; background: #f8fafc;
  248.   display: flex; align-items: center; justify-content: space-between;
  249.   border-bottom: 1px solid #e2e8f0;
  250. }
  251. #qte-title { font-size: 16px; font-weight: 600; color: #1e293b; }
  252. #qte-close {
  253.   background: none; border: none; color: #64748b; font-size: 26px;
  254.   cursor: pointer; line-height: 1;
  255. }
  256. #qte-close:hover { color: #1e293b; }
  257. #qte-url-bar {
  258.   padding: 8px 20px; background: #f8fafc;
  259.   border-bottom: 1px solid #e2e8f0;
  260.   font-size: 12px; color: #64748b;
  261.   display: flex; align-items: center; gap: 8px;
  262. }
  263. #qte-url-bar a { color: #3b82f6; text-decoration: none; word-break: break-all; }
  264. #qte-url-bar a:hover { text-decoration: underline; }
  265. #qte-controls {
  266.   padding: 12px 20px; background: #f8fafc;
  267.   border-bottom: 1px solid #e2e8f0;
  268. }
  269. .qte-ctrl-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
  270. .qte-label { font-size: 13px; color: #64748b; }
  271. .qte-depth {
  272.   width: 64px; padding: 5px 8px; background: #fff; color: #1e293b;
  273.   border: 1px solid #cbd5e1; border-radius: 6px; font-size: 13px;
  274. }
  275. .qte-btn {
  276.   background: #3b82f6; color: #fff; border: none; padding: 6px 14px;
  277.   border-radius: 6px; cursor: pointer; font-size: 13px;
  278. }
  279. .qte-btn:hover { background: #2563eb; }
  280. .qte-btn.ghost { background: #e2e8f0; color: #1e293b; }
  281. .qte-btn.ghost:hover { background: #cbd5e1; }
  282. .qte-btn:disabled { opacity: .5; cursor: not-allowed; }
  283. #qte-progress { font-size: 12px; color: #64748b; }
  284. #qte-tree-wrap {
  285.   min-height: 0; min-width: 0;
  286.   overflow-y: auto; overflow-x: auto;
  287.   overscroll-behavior: contain;
  288.   padding: 16px 20px; background: #ffffff;
  289. }
  290. #qte-tree-wrap::-webkit-scrollbar { width: 8px; height: 8px; }
  291. #qte-tree-wrap::-webkit-scrollbar-track { background: #f1f5f9; }
  292. #qte-tree-wrap::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
  293. #qte-tree-wrap::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
  294. #qte-tree-wrap { scrollbar-width: thin; scrollbar-color: #cbd5e1 #f1f5f9; }
  295. #qte-tree {
  296.   font-family: “Consolas”, “Microsoft YaHei”, monospace;
  297.   font-size: 13px; line-height: 1.7;
  298. }
  299. #qte-tree ul {
  300.   list-style: none; padding-left: 20px; margin: 0;
  301.   position: relative;
  302. }
  303. #qte-tree > ul { padding-left: 0; }
  304. #qte-tree ul::before {
  305.   content: ”; position: absolute; left: 6px; top: 0; bottom: 8px;
  306.   width: 1px; background: #cbd5e1; opacity: .6;
  307. }
  308. #qte-tree > ul::before { display: none; }
  309. #qte-tree li { position: relative; }
  310. .qte-node {
  311.   display: flex; align-items: center; padding: 1px 6px;
  312.   border-radius: 3px; cursor: pointer; user-select: none;
  313. }
  314. .qte-node:hover { background: #f1f5f9; }
  315. .qte-node::before {
  316.   content: ”; position: absolute; left: -14px; top: 14px;
  317.   width: 14px; height: 1px; background: #cbd5e1; opacity: .6;
  318. }
  319. #qte-tree > ul > li > .qte-node::before { display: none; }
  320. .qte-arrow {
  321.   width: 14px; height: 14px; margin-right: 2px; flex-shrink: 0;
  322.   transition: transform .15s; fill: #64748b;
  323. }
  324. .qte-arrow.expanded { transform: rotate(90deg); }
  325. .qte-arrow.hidden { visibility: hidden; }
  326. .qte-icon { width: 16px; height: 16px; margin-right: 6px; flex-shrink: 0; }
  327. .qte-name { white-space: nowrap; color: #1e293b; }
  328. .qte-meta { color: #94a3b8; font-size: 11px; margin-left: 8px; }
  329. .qte-size { color: #64748b; font-size: 11px; margin-left: 8px; }
  330. .qte-folder-size { color: #d97706; font-size: 11px; margin-left: 8px; }
  331. #qte-foot {
  332.   padding: 10px 20px; background: #f8fafc;
  333.   border-top: 1px solid #e2e8f0;
  334.   display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
  335. }
  336. `;
  337.     function injectStyle() {
  338.         const style = document.createElement(“style”);
  339.         style.textContent = STYLE;
  340.         document.head.appendChild(style);
  341.     }
  342.     // ============ 浮动按钮 ============
  343.     function createFab() {
  344.         const fab = document.createElement(“button”);
  345.         fab.id = “qte-fab”;
  346.         fab.title = “夸克目录树导出”;
  347.         fab.textContent = “&#128194;”;
  348.         fab.addEventListener(“click”, toggleDrawer);
  349.         document.body.appendChild(fab);
  350.     }
  351.     // ============ 抽屉面板 ============
  352.     function createDrawer() {
  353.         const mask = document.createElement(“div”);
  354.         mask.id = “qte-mask”;
  355.         mask.addEventListener(“click”, closeDrawer);
  356.         const drawer = document.createElement(“div”);
  357.         drawer.id = “qte-drawer”;
  358.         drawer.innerHTML = `
  359.           <div id=”qte-head”>
  360.             <span id=”qte-title”>&#128194; 夸克云盘目录树导出</span>
  361.             <button id=”qte-close”>×</button>
  362.           </div>
  363.           <div id=”qte-url-bar”>
  364.             &#128279; 当前分享:<a href=”${escapeHtml(location.href)}” target=”_blank” id=”qte-url-link”>${escapeHtml(location.href)}</a>
  365.           </div>
  366.           <div id=”qte-controls”>
  367.             <div class=”qte-ctrl-row”>
  368.               <span class=”qte-label”>扫描层级:</span>
  369.               <input type=”number” class=”qte-depth” id=”qte-scan-depth” min=”1″ max=”20″ value=”4″>
  370.               <button class=”qte-btn” id=”qte-export”>&#128640; 开始导出</button>
  371.               <span id=”qte-progress”></span>
  372.             </div>
  373.           </div>
  374.           <div id=”qte-tree-wrap”>
  375.             <div id=”qte-tree”></div>
  376.           </div>
  377.           <div id=”qte-foot”>
  378.             <button class=”qte-btn” id=”qte-download”>&#11015; 下载 TXT</button>
  379.             <button class=”qte-btn ghost” id=”qte-copy”>&#128203; 复制路径</button>
  380.             <button class=”qte-btn ghost” id=”qte-expand-all”>展开全部</button>
  381.             <button class=”qte-btn ghost” id=”qte-collapse-all”>折叠全部</button>
  382.             <span class=”qte-label” id=”qte-count” style=”margin-left:auto;”></span>
  383.           </div>
  384.         `;
  385.         document.body.appendChild(mask);
  386.         document.body.appendChild(drawer);
  387.         drawer.style.setProperty(“position”, “fixed”, “important”);
  388.         drawer.style.setProperty(“top”, “0”, “important”);
  389.         drawer.style.setProperty(“right”, “0”, “important”);
  390.         drawer.style.setProperty(“height”, “100vh”, “important”);
  391.         drawer.style.setProperty(“display”, “grid”, “important”);
  392.         drawer.style.setProperty(“grid-template-rows”, “auto auto auto 1fr auto”, “important”);
  393.         drawer.style.setProperty(“overflow”, “hidden”, “important”);
  394.         drawer.style.setProperty(“z-index”, “2147483647”, “important”);
  395.         const treeWrap = drawer.querySelector(“#qte-tree-wrap”);
  396.         treeWrap.style.setProperty(“min-height”, “0”, “important”);
  397.         treeWrap.style.setProperty(“height”, “auto”, “important”);
  398.         treeWrap.style.setProperty(“overflow-y”, “scroll”, “important”);
  399.         treeWrap.style.setProperty(“overflow-x”, “auto”, “important”);
  400.         treeWrap.style.setProperty(“overscroll-behavior”, “contain”, “important”);
  401.         const treeEl = drawer.querySelector(“#qte-tree”);
  402.         treeEl.style.setProperty(“display”, “block”, “important”);
  403.         treeEl.style.setProperty(“height”, “auto”, “important”);
  404.         treeEl.style.setProperty(“max-height”, “none”, “important”);
  405.         drawer.querySelector(“#qte-close”).addEventListener(“click”, closeDrawer);
  406.         drawer.querySelector(“#qte-export”).addEventListener(“click”, doExport);
  407.         drawer.querySelector(“#qte-download”).addEventListener(“click”, () => {
  408.             if (!lastResultText) { alert(“暂无导出内容,请先导出。”); return; }
  409.             downloadTxt(lastResultText, `quark目录_${shareInfo ? shareInfo.pwd_id : “”}.txt`);
  410.         });
  411.         drawer.querySelector(“#qte-copy”).addEventListener(“click”, async () => {
  412.             if (!lastResultText) { alert(“暂无导出内容”); return; }
  413.             try { await navigator.clipboard.writeText(lastResultText); alert(“已复制到剪贴板”); }
  414.             catch (e) {
  415.                 if (typeof GM_setClipboard !== “undefined”) { GM_setClipboard(lastResultText); alert(“已复制到剪贴板”); }
  416.                 else alert(“复制失败:” + e.message);
  417.             }
  418.         });
  419.         drawer.querySelector(“#qte-expand-all”).addEventListener(“click”, expandAll);
  420.         drawer.querySelector(“#qte-collapse-all”).addEventListener(“click”, collapseAll);
  421.         return drawer;
  422.     }
  423.     function toggleDrawer() {
  424.         const drawer = document.getElementById(“qte-drawer”) || createDrawer();
  425.         const mask = document.getElementById(“qte-mask”);
  426.         if (drawer.classList.contains(“show”)) closeDrawer();
  427.         else openDrawer();
  428.     }
  429.     function openDrawer() {
  430.         let drawer = document.getElementById(“qte-drawer”);
  431.         if (!drawer) drawer = createDrawer();
  432.         const mask = document.getElementById(“qte-mask”);
  433.         drawer.classList.add(“show”);
  434.         mask.classList.add(“show”);
  435.         const link = document.getElementById(“qte-url-link”);
  436.         if (link) {
  437.             link.href = location.href;
  438.             link.textContent = location.href;
  439.         }
  440.     }
  441.     function closeDrawer() {
  442.         const drawer = document.getElementById(“qte-drawer”);
  443.         const mask = document.getElementById(“qte-mask”);
  444.         if (drawer) drawer.classList.remove(“show”);
  445.         if (mask) mask.classList.remove(“show”);
  446.     }
  447.     // ============ 树形渲染 ============
  448.     function renderTree(root) {
  449.         const container = document.getElementById(“qte-tree”);
  450.         container.innerHTML = “”;
  451.         const ul = document.createElement(“ul”);
  452.         renderChildren(ul, root);
  453.         container.appendChild(ul);
  454.     }
  455.     function sortedKeys(node) {
  456.         return Object.keys(node.children).sort((a, b) => {
  457.             const af = node.children[a].isFolder;
  458.             const bf = node.children[b].isFolder;
  459.             if (af !== bf) return af ? -1 : 1;
  460.             return a.localeCompare(b, “zh-CN”);
  461.         });
  462.     }
  463.     function renderChildren(ul, parentNode) {
  464.         for (const key of sortedKeys(parentNode)) {
  465.             ul.appendChild(createNodeEl(parentNode.children[key]));
  466.         }
  467.     }
  468.     function createNodeEl(node) {
  469.         const li = document.createElement(“li”);
  470.         li.className = node.isFolder ? “qte-folder” : “qte-file”;
  471.         li._node = node;
  472.         const row = document.createElement(“div”);
  473.         row.className = “qte-node”;
  474.         const arrow = document.createElementNS(“http://www.w3.org/2000/svg”, “svg”);
  475.         arrow.setAttribute(“class”, “qte-arrow” + (node.isFolder ? “” : ” hidden”));
  476.         arrow.setAttribute(“viewBox”, “0 0 24 24”);
  477.         arrow.innerHTML = ‘<path d=”M10 17l5-5-5-5v10z”/>’;
  478.         const icon = document.createElementNS(“http://www.w3.org/2000/svg”, “svg”);
  479.         icon.setAttribute(“class”, “qte-icon”);
  480.         icon.setAttribute(“viewBox”, “0 0 24 24”);
  481.         if (node.isFolder) {
  482.             icon.setAttribute(“fill”, “#f59e0b”);
  483.             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”/>’;
  484.         } else {
  485.             icon.setAttribute(“fill”, “#64748b”);
  486.             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”/>’;
  487.         }
  488.         const name = document.createElement(“span”);
  489.         name.className = “qte-name”;
  490.         name.textContent = node.name;
  491.         row.appendChild(arrow);
  492.         row.appendChild(icon);
  493.         row.appendChild(name);
  494.         if (node.isFolder) {
  495.             const c = countChildren(node);
  496.             const meta = document.createElement(“span”);
  497.             meta.className = “qte-meta”;
  498.             meta.textContent = c.total > 0 ? `(${c.folders} 文件夹 / ${c.files} 文件)` : “(空)”;
  499.             row.appendChild(meta);
  500.             const sz = formatSize(node.size);
  501.             if (sz) {
  502.                 const sizeEl = document.createElement(“span”);
  503.                 sizeEl.className = “qte-folder-size”;
  504.                 sizeEl.textContent = sz;
  505.                 row.appendChild(sizeEl);
  506.             }
  507.         } else if (node.size) {
  508.             const sz = document.createElement(“span”);
  509.             sz.className = “qte-size”;
  510.             sz.textContent = formatSize(node.size);
  511.             row.appendChild(sz);
  512.         }
  513.         li.appendChild(row);
  514.         if (node.isFolder) {
  515.             const childUl = document.createElement(“ul”);
  516.             childUl.style.display = “none”;
  517.             li.appendChild(childUl);
  518.             row.addEventListener(“click”, () => {
  519.                 const hidden = childUl.style.display === “none”;
  520.                 if (hidden) {
  521.                     if (childUl.children.length === 0) renderChildren(childUl, node);
  522.                     childUl.style.display = “block”;
  523.                     arrow.classList.add(“expanded”);
  524.                 } else {
  525.                     childUl.style.display = “none”;
  526.                     arrow.classList.remove(“expanded”);
  527.                 }
  528.             });
  529.         }
  530.         return li;
  531.     }
  532.     function expandAll() {
  533.         if (!lastTree) return;
  534.         const container = document.getElementById(“qte-tree”);
  535.         container.innerHTML = “”;
  536.         const ul = document.createElement(“ul”);
  537.         renderAll(ul, lastTree);
  538.         container.appendChild(ul);
  539.     }
  540.     function renderAll(ul, node) {
  541.         for (const key of sortedKeys(node)) {
  542.             const child = node.children[key];
  543.             const li = createNodeEl(child);
  544.             ul.appendChild(li);
  545.             if (child.isFolder && Object.keys(child.children).length > 0) {
  546.                 const childUl = li.querySelector(“ul”);
  547.                 renderAll(childUl, child);
  548.                 childUl.style.display = “block”;
  549.                 li.querySelector(“.qte-arrow”).classList.add(“expanded”);
  550.             }
  551.         }
  552.     }
  553.     function collapseAll() {
  554.         if (!lastTree) return;
  555.         renderTree(lastTree);
  556.     }
  557.     // ============ 导出主流程 ============
  558.     async function doExport() {
  559.         if (isScanning) return;
  560.         if (!ensureShareInfo()) {
  561.             alert(“未检测到分享信息,请确认当前在夸克分享页面且已输入提取码。”);
  562.             return;
  563.         }
  564.         const scanDepth = parseInt(document.getElementById(“qte-scan-depth”).value, 10);
  565.         if (!scanDepth || scanDepth < 1) { alert(“请输入有效的扫描层级(≥1)”); return; }
  566.         const exportBtn = document.getElementById(“qte-export”);
  567.         const progressEl = document.getElementById(“qte-progress”);
  568.         const treeEl = document.getElementById(“qte-tree”);
  569.         const countEl = document.getElementById(“qte-count”);
  570.         isScanning = true;
  571.         exportBtn.disabled = true;
  572.         exportBtn.textContent = “&#9203; 扫描中…”;
  573.         progressEl.textContent = “准备扫描…”;
  574.         treeEl.innerHTML = ‘<div style=”color:#64748b;padding:10px;”>扫描中,请稍候…</div>’;
  575.         shareFolderName = “”;
  576.         try {
  577.             progressEl.textContent = “获取分享信息…”;
  578.             await fetchShareMeta();
  579.             progressEl.textContent = `扫描全部根目录,扫描深度 ${scanDepth}…`;
  580.             const items = await scanRoot(scanDepth);
  581.             items.sort((a, b) => a.path.localeCompare(b.path, “zh-CN”));
  582.             lastItems = items;
  583.             lastTree = buildTree(items);
  584.             const treeText = generateTreeText(lastTree);
  585.             lastResultText = location.href + “\n” + “=================================\n” + treeText;
  586.             renderTree(lastTree);
  587.                         expandToDepth(scanDepth);
  588.             const folderCount = items.filter(i => i.isFolder).length;
  589.             const fileCount = items.length – folderCount;
  590.             const totalSize = formatSize(lastTree.size || 0);
  591.             countEl.textContent = `共 ${items.length} 项(文件夹 ${folderCount} / 文件 ${fileCount})` + (totalSize ? `,总计 ${totalSize}` : “”);
  592.             progressEl.textContent = `&#9989; 扫描完成(扫描 ${scanDepth} 层),已下载 TXT。`;
  593.             downloadTxt(lastResultText, `quark目录_${shareInfo.pwd_id}.txt`);
  594.         } catch (e) {
  595.             console.error(e);
  596.             progressEl.textContent = “&#10060; 导出失败:” + e.message;
  597.             alert(“导出失败:” + e.message);
  598.         } finally {
  599.             isScanning = false;
  600.             exportBtn.disabled = false;
  601.             exportBtn.textContent = “&#128640; 开始导出”;
  602.         }
  603.     }
  604. function expandToDepth(depth) {
  605.     const container = document.getElementById(“qte-tree”);
  606.     if (!container) return;
  607.     function walk(ul, currentDepth) {
  608.         if (currentDepth > depth) return;
  609.         const lis = ul.querySelectorAll(“:scope > li”);
  610.         for (const li of lis) {
  611.             const row = li.querySelector(“:scope > .qte-node”);
  612.             const childUl = li.querySelector(“:scope > ul”);
  613.             if (!row || !childUl) continue;
  614.             const node = li._node;
  615.             if (node && node.isFolder) {
  616.                 if (childUl.children.length === 0) {
  617.                     renderChildren(childUl, node);
  618.                 }
  619.                 childUl.style.display = “block”;
  620.                 const arrow = row.querySelector(“.qte-arrow”);
  621.                 if (arrow) arrow.classList.add(“expanded”);
  622.                 walk(childUl, currentDepth + 1);
  623.             }
  624.         }
  625.     }
  626.     const rootUl = container.querySelector(“:scope > ul”);
  627.     if (rootUl) walk(rootUl, 1);
  628. }
  629.     // ============ 启动 ============
  630.     function init() {
  631.         injectStyle();
  632.         createFab();
  633.         let lastUrl = location.href;
  634.         new MutationObserver(() => {
  635.             if (location.href !== lastUrl) {
  636.                 lastUrl = location.href;
  637.                 shareInfo = null;
  638.                 pageCache.clear();
  639.                 const link = document.getElementById(“qte-url-link”);
  640.                 if (link) { link.href = location.href; link.textContent = location.href; }
  641.             }
  642.         }).observe(document, { subtree: true, childList: true });
  643.     }
  644.     if (document.body) init();
  645.     else window.addEventListener(“DOMContentLoaded”, init);
  646. })();
复制代码

2.Nodejs cli版。

电脑上需要先安装Nodejs,打开cmd窗口,切换到cli文件夹,输入nodeindex.js https://pan.quark.cn/s/5dc373f0a50a#/list/share,回车后即可下载。
夸克分享链接目录树导出工具V1.0
程序用法:

  1. # 基本用法(自动换 stoken,展开全部层级,自动导出 txt)
  2. node index.js “https://pan.quark.cn/s/xxxxxx”
  3. # 控制展开层级
  4. node index.js “https://pan.quark.cn/s/xxxxxx” –depth 3
  5. # 折叠模式(只显示第一层)
  6. node index.js “https://pan.quark.cn/s/xxxxxx” –no-expand
  7. # 有提取码
  8. node index.js “https://pan.quark.cn/s/xxxxxx” –passcode abcd
  9. # 手动指定 stoken(跳过自动换取,最稳的兜底)
  10. node index.js “https://pan.quark.cn/s/xxxxxx” –stoken “xxx”
  11. # JSON 输出(不导出 txt,便于管道处理)
  12. node index.js “https://pan.quark.cn/s/xxxxxx” –json
  13. # 指定导出文件路径
  14. node index.js “https://pan.quark.cn/s/xxxxxx” –output “D:\我的目录树\result.txt”
复制代码

程序核心代码:

  1. #!/usr/bin/env node
  2. /* eslint-disable no-console */
  3. ‘use strict’;
  4. /**
  5. * 夸克云盘分享目录树 CLI(免登录,仅查看公开分享)
  6. *
  7. * 用法:
  8. *   node index.js <url> [选项]
  9. *   node index.js                 # 显示帮助
  10. *
  11. * 示例:
  12. *   node index.js “https://pan.quark.cn/s/xxxxxx”
  13. *   node index.js “https://pan.quark.cn/s/xxxxxx” –depth 3
  14. *   node index.js “https://pan.quark.cn/s/xxxxxx” –no-expand
  15. *   node index.js “https://pan.quark.cn/s/xxxxxx” –passcode abcd
  16. *   node index.js “https://pan.quark.cn/s/xxxxxx” –stoken “xxx”   # 跳过自动换取
  17. *   node index.js “https://pan.quark.cn/s/xxxxxx” –json
  18. *   node index.js “https://pan.quark.cn/s/xxxxxx” –output result.txt
  19. *
  20. * 接口说明:
  21. *   – sharepage/token  -> POST {pwd_id, passcode}  换取 stoken
  22. *   – sharepage/detail -> GET  ?pwd_id=&stoken=&pdir_fid=&…  拉取文件列表
  23. */
  24. const https = require(‘https’);
  25. const fs = require(‘fs’);
  26. const path = require(‘path’);
  27. const { URL } = require(‘url’);
  28. // ============ 配置 ============
  29. const API_HOST = ‘drive-pc.quark.cn’;
  30. const API_PATH = ‘/1/clouddrive/share/sharepage/detail’;
  31. const PAGE_SIZE = 50;
  32. const RETRY = 3;
  33. // ============ Windows 中文乱码修复 ============
  34. if (process.platform === ‘win32’) {
  35.   require(‘child_process’).execSync(‘chcp 65001 >nul 2>&1’);
  36.   process.stdout.setDefaultEncoding(‘utf-8’);
  37.   process.stderr.setDefaultEncoding(‘utf-8’);
  38. }
  39. const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
  40. // ============ 参数解析 ============
  41. function parseArgs(argv) {
  42.   const args = {
  43.     _: [],
  44.     depth: Infinity,
  45.     expand: true,
  46.     json: false,
  47.     size: PAGE_SIZE,
  48.     stoken: ”,
  49.     passcode: ”,
  50.     output: ”,
  51.   };
  52.   for (let i = 0; i < argv.length; i++) {
  53.     const a = argv[i];
  54.     switch (a) {
  55.       case ‘-h’:
  56.       case ‘–help’:
  57.         args.help = true;
  58.         break;
  59.       case ‘-d’:
  60.       case ‘–depth’:
  61.         args.depth = parseInt(argv[++i], 10) || 1;
  62.         break;
  63.       case ‘–no-expand’:
  64.         args.expand = false;
  65.         break;
  66.       case ‘-j’:
  67.       case ‘–json’:
  68.         args.json = true;
  69.         break;
  70.       case ‘-s’:
  71.       case ‘–size’:
  72.         args.size = parseInt(argv[++i], 10) || PAGE_SIZE;
  73.         break;
  74.       case ‘–stoken’:
  75.         args.stoken = argv[++i] || ”;
  76.         break;
  77.       case ‘–passcode’:
  78.       case ‘-p’:
  79.         args.passcode = argv[++i] || ”;
  80.         break;
  81.       case ‘-o’:
  82.       case ‘–output’:
  83.         args.output = argv[++i] || ”;
  84.         break;
  85.       default:
  86.         if (a.startsWith(‘-‘)) {
  87.           console.error(`未知参数: ${a}`);
  88.         } else {
  89.           args._.push(a);
  90.         }
  91.     }
  92.   }
  93.   return args;
  94. }
  95. function printHelp() {
  96.   console.log(`
  97. 夸克云盘分享目录树 CLI(免登录,查看公开分享)
  98. 用法:
  99.   quark-tree <url> [选项]
  100. 选项:
  101.   -h, –help            显示帮助
  102.   -d, –depth <n>       展开层级,默认全部 (Infinity)
  103.       –no-expand       折叠模式,只显示第一层
  104.   -j, –json            以 JSON 输出目录树(便于程序处理,不导出 txt)
  105.   -s, –size <n>        每页数量 (默认 50)
  106.       –stoken <token>  直接指定 stoken,跳过自动换取
  107.   -p, –passcode <code> 分享提取码
  108.   -o, –output <file>   导出 txt 路径 (默认自动生成)
  109. 示例:
  110.   node index.js “https://pan.quark.cn/s/xxxxxx”
  111.   node index.js “https://pan.quark.cn/s/xxxxxx” –depth 3
  112.   node index.js “https://pan.quark.cn/s/xxxxxx” –passcode abcd
  113.   node index.js “https://pan.quark.cn/s/xxxxxx” –stoken “xxx” –json
  114.   node index.js “https://pan.quark.cn/s/xxxxxx” –output result.txt
  115. `.trim());
  116. }
  117. // ============ URL / 分享信息 ============
  118. function parseUrl(raw) {
  119.   try {
  120.     const u = new URL(raw);
  121.     const m = u.pathname.match(/\/s\/([a-zA-Z0-9]+)/);
  122.     if (!m) return null;
  123.     return {
  124.       pwd_id: m[1],
  125.       stoken: u.searchParams.get(‘stoken’) || ”,
  126.     };
  127.   } catch (e) {
  128.     return null;
  129.   }
  130. }
  131. // ============ HTTP ============
  132. function getJSON(url) {
  133.   return new Promise((resolve, reject) => {
  134.     https
  135.       .get(url, (res) => {
  136.         let buf = ”;
  137.         res.on(‘data’, (c) => (buf += c));
  138.         res.on(‘end’, () => {
  139.           try {
  140.             resolve(JSON.parse(buf));
  141.           } catch (e) {
  142.             reject(new Error(‘响应解析失败: ‘ + buf.slice(0, 200)));
  143.           }
  144.         });
  145.       })
  146.       .on(‘error’, reject);
  147.   });
  148. }
  149. function postJSON(urlPath, bodyObj) {
  150.   return new Promise((resolve, reject) => {
  151.     const data = JSON.stringify(bodyObj);
  152.     const options = {
  153.       hostname: API_HOST,
  154.       path: urlPath,
  155.       method: ‘POST’,
  156.       headers: {
  157.         ‘Content-Type’: ‘application/json’,
  158.         ‘Content-Length’: Buffer.byteLength(data),
  159.         ‘User-Agent’:
  160.           ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36’,
  161.         Referer: ‘https://pan.quark.cn/’,
  162.         Origin: ‘https://pan.quark.cn’,
  163.       },
  164.     };
  165.     const req = https.request(options, (res) => {
  166.       let buf = ”;
  167.       res.on(‘data’, (c) => (buf += c));
  168.       res.on(‘end’, () => {
  169.         try {
  170.           resolve(JSON.parse(buf));
  171.         } catch (e) {
  172.           reject(new Error(‘响应解析失败: ‘ + buf.slice(0, 200)));
  173.         }
  174.       });
  175.     });
  176.     req.on(‘error’, reject);
  177.     req.write(data);
  178.     req.end();
  179.   });
  180. }
  181. // ============ 夸克接口 ============
  182. async function getStoken(pwd_id, passcode) {
  183.   const p = ‘/1/clouddrive/share/sharepage/token’;
  184.   const body = { pwd_id, passcode: passcode || ” };
  185.   let lastErr;
  186.   for (let i = 0; i < RETRY; i++) {
  187.     try {
  188.       const j = await postJSON(p, body);
  189.       if (j && j.data && j.data.stoken) {
  190.         return { stoken: j.data.stoken, title: j.data.title || ” };
  191.       }
  192.       if (j && (j.error_msg || j.message)) {
  193.         throw new Error(j.error_msg || j.message);
  194.       }
  195.       throw new Error(‘换取 stoken 失败:返回结构异常’);
  196.     } catch (e) {
  197.       lastErr = e;
  198.       if (i === RETRY – 1) break;
  199.       await sleep(300 * (i + 1));
  200.     }
  201.   }
  202.   throw lastErr;
  203. }
  204. async function fetchPage(pwd_id, stoken, pdir_fid, page, size) {
  205.   const qs = new URLSearchParams({
  206.     pr: ‘ucpro’,
  207.     fr: ‘pc’,
  208.     pwd_id,
  209.     stoken: stoken || ”,
  210.     pdir_fid,
  211.     force: ‘0’,
  212.     _page: String(page),
  213.     _size: String(size),
  214.   });
  215.   const url = `https://${API_HOST}${API_PATH}?${qs.toString()}`;
  216.   let lastErr;
  217.   for (let i = 0; i < RETRY; i++) {
  218.     try {
  219.       const j = await getJSON(url);
  220.       if (j && j.data) return j;
  221.       lastErr = new Error(j?.error_msg || j?.message || ‘接口返回为空’);
  222.     } catch (e) {
  223.       lastErr = e;
  224.     }
  225.     await sleep(300 * (i + 1));
  226.   }
  227.   throw lastErr;
  228. }
  229. async function listAll(pwd_id, stoken, pdir_fid, size) {
  230.   const all = [];
  231.   let page = 1;
  232.   while (true) {
  233.     const j = await fetchPage(pwd_id, stoken, pdir_fid, page, size);
  234.     const list = j.data.list || [];
  235.     if (list.length === 0) break;
  236.     all.push(…list);
  237.     const total = j.data.total || 0;
  238.     if (all.length >= total || list.length < size) break;
  239.     page++;
  240.     await sleep(50);
  241.   }
  242.   return all;
  243. }
  244. async function scan(pwd_id, stoken, depth, size) {
  245.   const items = [];
  246.   const visited = new Set([‘0’]);
  247.   const queue = [{ fid: ‘0’, path: ”, depth: 0 }];
  248.   async function worker() {
  249.     while (queue.length) {
  250.       const task = queue.shift();
  251.       if (!task || task.depth > depth) continue;
  252.       const list = await listAll(pwd_id, stoken, task.fid, size);
  253.       for (const it of list) {
  254.         const full = (task.path ? task.path : ”) + ‘/’ + it.file_name;
  255.         items.push({
  256.           path: full,
  257.           name: it.file_name,
  258.           size: it.size || 0,
  259.           isFolder: !!it.dir,
  260.           fid: it.fid,
  261.         });
  262.         if (it.dir && !visited.has(it.fid)) {
  263.           visited.add(it.fid);
  264.           queue.push({ fid: it.fid, path: full, depth: task.depth + 1 });
  265.         }
  266.       }
  267.     }
  268.   }
  269.   const concurrency = 8;
  270.   await Promise.all(Array.from({ length: concurrency }, worker));
  271.   return items;
  272. }
  273. // ============ 构建树 ============
  274. function buildTree(items, rootName) {
  275.   const root = { name: rootName || ‘/’, isFolder: true, size: 0, children: {} };
  276.   for (const item of items) {
  277.     const parts = item.path.split(‘/’).filter(Boolean);
  278.     let node = root;
  279.     for (let i = 0; i < parts.length; i++) {
  280.       const part = parts[i];
  281.       const isLast = i === parts.length – 1;
  282.       if (!node.children[part]) {
  283.         node.children[part] = { name: part, isFolder: false, size: 0, children: {} };
  284.       }
  285.       const child = node.children[part];
  286.       if (isLast) {
  287.         child.isFolder = item.isFolder;
  288.         child.size = item.size || 0;
  289.       } else {
  290.         child.isFolder = true;
  291.       }
  292.       node = child;
  293.     }
  294.   }
  295.   function sum(node) {
  296.     if (!node.isFolder) return node.size || 0;
  297.     let t = 0;
  298.     for (const k of Object.keys(node.children)) t += sum(node.children[k]);
  299.     node.size = t;
  300.     return t;
  301.   }
  302.   sum(root);
  303.   return root;
  304. }
  305. function sortChildren(node) {
  306.   return Object.keys(node.children)
  307.     .sort((a, b) => {
  308.       const af = node.children[a].isFolder;
  309.       const bf = node.children[b].isFolder;
  310.       if (af !== bf) return af ? -1 : 1;
  311.       return a.localeCompare(b, ‘zh-CN’);
  312.     })
  313.     .map((k) => node.children[k]);
  314. }
  315. // ============ 格式化 ============
  316. function formatSize(bytes) {
  317.   if (!bytes) return ”;
  318.   const units = [‘B’, ‘KB’, ‘MB’, ‘GB’, ‘TB’];
  319.   let v = bytes;
  320.   let i = 0;
  321.   while (v >= 1024 && i < units.length – 1) {
  322.     v /= 1024;
  323.     i++;
  324.   }
  325.   return `${v.toFixed(i === 0 ? 0 : 2)} ${units[i]}`;
  326. }
  327. // ============ 终端颜色 ============
  328. const RED = ‘\x1b[31m’;
  329. const c = {
  330.   reset: ‘\x1b[0m’,
  331.   bold: ‘\x1b[1m’,
  332.   dim: ‘\x1b[2m’,
  333.   blue: ‘\x1b[34m’,
  334.   green: ‘\x1b[32m’,
  335.   yellow: ‘\x1b[33m’,
  336.   gray: ‘\x1b[90m’,
  337.   cyan: ‘\x1b[36m’,
  338. };
  339. function supportsColor() {
  340.   return process.stdout.isTTY && process.env.TERM !== ‘dumb’;
  341. }
  342. function paint(str, color) {
  343.   return supportsColor() ? `${color}${str}${c.reset}` : str;
  344. }
  345. function renderColor(node, { expandAll, depthLimit }) {
  346.   const lines = [];
  347.   function walk(children, prefix, isTop, currentDepth) {
  348.     for (let i = 0; i < children.length; i++) {
  349.       const child = children[i];
  350.       const isLast = i === children.length – 1;
  351.       const branch = isTop ? ” : isLast ? ‘└── ‘ : ‘├── ‘;
  352.       let name = child.name;
  353.       if (child.isFolder) name = paint(name + ‘/’, c.yellow);
  354.       else name = paint(name, c.reset);
  355.       const sz = formatSize(child.size);
  356.       const sizeStr = sz ? paint(`  (${sz})`, c.gray) : ”;
  357.       lines.push(prefix + branch + name + sizeStr);
  358.       if (child.isFolder && Object.keys(child.children).length > 0) {
  359.         const shouldExpand = expandAll || currentDepth < depthLimit;
  360.         if (shouldExpand) {
  361.           walk(
  362.             sortChildren(child),
  363.             prefix + (isTop ? ” : isLast ? ‘    ‘ : ‘│   ‘),
  364.             false,
  365.             currentDepth + 1
  366.           );
  367.         } else {
  368.           const hidden = Object.keys(child.children).length;
  369.           lines.push(
  370.             prefix +
  371.               (isTop ? ” : isLast ? ‘    ‘ : ‘│   ‘) +
  372.               (isLast ? ‘└── ‘ : ‘├── ‘) +
  373.               paint(`… (${hidden} 项,用 –depth 展开)`, c.dim)
  374.           );
  375.         }
  376.       }
  377.     }
  378.   }
  379.   walk(sortChildren(node), ”, true, 1);
  380.   return lines.join(‘\n’);
  381. }
  382. function renderText(node, { expandAll, depthLimit }) {
  383.   const lines = [];
  384.   function walk(children, prefix, isTop, currentDepth) {
  385.     for (let i = 0; i < children.length; i++) {
  386.       const child = children[i];
  387.       const isLast = i === children.length – 1;
  388.       const branch = isTop ? ” : isLast ? ‘└── ‘ : ‘├── ‘;
  389.       let line = prefix + branch + child.name + (child.isFolder ? ‘/’ : ”);
  390.       const sz = formatSize(child.size);
  391.       if (sz) line += `  (${sz})`;
  392.       lines.push(line);
  393.       if (child.isFolder && Object.keys(child.children).length > 0) {
  394.         const shouldExpand = expandAll || currentDepth < depthLimit;
  395.         if (shouldExpand) {
  396.           walk(
  397.             sortChildren(child),
  398.             prefix + (isTop ? ” : isLast ? ‘    ‘ : ‘│   ‘),
  399.             false,
  400.             currentDepth + 1
  401.           );
  402.         }
  403.       }
  404.     }
  405.   }
  406.   walk(sortChildren(node), ”, true, 1);
  407.   return lines.join(‘\n’);
  408. }
  409. // ============ 导出文件 ============
  410. function saveToFile(text, args, pwd_id, rootTitle) {
  411.   let filePath;
  412.   if (args.output) {
  413.     filePath = path.resolve(args.output);
  414.   } else {
  415.     const safeTitle = (rootTitle || pwd_id).replace(/[\\/:*?”<>|]/g, ‘_’).slice(0, 50);
  416.     const date = new Date().toISOString().slice(0, 10);
  417.     const fileName = `quark目录_${safeTitle}_${date}.txt`;
  418.     filePath = path.resolve(process.cwd(), fileName);
  419.   }
  420.   fs.writeFileSync(filePath, text, ‘utf-8’);
  421.   return filePath;
  422. }
  423. // ============ 主流程 ============
  424. async function main() {
  425.   const args = parseArgs(process.argv.slice(2));
  426.   if (args.help || args._.length === 0) {
  427.     printHelp();
  428.     process.exit(args.help ? 0 : 1);
  429.   }
  430.   const url = args._[0];
  431.   const info = parseUrl(url);
  432.   if (!info) {
  433.     console.error(paint(‘&#10007; 无法解析分享链接,请检查是否为 pan.quark.cn/s/xxx 格式’, RED));
  434.     process.exit(1);
  435.   }
  436.   const { pwd_id } = info;
  437.   console.log(paint(‘&#128279; 分享ID: ‘, c.gray) + pwd_id);
  438.   let stoken = args.stoken || info.stoken;
  439.   let rootTitle = ”;
  440.   if (!stoken) {
  441.     console.log(paint(‘&#128273; 正在换取访问凭证 (stoken)…’, c.gray));
  442.     try {
  443.       const result = await getStoken(pwd_id, args.passcode);
  444.       stoken = result.stoken;
  445.       rootTitle = result.title;
  446.       console.log(paint(‘&#10003; 获取成功’, c.gray));
  447.       if (args.passcode) console.log(paint(‘&#128274; 使用提取码: ‘ + args.passcode, c.gray));
  448.     } catch (e) {
  449.       const msg = String(e.message || ”);
  450.       console.error(paint(‘&#10007; 换取 stoken 失败: ‘ + msg, RED));
  451.       if (msg.includes(‘passcode’) || msg.includes(‘提取’) || msg.includes(‘密码’)) {
  452.         console.error(paint(‘  该分享需要提取码,请用 –passcode <code> 指定。’, c.gray));
  453.       } else {
  454.         console.error(
  455.           paint(‘  可尝试用浏览器打开分享页,F12 复制 stoken 后用 –stoken 指定。’, c.gray)
  456.         );
  457.       }
  458.       process.exit(1);
  459.     }
  460.   } else {
  461.     console.log(paint(‘&#128273; 使用指定 stoken’, c.gray));
  462.   }
  463.   const userDepth = Number.isFinite(args.depth) ? args.depth : Infinity;
  464.   console.log(paint(‘&#9203; 正在扫描目录树,请稍候…\n’, c.gray));
  465.   try {
  466.     const items = await scan(pwd_id, stoken, userDepth, args.size);
  467.     const tree = buildTree(items, rootTitle || pwd_id);
  468.     const folderCount = items.filter((i) => i.isFolder).length;
  469.     const fileCount = items.length – folderCount;
  470.     const plainText = renderText(tree, {
  471.       expandAll: args.expand && !Number.isFinite(args.depth),
  472.       depthLimit: args.depth,
  473.     });
  474.     const header = `${url}\n=================================`;
  475.     const summary =
  476.       `\n\n共 ${items.length} 项(文件夹 ${folderCount} / 文件 ${fileCount})` +
  477.       (tree.size ? `\n总计: ${formatSize(tree.size)}` : ”);
  478.     const resultText = `${header}\n${plainText}${summary}`;
  479.     if (args.json) {
  480.       console.log(JSON.stringify(tree, null, 2));
  481.     } else {
  482.       const expandAll = args.expand && !Number.isFinite(args.depth);
  483.       const renderer = supportsColor() ? renderColor : renderText;
  484.       console.log(renderer(tree, { expandAll, depthLimit: args.depth }));
  485.       console.log(”);
  486.       console.log(
  487.         paint(`共 ${items.length} 项(文件夹 ${folderCount} / 文件 ${fileCount})`, c.gray)
  488.       );
  489.       if (tree.size) console.log(paint(`总计: ${formatSize(tree.size)}`, c.gray));
  490.     }
  491.     // ===== 自动保存 txt(–json 时不导出) =====
  492.     if (!args.json) {
  493.       try {
  494.         const savedPath = saveToFile(resultText, args, pwd_id, rootTitle);
  495.         console.log(paint(`\n&#128196; 已保存: ${savedPath}`, c.green));
  496.       } catch (e) {
  497.         console.error(paint(`\n&#10007; 保存文件失败: ${e.message}`, RED));
  498.       }
  499.     }
  500.   } catch (e) {
  501.     console.error(paint(‘\n&#10007; 导出失败: ‘ + e.message, RED));
  502.     process.exit(1);
  503.   }
  504. }
  505. main();


付费下载
当前内容需要支付免费 元宝才能下载
VIP折扣
    折扣详情
  • 体验VIP会员

    免费

  • 月卡VIP会员

    免费

  • 年卡VIP会员

    免费

  • 永久VIP会员

    免费

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

本站所有资源来源于网络,仅限用于学习研究;无任何技术支持!不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除内容。如果您喜欢,请支持正版。如有侵权请邮件与我们联系处理。

常见问题
  • 网盘有时候会因为名字 关键词导致失效 大家可以给管理员提供失效信息,我们会给大家适当积分进行奖励 我们会第一时间进行补充修正 感谢大家的配合 让我们共同努力 打造良好的资源分享平台
查看详情

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务