127 lines
6.1 KiB
JavaScript
Executable File
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('╚════════════════════════════════════════════╝');
|