Second commit
This commit is contained in:
Executable
+126
@@ -0,0 +1,126 @@
|
||||
#!/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('╚════════════════════════════════════════════╝');
|
||||
Reference in New Issue
Block a user