#!/usr/bin/env node /** * Feishu Wiki 文档获取工具 * * 用法:node fetch-feishu-wiki.mjs * * 图片下载:使用 lark-cli docs +media-preview --as user 逐张下载 */ import { execSync } from 'child_process'; import { writeFileSync, existsSync, mkdirSync, readFileSync, renameSync } from 'fs'; import { resolve } from 'path'; // ── CLI ──────────────────────────────────────────────────────────────────────── const args = process.argv.slice(2); if (!args[0] || args[0] === '--help' || args[0] === '-h') { console.log('用法:node fetch-feishu-wiki.mjs '); process.exit(0); } const WIKI_URL = args[0].trim(); // ── 1. 获取 wiki 节点 ───────────────────────────────────────────────────────── console.log('🔍 正在获取 wiki 节点信息...'); let docToken, docTitle; try { const r = execSync(`lark-cli wiki +node-get --as bot --node-token ${WIKI_URL}`, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 }); const { ok, data, error } = JSON.parse(r); if (!ok) { console.error('❌', error.message); process.exit(1); } docToken = data.obj_token; docTitle = (data.title || 'untitled').trim(); } catch (e) { console.error('❌ 获取 wiki 节点失败:', e.message); process.exit(1); } console.log(`📄 文档标题: ${docTitle}`); console.log(`🔑 文档 token: ${docToken}`); // ── 2. 获取 Markdown ────────────────────────────────────────────────────────── console.log('📥 正在获取文档内容...'); let markdown; try { const r = execSync( `lark-cli docs +fetch --as bot --api-version v2 --doc ${docToken} --doc-format markdown`, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 }); const { ok, data, error } = JSON.parse(r); if (!ok) { console.error('❌', error.message); process.exit(1); } markdown = data.document.content; } catch (e) { console.error('❌ 获取文档内容失败:', e.message); process.exit(1); } console.log(`✅ Markdown 获取成功,共 ${markdown.length} 字符`); // ── 3. 提取图片 ─────────────────────────────────────────────────────────────── const imageRegex = /!\[([^\]]*)\]\((https:\/\/feishu\.cn\/file\/([^)]+))\)/g; const images = []; let m; while ((m = imageRegex.exec(markdown)) !== null) { images.push({ alt: m[1], url: m[2], token: m[3] }); } console.log(`🖼️ 找到 ${images.length} 张图片`); // ── 4. 目录准备 ─────────────────────────────────────────────────────────────── const safeTitle = docTitle.replace(/[/\\:*?"<>|]/g, '_').trim(); const outputDir = resolve('output'); const imagesDir = resolve(outputDir, safeTitle); if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true }); if (images.length > 0 && !existsSync(imagesDir)) mkdirSync(imagesDir, { recursive: true }); // ── 5. 下载图片 ─────────────────────────────────────────────────────────────── let updatedMarkdown = markdown; if (images.length > 0) { console.log(' 📥 下载图片中...'); for (let i = 0; i < images.length; i++) { const { token, url } = images[i]; const imageFilename = `${i + 1}_${token}.png`; const imageLocalPath = resolve(imagesDir, imageFilename); const imageRelativePath = `./${safeTitle}/${imageFilename}`; process.stdout.write(` [${i + 1}/${images.length}] ${token.substring(0, 14)}... `); try { // lark-cli docs +media-preview 输出到当前目录 const tmpOutput = `_scraper_tmp_${i}.png`; const r = execSync( `lark-cli docs +media-preview --as user --token ${token} --output ./${tmpOutput} --overwrite`, { encoding: 'utf-8', maxBuffer: 5 * 1024 * 1024, timeout: 30000 } ); const parsed = JSON.parse(r); if (parsed.ok) { // 将临时文件移动到目标位置 const tmpPath = resolve(tmpOutput); if (existsSync(tmpPath)) { renameSync(tmpPath, imageLocalPath); updatedMarkdown = updatedMarkdown.replace(url, imageRelativePath); console.log(`✓ (${(parsed.data?.size_bytes || 0).toLocaleString()} bytes)`); } else { console.log('✗ (文件未生成)'); } } else { console.log(`✗ (${parsed.error?.message || 'unknown'})`); } } catch (e) { console.log(`✗ (${e.message.split('\n')[0]})`); // 清理可能残留的临时文件 try { const tmpPath = resolve(`_scraper_tmp_${i}.png`); if (existsSync(tmpPath)) renameSync(tmpPath, imageLocalPath); } catch {} } } // 清理可能的残留临时文件 for (let i = 0; i < images.length; i++) { try { const p = resolve(`_scraper_tmp_${i}.png`); if (existsSync(p)) { const dest = resolve(imagesDir, `${i + 1}_${images[i].token}.png`); renameSync(p, dest); } } catch {} } } // ── 6. 保存 Markdown ────────────────────────────────────────────────────────── const outputPath = resolve(outputDir, `${safeTitle}.md`); writeFileSync(outputPath, updatedMarkdown, 'utf-8'); console.log(''); console.log('╔════════════════════════════════════════════╗'); console.log(`║ ✅ 完成!`); console.log(`║ 📄 ${outputPath}`); if (images.length > 0) { console.log(`║ 🖼️ 图片: ${imagesDir}`); } console.log('╚════════════════════════════════════════════╝');