Files
YTScraper/feishu-to-md.mjs
T
2026-07-05 13:22:25 +08:00

126 lines
5.7 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 飞书文档 → Markdown 转换工具(含图片本地化)
*
* 流程:
* 1. wiki +node-get → 解析 obj_token
* 2. drive +export markdown → 拿到含 feishu.cn/file/<token> 的 Markdown
* 3. docs +media-preview → 逐张下载图片到本地
* 4. 替换图片链接为相对路径,保存最终 Markdown
*
* 用法:node feishu-to-md.mjs <wiki_url_or_token> [输出目录]
*/
import { execSync } from 'child_process';
import { writeFileSync, readFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
import { resolve, join, basename } from 'path';
const argv = process.argv.slice(2);
if (!argv[0] || argv[0] === '--help' || argv[0] === '-h') {
console.log(`
用法:node feishu-to-md.mjs <wiki_url_or_token> [输出目录]
示例:
node feishu-to-md.mjs https://yitanger.feishu.cn/wiki/R9NywV2i9izjuqkPd63cWuafn4g
node feishu-to-md.mjs R9NywV2i9izjuqkPd63cWuafn4g
`);
process.exit(0);
}
const input = argv[0].trim();
const outDir = argv[1] ? resolve(argv[1]) : resolve('./output');
const wikiUrl = input.startsWith('http') ? input : `https://yitanger.feishu.cn/wiki/${input}`;
// lark-cli 命令必须在 outDir 下执行(+media-preview 只接受相对路径)
process.chdir(outDir);
console.log('╔════════════════════════════════════════════╗');
console.log('║ 飞书文档 → Markdown 转换工具 ║');
console.log('╚════════════════════════════════════════════╝');
console.log(`📎 URL: ${wikiUrl}\n`);
// ── 1. 解析 wiki node → obj_token ────────────────────────────────────────────
console.log('🔍 正在获取 wiki 节点信息...');
const nodeInfo = larkJson(`lark-cli wiki +node-get --node-token "${wikiUrl}"`);
if (!nodeInfo.ok) die('获取 wiki 节点失败', nodeInfo);
const { obj_token, title } = nodeInfo.data;
console.log(`✅ 标题:${title}`);
console.log(`✅ obj_token${obj_token}\n`);
const safeTitle = title.replace(/[/\\:*?"<>|]/g, '_').trim();
const imgDir = join(outDir, `${safeTitle}_files`);
if (!existsSync(imgDir)) mkdirSync(imgDir, { recursive: true });
// ── 2. 导出 Markdown ──────────────────────────────────────────────────────────
console.log('📄 正在导出 Markdown...');
const exportResult = larkJson(
`lark-cli drive +export --token ${obj_token} --doc-type docx --file-extension markdown --output-dir . --overwrite`
);
if (!exportResult.ok) die('导出失败', exportResult);
const mdPath = exportResult.data.saved_path;
let mdContent = readFileSync(mdPath, 'utf-8');
console.log(`✅ 导出成功(${mdContent.length} 字符)\n`);
// ── 3. 下载图片 ────────────────────────────────────────────────────────────────
const TOKEN_RE = /!\[[^\]]*\]\(https:\/\/feishu\.cn\/file\/([A-Za-z0-9]+)\)/g;
const tokens = [...new Set([...mdContent.matchAll(TOKEN_RE)].map(m => m[1]))];
console.log(`🖼️ 发现 ${tokens.length} 张图片,开始下载...`);
const tokenToLocal = new Map();
let n = 0;
for (const token of tokens) {
n++;
const relPath = `${safeTitle}_files/image_${n}.png`;
try {
execSync(`lark-cli docs +media-preview --token ${token} --output "${relPath}" --overwrite`, {
stdio: 'ignore',
});
tokenToLocal.set(token, relPath);
console.log(` ✓ image_${n}.png`);
} catch {
tokenToLocal.set(token, `https://feishu.cn/file/${token}`);
console.log(` ✗ image_${n}.png(下载失败,保留原链接)`);
}
}
console.log('');
// ── 4. 替换图片链接并清理 ─────────────────────────────────────────────────────
mdContent = mdContent.replace(
/!\[([^\]]*)\]\(https:\/\/feishu\.cn\/file\/([A-Za-z0-9]+)\)/g,
(_, alt, token) => `![${alt}](${tokenToLocal.get(token) ?? `https://feishu.cn/file/${token}`})`
);
// 清理 <title> 标签和 <grid>/<column> 残留
mdContent = mdContent
.replace(/<title>[\s\S]*?<\/title>\n?/g, '')
.replace(/<\/?grid[^>]*>\n?/g, '')
.replace(/<\/?column[^>]*>\n?/g, '')
.replace(/\n{3,}/g, '\n\n')
.trim();
// 加首行标题和来源
const finalContent = `# ${title}\n\n> Source: ${wikiUrl}\n\n${mdContent}`;
writeFileSync(mdPath, finalContent, 'utf-8');
console.log('╔════════════════════════════════════════════╗');
console.log(`║ ✅ 完成!共 ${finalContent.length} 字符`);
console.log(`║ 📄 ${mdPath}`);
console.log(`║ 🖼️ ${imgDir} (${tokens.length} 张)`);
console.log('╚════════════════════════════════════════════╝');
// ── helpers ──────────────────────────────────────────────────────────────────
function larkJson(cmd) {
try {
return JSON.parse(execSync(cmd, { encoding: 'utf-8' }));
} catch (e) {
// lark-cli 失败时把 JSON 输出到 stderr
try { return JSON.parse(e.stderr || e.stdout || '{}'); } catch { return {}; }
}
}
function die(msg, info) {
console.error(`❌ ${msg}:`, info?.error?.message || JSON.stringify(info));
process.exit(1);
}