// ==UserScript==
// @name NBNT: 新版百度网盘共享文件库目录导出工具
// @namespace http://tampermonkey.net/
// @version 0.270
// @description 用于导出百度网盘共享文件库目录和文件列表
// @author UJiN
// @license MIT
// @match https://pan.baidu.com/disk*
// @icon https://nd-static.bdstatic.com/m-static/v20-main/favicon-main.ico
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @require https://unpkg.com/xlsx/dist/xlsx.full.min.js
// ==/UserScript==
(function () {
'use strict';
let directories = []; // 存储解析后的目录数据
let depthSetting = 1; // 默认层数设置
// 添加并发控制池
class RequestPool {
constructor() {
this.maxConcurrent = config.maxConcurrent;
this.currentRequests = 0;
this.queue = [];
this.requestInterval = config.requestInterval;
this.lastRequestTime = 0;
}
async add(fn) {
if (this.currentRequests >= this.maxConcurrent) {
await new Promise(resolve => this.queue.push(resolve));
}
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
if (timeSinceLastRequest < this.requestInterval) {
await new Promise(resolve =>
setTimeout(resolve, this.requestInterval - timeSinceLastRequest)
);
}
this.currentRequests++;
this.lastRequestTime = Date.now();
try {
return await fn();
} finally {
this.currentRequests--;
if (this.queue.length > 0) {
const next = this.queue.shift();
next();
}
}
}
}
// 添加默认配置
const defaultConfig = {
maxConcurrent: 2, // 最大并发请求数
requestInterval: 3000, // 请求间隔(毫秒)
maxRetries: 3, // 最大重试次数
defaultDepth: 1, // 默认获取层数
indentStyle: 'tree' // 目录分级样式:tree(树形)或 tab(制表符)
};
// 获取配置(如果没有则使用默认值)
let config = {
...defaultConfig,
...GM_getValue('nbntConfig', {})
};
// 保存配置
function saveConfig() {
GM_setValue('nbntConfig', config);
}
// 创建配置面板
function createConfigPanel() {
const panel = document.createElement('div');
panel.style.cssText = `
position: fixed;
top: 60px;
right: 60px;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10000;
width: 380px;
display: none;
font-family: "Microsoft YaHei", sans-serif;
`;
// 添加标签页样式
const style = document.createElement('style');
style.textContent = `
.config-tabs {
display: flex;
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
}
.config-tab {
padding: 8px 16px;
cursor: pointer;
color: #666;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.config-tab.active {
color: #06a7ff;
border-bottom-color: #06a7ff;
}
.config-content {
display: none;
}
.config-content.active {
display: block;
}
`;
document.head.appendChild(style);
panel.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0; font-size: 16px; color: #333;">NBNT 配置</h3>
<button id="closeConfig" style="border: none; background: none; cursor: pointer; padding: 4px;">
<i class="u-icon-close" style="font-size: 16px; color: #666;"></i>
</button>
</div>
<div class="config-tabs">
<div class="config-tab active" data-tab="features">功能设置</div>
<div class="config-tab" data-tab="params">参数设置</div>
</div>
<div id="featuresContent" class="config-content active">
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 8px; color: #666;">目录分级样式</label>
<select id="indentStyle" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; outline: none; transition: all 0.3s;">
<option value="tree" ${config.indentStyle === 'tree' ? 'selected' : ''}>树形样式 (├── │ └──)</option>
<option value="tab" ${config.indentStyle === 'tab' ? 'selected' : ''}>制表符 (Tab)</option>
</select>
</div>
<div style="margin-bottom: 20px;">
<label style="display: flex; align-items: center; color: #666; cursor: pointer;">
<input type="checkbox" id="showDirSize" ${config.showDirSize ? 'checked' : ''}
style="margin-right: 8px; width: 16px; height: 16px;">
显示目录大小(仅在导出全部时生效)
</label>
</div>
</div>
<div id="paramsContent" class="config-content">
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 8px; color: #666;">最大并发请求数</label>
<input type="number" id="maxConcurrent" value="${config.maxConcurrent}" min="1" max="5"
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; outline: none; transition: all 0.3s;">
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 8px; color: #666;">请求间隔(毫秒)</label>
<input type="number" id="requestInterval" value="${config.requestInterval}" min="1000" step="500"
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; outline: none; transition: all 0.3s;">
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 8px; color: #666;">最大重试次数</label>
<input type="number" id="maxRetries" value="${config.maxRetries}" min="1" max="5"
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; outline: none; transition: all 0.3s;">
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 8px; color: #666;">默认获取层数</label>
<input type="number" id="defaultDepth" value="${config.defaultDepth}" min="1"
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; outline: none; transition: all 0.3s;">
</div>
</div>
<div style="text-align: right; border-top: 1px solid #eee; padding-top: 16px;">
<button id="saveConfig"
style="padding: 8px 24px; background: #06a7ff; color: white; border: none; border-radius: 4px; cursor: pointer; transition: all 0.3s;">
保存
</button>
</div>
`;
document.body.appendChild(panel);
// 添加标签页切换功能
const tabs = panel.querySelectorAll('.config-tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
const contents = panel.querySelectorAll('.config-content');
contents.forEach(content => content.classList.remove('active'));
panel.querySelector(`#${tab.dataset.tab}Content`).classList.add('active');
});
});
// 添加输入框焦点样式
const inputs = panel.querySelectorAll('input[type="number"]');
inputs.forEach(input => {
input.addEventListener('focus', () => {
input.style.borderColor = '#06a7ff';
input.style.boxShadow = '0 0 0 2px rgba(6,167,255,0.2)';
});
input.addEventListener('blur', () => {
input.style.borderColor = '#ddd';
input.style.boxShadow = 'none';
});
});
// 创建一个通用的关闭面板函数
const closePanel = () => {
// 重置面板内容为当前保存的配置
document.getElementById('maxConcurrent').value = config.maxConcurrent;
document.getElementById('requestInterval').value = config.requestInterval;
document.getElementById('maxRetries').value = config.maxRetries;
document.getElementById('defaultDepth').value = config.defaultDepth;
document.getElementById('showDirSize').checked = config.showDirSize;
document.getElementById('indentStyle').value = config.indentStyle;
panel.style.display = 'none';
const configButton = document.querySelector('#nbnt-config-button');
if (configButton) {
configButton.classList.remove('is-select');
}
};
// 保存配置并关闭面板
const saveAndClose = () => {
config.maxConcurrent = parseInt(document.getElementById('maxConcurrent').value);
config.requestInterval = parseInt(document.getElementById('requestInterval').value);
config.maxRetries = parseInt(document.getElementById('maxRetries').value);
config.defaultDepth = parseInt(document.getElementById('defaultDepth').value);
config.showDirSize = document.getElementById('showDirSize').checked;
config.indentStyle = document.getElementById('indentStyle').value;
saveConfig();
closePanel();
};
// 绑定事件
document.getElementById('saveConfig').onclick = saveAndClose;
document.getElementById('closeConfig').onclick = closePanel;
// 添加点击外部关闭面板
const handleOutsideClick = function(e) {
if (!panel.contains(e.target) && !e.target.closest('#nbnt-config-button')) {
closePanel();
}
};
// 添加事件监听器
document.addEventListener('click', handleOutsideClick);
return {
show: () => panel.style.display = 'block',
hide: closePanel,
destroy: () => {
document.removeEventListener('click', handleOutsideClick);
if (panel.parentNode) {
panel.parentNode.removeChild(panel);
}
}
};
}
// 添加进度条组件
function createProgressBar() {
const progressContainer = document.createElement('div');
progressContainer.id = 'directory-progress';
progressContainer.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 9999;
display: none;
width: 350px;
font-family: "Microsoft YaHei", sans-serif;
`;
const titleDiv = document.createElement('div');
titleDiv.style.cssText = `
font-weight: bold;
margin-bottom: 15px;
color: #333;
font-size: 14px;
`;
titleDiv.textContent = '目录获取进度';
const progressText = document.createElement('div');
progressText.id = 'progress-text';
progressText.style.cssText = `
margin-bottom: 10px;
color: #666;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 280px;
`;
progressText.textContent = '正在获取目录信息...';
const progressBarOuter = document.createElement('div');
progressBarOuter.style.cssText = `
width: 100%;
height: 6px;
background: #f0f0f0;
border-radius: 3px;
overflow: hidden;
`;
const progressBarInner = document.createElement('div');
progressBarInner.id = 'progress-bar';
progressBarInner.style.cssText = `
width: 0%;
height: 100%;
background: linear-gradient(90deg, #2196F3, #00BCD4);
transition: width 0.3s ease;
border-radius: 3px;
`;
progressBarOuter.appendChild(progressBarInner);
progressContainer.appendChild(titleDiv);
progressContainer.appendChild(progressText);
progressContainer.appendChild(progressBarOuter);
document.body.appendChild(progressContainer);
return {
show: () => progressContainer.style.display = 'block',
hide: () => progressContainer.style.display = 'none',
remove: () => {
if (progressContainer.parentNode) {
progressContainer.parentNode.removeChild(progressContainer);
}
},
updateProgress: (current, total) => {
const percentage = Math.min((current / total) * 100, 100);
progressBarInner.style.width = `${percentage}%`;
progressText.textContent = `进度:${current}/${total} (${percentage.toFixed(1)}%)`;
},
updateText: (text) => {
progressText.textContent = text;
}
};
}
// 等待文件库按钮和标题加载,并添加点击事件监听
function waitForLibraryElements() {
let isProcessing = false;
// 检查并添加按钮到操作栏
function checkAndAddOperationButtons() {
if (isProcessing) return;
isProcessing = true;
try {
const actionContainer = document.querySelector('.im-r-contain__actions');
if (!actionContainer) {
isProcessing = false;
return;
}
const operateDiv = document.querySelector('.im-file-nav__operate');
const downloadButton = operateDiv?.querySelector('.u-icon-download')?.closest('button');
if (!operateDiv || !downloadButton) {
isProcessing = false;
return;
}
const existingCheckButton = document.querySelector('#check-dir-button');
const existingFetchButton = document.querySelector('#fetch-dir-button');
const existingConfigButton = document.querySelector('#nbnt-config-button');
// 如果操作按钮已存在则返回
if (existingCheckButton || existingFetchButton) {
isProcessing = false;
return;
}
// 如果配置按钮已存在则移除
if (existingConfigButton) {
existingConfigButton.remove();
if (window.currentConfigPanel) {
window.currentConfigPanel.destroy();
}
}
// 添加样式
const style = document.createElement('style');
style.textContent = `
.export-dropdown {
position: relative;
display: inline-flex;
align-items: center;
cursor: pointer;
height: 24px;
line-height: 24px;
}
.export-dropdown::after {
content: '';
position: absolute;
right: -12px;
top: 6px;
width: 1px;
height: 12px;
background-color: rgb(217, 217, 217);
}
.export-dropdown-menu {
display: none;
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
background: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
padding: 4px;
z-index: 99999;
margin-top: 2px;
border: 1px solid #e8e8e8;
white-space: nowrap;
flex-direction: row;
}
.export-dropdown-menu.show {
display: flex;
}
.export-item {
padding: 4px 12px;
cursor: pointer;
color: #333;
font-size: 12px;
line-height: 1.5;
border-right: 1px solid #e8e8e8;
}
.export-item:last-child {
border-right: none;
}
.export-item:hover {
background: #f5f5f5;
}
`;
document.head.appendChild(style);
const checkButton = document.createElement('button');
checkButton.id = 'check-dir-button';
checkButton.type = 'button';
checkButton.className = 'u-button u-button--default u-button--mini';
checkButton.innerHTML = `
<i class="u-icon-search"></i>
<span>检查目录</span>
`;
const exportDropdown = document.createElement('div');
exportDropdown.className = 'export-dropdown u-button u-button--default u-button--mini';
exportDropdown.innerHTML = `
<i class="u-icon-folder"></i>
<span>导出目录</span>
<i class="u-icon-arrow-down" style="margin-left: 4px;"></i>
<div class="export-dropdown-menu">
<div class="export-item" data-type="txt">导出为TXT</div>
<div class="export-item" data-type="xlsx">导出为Excel</div>
</div>
`;
const fetchAllDropdown = document.createElement('div');
fetchAllDropdown.className = 'export-dropdown u-button u-button--default u-button--mini';
fetchAllDropdown.innerHTML = `
<i class="u-icon-download-bold"></i>
<span>导出全部</span>
<i class="u-icon-arrow-down" style="margin-left: 4px;"></i>
<div class="export-dropdown-menu">
<div class="export-item" data-type="txt">导出为TXT</div>
<div class="export-item" data-type="xlsx">导出为Excel</div>
</div>
`;
// 创建配置按钮
const configButton = document.createElement('div');
configButton.id = 'nbnt-config-button';
configButton.innerHTML = `
<span class="u-tooltip u-uicon is-hover" tabindex="0" aria-describedby="nbnt-config-tooltip">
<i class="u-uicon__font u-icon-setting" style="color: #666;"></i>
</span>
<div class="u-tooltip__popper" id="nbnt-config-tooltip">NBNT 配置</div>
`;
configButton.style.cssText = `
display: inline-flex;
margin: 40px 8px;
cursor: pointer;
position: relative;
`;
// 添加悬停提示样式
const tooltipStyle = document.createElement('style');
tooltipStyle.textContent = `
.u-tooltip__popper {
display: none;
position: absolute;
background: rgba(51, 51, 51, 0.9);
color: #fff;
padding: 5px 12px;
border-radius: 4px;
font-size: 12px;
line-height: 1.6;
white-space: nowrap;
top: 50%;
right: 100%;
transform: translateY(-50%);
margin-right: 8px;
z-index: 10001;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.u-tooltip__popper::after {
content: '';
position: absolute;
right: -4px;
top: 50%;
transform: translateY(-50%);
border-width: 4px;
border-style: solid;
border-color: transparent transparent transparent rgba(51, 51, 51, 0.9);
}
#nbnt-config-button:hover .u-tooltip__popper {
display: block;
}
#nbnt-config-button.is-select i {
color: #06a7ff !important;
}
`;
document.head.appendChild(tooltipStyle);
const configPanel = createConfigPanel();
window.currentConfigPanel = configPanel;
configButton.onclick = () => {
const isActive = configButton.classList.contains('is-select');
if (isActive) {
configButton.classList.remove('is-select');
configPanel.hide();
} else {
configButton.classList.add('is-select');
configPanel.show();
}
};
// 将配置按钮添加到操作区域
actionContainer.appendChild(configButton);
checkButton.onclick = function() {
const selected = getSelectedDirectory();
if (!selected) {
alert('请选中一个目录!');
return;
}
const { dirInfo, title } = selected;
checkDirectoryInfo(dirInfo.msg_id, title);
};
// 处理导出选项点击
exportDropdown.addEventListener('click', function(e) {
e.stopPropagation();
const menu = this.querySelector('.export-dropdown-menu');
menu.classList.toggle('show');
});
// 点击其他地方关闭菜单
document.addEventListener('click', function() {
const menus = document.querySelectorAll('.export-dropdown-menu');
menus.forEach(menu => menu.classList.remove('show'));
});
// 防止菜单项点击事件冒泡
exportDropdown.querySelector('.export-dropdown-menu').addEventListener('click', function(e) {
e.stopPropagation();
});
exportDropdown.querySelector('.export-dropdown-menu').addEventListener('click', async function(e) {
const exportType = e.target.dataset.type;
if (!exportType) return;
try {
const selected = getSelectedDirectory();
if (!selected) {
alert('请选中一个目录!');
return;
}
const { dirInfo, title } = selected;
console.log("选中的目录信息:", dirInfo);
const uk = dirInfo.uk;
const fsId = dirInfo.fs_id;
const gid = dirInfo.group_id;
const msgId = dirInfo.msg_id;
depthSetting = parseInt(prompt("请输入要获取的子目录层数:", config.defaultDepth), 10);
if (isNaN(depthSetting) || depthSetting < 1) {
alert("请输入有效的层数!");
return;
}
const result = await fetchSubdirectories(uk, msgId, fsId, gid, title, depthSetting);
if (!window.cancelOperation && result) {
if (exportType === 'txt') {
const formattedContent = formatDirectoryTree(result.tree);
saveAsTxt(formattedContent, title);
} else if (exportType === 'xlsx') {
saveAsExcel(result, title);
}
}
} finally {
cleanup();
}
});
// 处理导出全部按钮的点击事件
fetchAllDropdown.addEventListener('click', function(e) {
e.stopPropagation();
const menu = this.querySelector('.export-dropdown-menu');
menu.classList.toggle('show');
});
// 防止菜单项点击事件冒泡
fetchAllDropdown.querySelector('.export-dropdown-menu').addEventListener('click', function(e) {
e.stopPropagation();
});
// 修改导出全部选项的点击处理
fetchAllDropdown.querySelector('.export-dropdown-menu').addEventListener('click', async function(e) {
const exportType = e.target.dataset.type;
if (!exportType) return;
try {
const selected = getSelectedDirectory();
if (!selected) {
alert('请选中一个目录!');
return;
}
const { dirInfo, title } = selected;
console.log("选中的目录信息:", dirInfo);
const uk = dirInfo.uk;
const fsId = dirInfo.fs_id;
const gid = dirInfo.group_id;
const msgId = dirInfo.msg_id;
depthSetting = parseInt(prompt("请输入要获取的层数:", config.defaultDepth), 10);
if (isNaN(depthSetting) || depthSetting < 1) {
alert("请输入有效的层数!");
return;
}
const result = await fetchAllContent(uk, msgId, fsId, gid, title, depthSetting);
if (!window.cancelOperation && result) {
if (exportType === 'txt') {
// TXT 导出时进行格式化
const formattedContent = formatAllContent(result.tree);
saveAsTxt(formattedContent, title + "_完整");
} else if (exportType === 'xlsx') {
// Excel 导出使用原始数据结构
saveAsExcel(result, title + "_完整");
}
}
} finally {
cleanup();
}
});
// 修改按钮插入顺序
requestAnimationFrame(() => {
downloadButton.after(fetchAllDropdown);
downloadButton.after(document.createTextNode(' ')); // 添加空格
downloadButton.after(exportDropdown);
downloadButton.after(document.createTextNode(' ')); // 添加空格
downloadButton.after(checkButton);
});
} finally {
isProcessing = false;
}
}
// 创建一个防抖函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 使用防抖包装检查函数
const debouncedCheck = debounce(checkAndAddOperationButtons, 200);
// 修改 MutationObserver 的配置
const observer = new MutationObserver((mutations) => {
// 只在有相关变化时触发检查
const hasRelevantChanges = mutations.some(mutation => {
return mutation.addedNodes.length > 0 &&
Array.from(mutation.addedNodes).some(node => {
return node.classList?.contains('im-file-nav__operate') ||
node.querySelector?.('.im-file-nav__operate');
});
});
if (hasRelevantChanges) {
debouncedCheck();
}
});
// 使用更具体的观察配置
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
// 初始检查
checkAndAddOperationButtons();
// 拦截请求(只需要执行一次)
interceptNetworkRequests();
}
// 获取当前选中的目录
function getSelectedDirectory() {
// 同时支持根目录和子目录的选择器
const selectedDirs = document.querySelectorAll('.im-pan-table__body-row.selected, .im-pan-list__item.selected');
if (selectedDirs.length !== 1) return null;
const selectedDir = selectedDirs[0];
const title = selectedDir.querySelector('.im-pan-list__file-name-title-text')?.innerText;
if (!title) return null;
// 在 directories 中查找匹配的记录
const matchedDir = directories.find(dir => dir.server_filename === title);
if (!matchedDir) {
console.error(`未找到目录 "${title}" 的记录`);
return null;
}
return {
element: selectedDir,
title: title,
dirInfo: matchedDir
};
}
// 检查目录信息并显示相关信息
function checkDirectoryInfo(msgId, title) {
console.log(`检查目录: ${title}, msgId: ${msgId}`);
const matchedDir = directories.find(dir => dir.msg_id === msgId);
console.log("当前目录数据:", directories);
if (matchedDir) {
alert(`匹配到目录: ${title}\nfs_id: ${matchedDir.fs_id}\ngroup_id: ${matchedDir.group_id}\nuk: ${matchedDir.uk}`);
console.log("匹配的目录信息:", matchedDir);
} else {
alert(`未找到与目录 "${title}" 匹配的记录。`);
}
}
// 拦截 XMLHttpRequest 请求
function interceptNetworkRequests() {
const originalOpen = XMLHttpRequest.prototype.open; // 保存原始 XMLHttpRequest.open
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
if (url.includes('mbox/group/listshare')) {
console.log("准备拦截 XMLHttpRequest 请求:", url);
this.addEventListener('load', function () {
try {
const data = this.responseType === 'json' ? this.response : JSON.parse(this.responseText);
console.debug("完整的响应数据:", data); // 调试输出完整数据
processLibraryData(data);
} catch (e) {
console.error("解析响应失败:", e);
}
});
}
// 拦截进入目录的请求
if (url.includes('mbox/msg/shareinfo')) {
console.log("准备拦截进入目录的 XMLHttpRequest 请求:", url);
this.addEventListener('load', function () {
try {
const data = this.responseType === 'json' ? this.response : JSON.parse(this.responseText);
console.debug("完整的响应数据:", data); // 调试输出完整数据
processDirectoryData(data);
} catch (e) {
console.error("解析响应失败:", e);
}
});
}
return originalOpen.apply(this, [method, url, ...rest]);
};
}
// 处理文件库数据:取需要的信息并存储
function processLibraryData(data) {
if (!data || data.errno !== 0) {
console.error("文件库数据获取失败,错误码:", data?.errno);
return;
}
// 在获取新的文件库数据时才清空旧数据
directories = [];
const msgList = data.records?.msg_list || [];
msgList.forEach((msg, index) => {
const group_id = msg.group_id;
const uk = msg.uk;
msg.file_list.forEach(file => {
if (parseInt(file.isdir) === 1) {
directories.push({
fs_id: file.fs_id,
server_filename: file.server_filename,
group_id: group_id,
msg_id: msg.msg_id,
uk: uk
});
}
});
});
}
// 处理目录数据:提取需要的信息并存储
function processDirectoryData(data) {
if (!data || data.errno !== 0) {
console.error("目录数据获取失败,错误码:", data?.errno);
return;
}
const records = data.records || [];
records.forEach(record => {
// 保存所有目录信息,包括子目录
if (parseInt(record.isdir) === 1) {
// 处理路径,移除"我的资源"前缀
let processedPath = record.path;
if (processedPath.startsWith('/我的资源/')) {
processedPath = processedPath.substring('/我的资源'.length);
}
// 从处理后的路径中提取各级目录
const pathParts = processedPath.split('/').filter(p => p);
const rootName = pathParts[0];
// 查找根目录信息
const rootDir = directories.find(d => d.server_filename === rootName);
if (rootDir) {
// 检查是否已存在相同的记录
const existingRecord = directories.find(d => d.fs_id === record.fs_id);
if (!existingRecord) {
// 构建完整的目录信息
const dirInfo = {
fs_id: record.fs_id,
server_filename: record.server_filename,
path: processedPath,
group_id: rootDir.group_id,
msg_id: rootDir.msg_id,
uk: rootDir.uk,
parent_path: pathParts.slice(0, -1).join('/'),
level: pathParts.length - 1 // 添加层级信息
};
// 添加到目录列表
directories.push(dirInfo);
}
} else {
// 如果是根目录级别的分享,直接添加
if (pathParts.length === 1) {
const dirInfo = {
fs_id: record.fs_id,
server_filename: record.server_filename,
path: processedPath,
group_id: record.group_id,
msg_id: record.msg_id,
uk: record.uk,
level: 0
};
directories.push(dirInfo);
}
}
}
});
// 按层级排序,方便调试查看
directories.sort((a, b) => (a.level || 0) - (b.level || 0));
}
// 统一的获取内容函数
async function fetchContent(uk, msgId, fsId, gid, title, depth, fetchMode = 'directory') {
console.log(`开始获取内容: ${title}, 深度: ${depth}, 模式: ${fetchMode}`);
const startTime = performance.now();
const progressBar = createProgressBar();
progressBar.show();
let result = {
name: title,
children: [],
level: 0,
isRoot: true,
isDir: true,
startTime: startTime
};
let totalItems = 0;
let processedItems = 0;
async function fetchItems(parentDir, currentDepth) {
if (currentDepth >= depth) return;
let page = 1;
let hasMore = true;
const allRecords = [];
const maxRetries = config.maxRetries;
const requestPool = new RequestPool();
while (hasMore) {
progressBar.updateText(`正在获取 "${parentDir.name}" 的第 ${page} 页数据...`);
console.log(`[${parentDir.name}] 正在获取第 ${page} 页数据...`);
const url = `https://pan.baidu.com/mbox/msg/shareinfo?from_uk=${encodeURIComponent(uk)}&msg_id=${encodeURIComponent(msgId)}&type=2&num=100&page=${page}&fs_id=${encodeURIComponent(parentDir.fs_id || fsId)}&gid=${encodeURIComponent(gid)}&limit=100&desc=1&clienttype=0&app_id=250528&web=1`;
let retryCount = 0;
let success = false;
while (retryCount < maxRetries && !success) {
try {
const data = await requestPool.add(async () => {
const response = await fetch(url, {
timeout: 30000,
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
});
if (data.errno !== 0) {
throw new Error(`API error: ${data.errno}`);
}
// 根据模式过滤记录
const filteredRecords = fetchMode === 'directory'
? data.records.filter(record => parseInt(record.isdir) === 1)
: data.records;
allRecords.push(...filteredRecords);
hasMore = data.has_more === 1;
success = true;
console.log(`[${parentDir.name}] 第 ${page} 页获取成功,本页记录数: ${filteredRecords.length},hasMore: ${hasMore}`);
} catch (error) {
retryCount++;
console.error(`[${parentDir.name}] 页面 ${page} 获取失败 (${retryCount}/${maxRetries}):`, error);
if (retryCount < maxRetries) {
const delay = Math.min(1000 * Math.pow(2, retryCount), 10000);
progressBar.updateText(`请求失败,${delay/1000}秒后重试...`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
progressBar.updateText(`获取 "${parentDir.name}" 第 ${page} 页失败,跳过...`);
hasMore = false;
}
}
}
if (success) {
page++;
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
totalItems += allRecords.length;
const promises = allRecords.map(async record => {
const childItem = {
name: record.server_filename,
fs_id: record.fs_id,
isDir: parseInt(record.isdir) === 1,
size: record.size,
children: [],
level: currentDepth + 1,
parentLevel: currentDepth
};
parentDir.children.push(childItem);
if (childItem.isDir && currentDepth + 1 < depth) {
await fetchItems(childItem, currentDepth + 1);
}
processedItems++;
progressBar.updateProgress(processedItems, totalItems);
});
await Promise.all(promises);
}
try {
await fetchItems(result, 0);
progressBar.updateText('内容获取完成!');
setTimeout(() => progressBar.hide(), 2000);
return {
tree: result,
startTime: startTime
};
} finally {
progressBar.remove();
result = null;
cleanup();
}
}
// 获取子目录信息
async function fetchSubdirectories(uk, msgId, fsId, gid, title, depth) {
return fetchContent(uk, msgId, fsId, gid, title, depth, 'directory');
}
// 添加清理文件名的函数
function cleanFileName(name) {
// 移除零宽空格和其他不可见字符
return name.replace(/[\u200b\u200c\u200d\u200e\u200f\ufeff]/g, '');
}
// 添加通用的排序函数
function sortTreeNodes(node) {
if (node.children && node.children.length > 0) {
node.children.sort((a, b) => {
const numA = extractNumber(a.name);
const numB = extractNumber(b.name);
if (numA !== numB) {
return numA - numB;
}
return a.name.localeCompare(b.name, 'zh-CN');
});
node.children.forEach(sortTreeNodes);
}
}
function extractNumber(str) {
const match = str.match(/^(\d+)\./);
return match ? parseInt(match[1]) : Infinity;
}
function formatDirectoryTree(dir) {
const formatStartTime = performance.now();
// 在格式化之前先排序
sortTreeNodes(dir);
const SYMBOLS = config.indentStyle === 'tree' ? {
space: ' ',
branch: '│ ',
tee: '├──',
last: '└──'
} : {
space: '\t',
branch: '\t',
tee: '',
last: ''
};
let result = '';
const currentTime = new Date().toLocaleString();
// 添加标题和信息头
result += `目录结构导出清单\n`;
result += `导出时间:${currentTime}\n`;
result += `根目录:${dir.name}\n`;
result += `${'='.repeat(50)}\n\n`;
// 内部函数,用于格式化目录
function formatDir(node, prefix = '', isLastArray = []) {
if (node.isRoot) {
result += `${cleanFileName(node.name)}\n`;
if (node.children && node.children.length > 0) {
node.children.forEach((child, index) => {
const isLast = index === node.children.length - 1;
formatDir(child, SYMBOLS.space, [isLast]);
});
}
} else {
const connector = config.indentStyle === 'tree'
? (isLastArray[isLastArray.length - 1] ? SYMBOLS.last : SYMBOLS.tee)
: '';
result += `${prefix}${connector}${cleanFileName(node.name)}\n`;
if (node.children && node.children.length > 0) {
node.children.forEach((child, index) => {
const isLast = index === node.children.length - 1;
const newPrefix = prefix + (isLastArray[isLastArray.length - 1] ? SYMBOLS.space : SYMBOLS.branch);
formatDir(child, newPrefix, [...isLastArray, isLast]);
});
}
}
}
formatDir(dir, '', []);
const endTime = performance.now();
const formatTime = ((endTime - formatStartTime) / 1000).toFixed(2); // 格式化耗时
const totalTime = ((endTime - (dir.startTime || formatStartTime)) / 1000).toFixed(2); // 总耗时
// 添加页脚和统计信息
result += `\n${'='.repeat(50)}\n`;
result += `统计信息:\n`;
result += `目录数量:${countDirectories(dir)} 个\n`;
result += `格式化耗时:${formatTime} 秒\n`;
if (dir.startTime) {
result += `总处理耗时:${totalTime} 秒\n`;
}
return result;
}
// 添加统计目录数量的辅助函数
function countDirectories(dir) {
let count = 0;
function traverse(node) {
if (node.children && node.children.length > 0) {
count += node.children.length;
node.children.forEach(traverse);
}
}
traverse(dir);
return count;
}
// 保存为 TXT 文件
function saveAsTxt(content, title) {
const blob = new Blob([content], { type: 'text/plain' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `${title}.txt`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
console.log(`已保存文件: ${title}.txt`);
}
// 添加获取全部内容的函数
async function fetchAllContent(uk, msgId, fsId, gid, title, depth) {
return fetchContent(uk, msgId, fsId, gid, title, depth, 'all');
}
function formatAllContent(dir) {
const formatStartTime = performance.now();
// 在格式化之前先排序
sortTreeNodes(dir);
let result = '';
const currentTime = new Date().toLocaleString();
const SYMBOLS = config.indentStyle === 'tree' ? {
space: ' ',
branch: '│ ',
tee: '├──',
last: '└──'
} : {
space: '\t',
branch: '\t',
tee: '',
last: ''
};
result += `完整目录结构导出清单\n`;
result += `导出时间:${currentTime}\n`;
result += `根目录:${dir.name}\n`;
result += `${'='.repeat(50)}\n\n`;
let fileCount = 0;
let dirCount = 0;
let totalSize = 0;
// 计算每个目录的总大小
function calculateDirSize(node) {
let size = 0;
if (!node.isDir) {
return parseInt(node.size) || 0;
}
if (node.children && node.children.length > 0) {
node.children.forEach(child => {
size += calculateDirSize(child);
});
}
node.totalSize = size;
return size;
}
calculateDirSize(dir);
function formatItem(node, prefix = '', isLastArray = []) {
if (node.isRoot) {
const sizeStr = config.showDirSize ? ` (${formatSize(node.totalSize || 0)})` : '';
result += `${cleanFileName(node.name)}/${sizeStr}\n`;
if (node.children && node.children.length > 0) {
node.children.forEach((child, index) => {
const isLast = index === node.children.length - 1;
formatItem(child, SYMBOLS.space, [isLast]);
});
}
} else {
const connector = config.indentStyle === 'tree'
? (isLastArray[isLastArray.length - 1] ? SYMBOLS.last : SYMBOLS.tee)
: '';
const cleanName = cleanFileName(node.name);
const itemName = node.isDir ? `${cleanName}/` : cleanName;
let sizeStr = '';
if (node.isDir) {
sizeStr = config.showDirSize ? ` (${formatSize(node.totalSize || 0)})` : '';
} else {
sizeStr = ` (${formatSize(parseInt(node.size) || 0)})`;
}
result += `${prefix}${connector}${itemName}${sizeStr}\n`;
if (node.isDir) {
dirCount++;
} else {
fileCount++;
totalSize += parseInt(node.size) || 0;
}
if (node.children && node.children.length > 0) {
node.children.forEach((child, index) => {
const isLast = index === node.children.length - 1;
const newPrefix = prefix + (isLastArray[isLastArray.length - 1] ? SYMBOLS.space : SYMBOLS.branch);
formatItem(child, newPrefix, [...isLastArray, isLast]);
});
}
}
}
formatItem(dir, '', []);
const endTime = performance.now();
const formatTime = ((endTime - formatStartTime) / 1000).toFixed(2);
const totalTime = ((endTime - dir.startTime) / 1000).toFixed(2);
result += `\n${'='.repeat(50)}\n`;
result += `统计信息:\n`;
result += `目录数量:${dirCount}\n`;
result += `文件数量:${fileCount}\n`;
result += `文件大小:${formatSize(totalSize)}\n`;
result += `处理总计:${dirCount + fileCount} 个项目\n`;
result += `格式化耗时:${formatTime} 秒\n`;
result += `总处理耗时:${totalTime} 秒\n`;
return result;
}
function formatSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function cleanup() {
const progressBar = document.getElementById('directory-progress');
if (progressBar && progressBar.parentNode) {
progressBar.parentNode.removeChild(progressBar);
}
}
function saveAsExcel(data, title) {
const wb = XLSX.utils.book_new();
const excelData = [];
excelData.push(['目录结构导出清单']);
excelData.push([`导出时间: ${new Date().toLocaleString()}`]);
let fileCount = 0;
let dirCount = 0;
let totalSize = 0;
function countItems(node) {
if (!node.isRoot) {
if (node.isDir) {
dirCount++;
} else {
fileCount++;
totalSize += node.size || 0;
}
}
if (node.children && node.children.length > 0) {
node.children.forEach(countItems);
}
}
countItems(data.tree);
excelData.push(['统计信息']);
excelData.push([`目录数量: ${dirCount}`]);
if (fileCount > 0) {
excelData.push([`文件数量: ${fileCount}`]);
excelData.push([`文件大小: ${formatSize(totalSize)}`]);
excelData.push([`处理总计: ${dirCount + fileCount} 个项目`]);
}
excelData.push([`格式化耗时: ${((performance.now() - data.startTime) / 1000).toFixed(2)} 秒`]);
excelData.push(['']);
function getMaxDepth(node, currentDepth = 0) {
if (!node.children || node.children.length === 0) {
return currentDepth;
}
return Math.max(...node.children.map(child =>
getMaxDepth(child, currentDepth + 1)
));
}
const actualDepth = Math.min(depthSetting, getMaxDepth(data.tree) + 1);
const headers = [];
for (let i = 1; i <= actualDepth; i++) {
headers.push(`${i}级目录`);
}
excelData.push(headers);
const allRows = [];
function processNode(node, level = 0, parentRow = []) {
if (level >= actualDepth) return;
const currentRow = [...parentRow];
if (!node.isRoot) {
currentRow[level] = node.name;
allRows.push([...currentRow]);
}
if (node.children && node.children.length > 0) {
if (node.isRoot) {
sortTreeNodes(node);
node.children.forEach(child => {
const newRow = new Array(actualDepth).fill('');
newRow[0] = child.name;
allRows.push([...newRow]);
if (child.children && child.children.length > 0) {
child.children.forEach(grandChild => {
processNode(grandChild, 1, newRow);
});
}
});
} else {
node.children.forEach(child => {
processNode(child, level + 1, currentRow);
});
}
}
}
processNode(data.tree, 0, new Array(actualDepth).fill(''));
excelData.push(...allRows);
const ws = XLSX.utils.aoa_to_sheet(excelData);
const colWidths = [];
for (let i = 0; i < actualDepth; i++) {
colWidths.push({ wch: 45 });
}
ws['!cols'] = colWidths;
ws['!merges'] = [
{ s: { r: 0, c: 0 }, e: { r: 0, c: actualDepth - 1 } }, // 标题行
{ s: { r: 1, c: 0 }, e: { r: 1, c: actualDepth - 1 } }, // 时间行
{ s: { r: 2, c: 0 }, e: { r: 2, c: actualDepth - 1 } }, // 统计信息标题
{ s: { r: 3, c: 0 }, e: { r: 3, c: actualDepth - 1 } }, // 目录数量
];
if (fileCount > 0) {
ws['!merges'].push(
{ s: { r: 4, c: 0 }, e: { r: 4, c: actualDepth - 1 } }, // 文件数量
{ s: { r: 5, c: 0 }, e: { r: 5, c: actualDepth - 1 } }, // 文件大小
{ s: { r: 6, c: 0 }, e: { r: 6, c: actualDepth - 1 } }, // 处理总计
{ s: { r: 7, c: 0 }, e: { r: 7, c: actualDepth - 1 } }, // 格式化耗时
{ s: { r: 8, c: 0 }, e: { r: 8, c: actualDepth - 1 } } // 空行
);
} else {
ws['!merges'].push(
{ s: { r: 4, c: 0 }, e: { r: 4, c: actualDepth - 1 } }, // 格式化耗时
{ s: { r: 5, c: 0 }, e: { r: 5, c: actualDepth - 1 } } // 空行
);
}
XLSX.utils.book_append_sheet(wb, ws, '目录结构');
XLSX.writeFile(wb, `${title}.xlsx`);
}
waitForLibraryElements();
})();