最近在用 yuque-exporter 把语雀的内容批量导出为本地 Markdown,用来导入到自己的笔记系统。工具本身很好用,但在实际跑的过程中遇到了几个会导致直接崩溃的问题,顺手修了一下,记录在这里。
修复后的版本在:github.com/onecaicai/yuque-exporter
Bug 1:首次运行直接崩溃#
第一次在一台新机器上运行,没有任何历史数据,程序直接报错退出,提示找不到文件。
crawler.ts 中用 fast-glob 去查找已有的 docs-published-at.json:
1
2
| const docsPublishedAtPath = await fg('**/docs-published-at.json', { cwd: metaDir, deep: 3 });
const docsPublishedAtMap = await readJSON(path.join(metaDir, docsPublishedAtPath[0]));
|
首次运行时这个文件根本不存在,docsPublishedAtPath[0] 是 undefined,readJSON 直接抛出异常。
改为直接构造文件路径,用 try/catch 处理文件不存在的情况,初始化为空对象:
1
2
3
4
5
6
7
| const docsPublishedAtPath = path.join(metaDir, namespace, 'docs-published-at.json');
let docsPublishedAtMap: Record<number, string> = {};
try {
docsPublishedAtMap = await readJSON(docsPublishedAtPath);
} catch {
// first crawl for this repo
}
|
同样的问题在 doc.ts 里也存在,原来是在模块顶层直接 await 读文件(ES module 顶层 await),改为懒加载函数,第一次调用时才读:
1
2
3
4
5
6
| async function loadDocsPublishedAtMap() {
if (docsPublishedAtMap) return docsPublishedAtMap;
const docsPublishedAtPath = await fg('**/docs-published-at.json', { cwd: metaDir, deep: 3 });
docsPublishedAtMap = await readJSON(path.join(metaDir, docsPublishedAtPath[0]));
return docsPublishedAtMap;
}
|
Bug 2:本地 doc 文件缺失时不重新爬取#
增量更新逻辑只判断 published_at 是否变化,如果手动删除了某个文档的本地 JSON 缓存,重新运行时这个文档会被跳过,导出目录里缺少对应的 Markdown 文件。
crawler.ts 的过滤逻辑:
1
2
3
| const docChangedList = docList
.filter(doc => typeof docsPublishedAtMap[doc.id] === 'undefined'
|| docsPublishedAtMap[doc.id] !== doc.published_at);
|
只看 published_at 有没有变,不管本地文件是否真的存在。
doc.ts 在构建阶段也没有做文件存在性校验,直接 readJSON 会崩溃。
爬取阶段增加本地文件存在性检查,文件缺失也纳入需要重新爬取的范围:
1
2
3
4
| const docMissing = !(await exists(path.join(metaDir, namespace, 'docs', `${doc.slug}.json`)));
if (publishedChanged || docMissing) {
docChangedList.push(doc);
}
|
构建阶段在读取前先判断文件是否存在,缺失时打印警告跳过,而不是崩溃:
1
2
3
4
| if (!(await exists(docMetaPath))) {
console.warn(`[WARN] skip missing doc: ${doc.namespace}/${doc.url}`);
return null;
}
|
Bug 3:redirect location 为数组时处理出错#
部分语雀分享链接在获取重定向地址时,返回结果错误,生成的 Markdown 链接无法正常访问。
utils.ts 中处理 HTTP 重定向:
1
2
3
4
| const redirectLink = headers.location;
if (!redirectLink) return url;
if (redirectLink[0] === '/') return `${host}${redirectLink}`;
return redirectLink;
|
HTTP 协议里 headers.location 的值可能是数组(undici 返回的类型是 string | string[]),当它是数组时,redirectLink[0] 取到的是字符串的第一个字符而不是数组的第一个元素,判断逻辑完全错误。
1
2
3
| const location = Array.isArray(redirectLink) ? redirectLink[0] : redirectLink;
if (location[0] === '/') return `${host}${location}`;
return location;
|
三个 bug 都是边界情况,原作者可能在自己的环境下没有触发,项目也已经有两年多没有维护了。修复内容已经提交 PR:atian25/yuque-exporter#40。
如果你也在用这个工具导出语雀文档,可以直接用我 fork 后修复的版本:
1
2
3
4
| git clone https://github.com/onecaicai/yuque-exporter.git
cd yuque-exporter
npm install
npx yuque-exporter --token=<your token>
|