Files
YTScraper/fetch-feishu-wiki.mjs
2026-07-05 13:22:25 +08:00

127 lines
6.1 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Feishu Wiki 文档获取工具
*
* 用法:node fetch-feishu-wiki.mjs <WIKI_URL>
*
* 图片下载:使用 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 <WIKI_URL>');
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('╚════════════════════════════════════════════╝');