Skip to content

Instantly share code, notes, and snippets.

@sorrycc
Created March 6, 2026 04:45
Show Gist options
  • Select an option

  • Save sorrycc/296b442bbe07c6f76442a4fa6233904d to your computer and use it in GitHub Desktop.

Select an option

Save sorrycc/296b442bbe07c6f76442a4fa6233904d to your computer and use it in GitHub Desktop.
语雀文档导出工具
// ==UserScript==
// @name 语雀文档导出工具
// @namespace http://tampermonkey.net/
// @version 0.1.0
// @description 导出语雀文档为 Markdown 文件
// @author sorrycc
// @match https://www.yuque.com/*
// @match https://yuque.antfin.com/*
// @require https://cdn.jsdelivr.net/gh/eligrey/FileSaver.js@v2.0.4/dist/FileSaver.min.js
// @require https://cdn.jsdelivr.net/gh/Stuk/jszip@v3.0.0/dist/jszip.min.js
// @grant none
// ==/UserScript==
(function() {
'use strict';
let keySequence = '';
const TARGET_SEQUENCE = ',d';
// 日志函数
const log = (message, isError = false) => {
const style = isError ? 'color: red' : 'color: green';
console.log(`%c[语雀导出工具] ${message}`, style);
};
// 获取完整路径
const getFullPath = (toc, allTocs) => {
const paths = [];
let currentToc = toc;
while (currentToc.parent_uuid) {
const parent = allTocs.find(t => t.uuid === currentToc.parent_uuid);
if (!parent) break;
paths.unshift(parent.title);
currentToc = parent;
}
return paths;
};
// 导出文档
async function exportDocs() {
try {
// 1. 检查 window.appData
if (!window.appData) {
throw new Error('未找到页面数据,请确保在语雀文档页面执行');
}
const { book, docUrl, group, defaultSpaceHost } = window.appData;
if (!book || !book.toc || !group || !defaultSpaceHost) {
throw new Error('页面数据不完整');
}
let namespace = book.namespace;
if (!namespace) {
const url = new URL(docUrl);
const pathSegments = url.pathname.split('/').filter(Boolean);
namespace = pathSegments.slice(0, 2).join('/');
}
if (!namespace) {
log(`appData: ${window.appData}`);
throw new Error('namespace not found');
}
log('开始导出文档...');
log(`namespace: ${namespace}`);
log(`文档组:${group.name}`);
log(`文档数量:${book.toc.length}`);
// 2. 创建 zip 实例
const zip = new JSZip();
// 3. 遍历获取所有文档
for (const toc of book.toc) {
if (toc.type !== 'DOC' || toc.visible === 0) continue;
try {
// 构建下载 URL
const url = `${defaultSpaceHost}/${namespace}/${toc.url}/markdown?plain=true&linebreak=false&anchor=false`;
// 获取文档内容
log(`正在获取:${toc.title}`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`获取失败:${response.status}`);
}
const markdown = await response.text();
// 构建文件路径
const paths = getFullPath(toc, book.toc);
const filePath = [...paths, `${toc.title}.md`].join('/');
// 添加到 zip
zip.file(filePath, markdown);
log(`已添加:${filePath}`);
} catch (err) {
log(`处理文档 "${toc.title}" 时出错: ${err.message}`, true);
}
}
// 4. 生成并下载 zip
log('正在生成压缩包...');
const content = await zip.generateAsync({type: "blob"});
const fileName = `${group.name}.zip`;
saveAs(content, fileName);
log(`导出完成:${fileName}`);
} catch (err) {
log(`导出失败:${err.message}`, true);
}
}
// 监听键盘事件
document.addEventListener('keypress', (e) => {
keySequence += e.key;
// 只保留最后两个字符
if (keySequence.length > 2) {
keySequence = keySequence.slice(-2);
}
// 检查是否匹配目标序列
if (keySequence === TARGET_SEQUENCE) {
keySequence = '';
exportDocs();
}
});
log('脚本已加载,按 ,d 开始导出');
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment