最近在用 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]undefinedreadJSON 直接抛出异常。

修复

改为直接构造文件路径,用 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>