Second commit

This commit is contained in:
beyondx123
2026-07-05 13:22:25 +08:00
parent 991eec8cef
commit 09c79b677b
8 changed files with 6944 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
#!/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);
}