1. 项目背景:当离职遇上文档封锁
去年年底,我准备从工作了3年的公司离职时,遭遇了一个意想不到的障碍——飞书账号里积累的500多篇技术文档突然无法导出。这些文档包含了我参与过的所有项目技术方案、问题排查记录和团队知识沉淀,对职业发展至关重要。公司IT部门以"信息安全"为由,直接关闭了我的文档导出权限。
1.1 飞书文档管理的痛点现状
飞书作为国内主流的企业协作平台,其文档管理存在几个典型问题:
- 导出限制严格:管理员可以随时关闭成员的文档导出权限
- 批量操作缺失:官方仅支持单篇文档手动导出(格式选择有限)
- API权限封闭:个人开发者难以获取完整的文档操作权限
我尝试了各种常规方法:
- 逐篇复制内容到本地(耗时且丢失格式)
- 使用浏览器开发者工具抓取(违反安全策略)
- 申请临时导出权限(被直接拒绝)
关键发现:飞书网页版在渲染文档时,其实已经将完整的Markdown源码传输到前端,只是通过CSS隐藏了导出按钮
2. 技术方案设计:逆向工程的艺术
2.1 核心思路拆解
通过Chrome DevTools分析飞书网页版网络请求,发现关键接口:
GET /space/api/box/stream/download/all/获取文档元数据POST /docs/v2/get获取文档完整内容(含Markdown源码)
解决方案架构:
graph TD A[模拟登录] --> B[获取文档列表] B --> C[并行请求文档内容] C --> D[自动转存本地]2.2 技术选型对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Puppeteer | 模拟真实操作 | 性能差易被检测 | 简单爬取 |
| 直接调用API | 高效稳定 | 需要破解鉴权 | 批量导出 |
| 浏览器插件 | 用户体验好 | 依赖具体浏览器 | 个人使用 |
最终选择Node.js+TypeScript技术栈:
- axios:处理鉴权和API请求
- cheerio:解析DOM获取隐藏数据
- p-limit:控制并发避免封禁
- inquirer:交互式命令行界面
3. 核心实现细节
3.1 鉴权破解关键代码
async function getAuthCookie() { const { default: fetch } = await import('node-fetch'); const response = await fetch('https://www.feishu.cn', { headers: { 'User-Agent': 'Mozilla/5.0' } }); // 提取Set-Cookie中的关键字段 const cookies = response.headers.raw()['set-cookie']; return cookies.map(c => c.split(';')[0]).join('; '); }3.2 文档元数据获取
interface DocItem { obj_type: string; obj_token: string; title: string; } async function listDocs(spaceId: string): Promise<DocItem[]> { const params = new URLSearchParams({ offset: '0', limit: '50', sort_by: 'created_time', sort_order: 'desc' }); const res = await axios.get( `https://www.feishu.cn/space/api/box/stream/download/all/?${params}`, { headers: { Cookie: await getAuthCookie() } } ); return res.data.data.items.filter((item: DocItem) => ['doc', 'sheet'].includes(item.obj_type) ); }3.3 内容抓取与转换
async function exportDoc(doc: DocItem) { try { const { data } = await axios.post( 'https://www.feishu.cn/docs/v2/get', { doc_token: doc.obj_token }, { headers: { Cookie: await getAuthCookie() } } ); const $ = cheerio.load(data.data.content); const markdown = turndownService.turndown($('body').html()); fs.writeFileSync( `./output/${doc.title}.md`, `# ${doc.title}\n\n${markdown}` ); } catch (err) { console.error(`导出失败 ${doc.title}:`, err.message); } }4. 工程化优化方案
4.1 性能优化策略
并发控制:限制同时请求数(建议5-10个)
const limit = pLimit(10); const promises = docs.map(doc => limit(() => exportDoc(doc)) ); await Promise.all(promises);断点续传:记录已导出文档ID
const exportedIds = new Set( fs.existsSync('./progress.json') ? JSON.parse(fs.readFileSync('./progress.json', 'utf-8')) : [] );自动重试机制:指数退避算法
async function withRetry(fn: Function, max = 3) { let attempt = 0; while (attempt < max) { try { return await fn(); } catch (err) { attempt++; await new Promise(r => setTimeout(r, 1000 * 2 ** attempt)); } } throw new Error(`重试${max}次失败`); }
4.2 安全防护措施
请求间隔随机化:避免固定频率触发风控
function randomDelay() { return new Promise(r => setTimeout(r, 1000 + Math.random() * 3000) ); }User-Agent轮换:模拟不同设备
const agents = [ 'Mozilla/5.0 (Windows NT 10.0)', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)' ];IP代理池(可选):使用住宅IP服务
5. 完整使用指南
5.1 环境准备
# 安装Node.js(建议v16+) nvm install 16.14.0 # 克隆仓库 git clone https://github.com/your-repo/feishu-exporter.git cd feishu-exporter # 安装依赖 npm install # 编译TypeScript npm run build5.2 配置说明
创建.env文件:
FEISHU_SPACE_ID=你的空间ID OUTPUT_DIR=./output MAX_CONCURRENT=5获取空间ID的方法:
- 浏览器打开飞书文档
- 地址栏类似:
https://your-domain.feishu.cn/docs/doccn123456
5.3 运行命令
npm start # 或直接运行编译后的JS node dist/index.js程序会交互式询问:
? 选择导出格式 (Markdown/PDF/DOCX): Markdown ? 是否包含评论 (y/n): n ? 是否保留修订记录 (y/n): n6. 法律风险提示
6.1 合规使用建议
- 仅导出个人创作内容:避免涉及公司机密
- 遵守公司政策:部分企业有明确的知识产权规定
- 数据最小化原则:只导出必要文档
6.2 免责声明
本工具仅限技术交流,使用者应自行承担相关法律责任。建议:
- 离职前与公司协商文档交接
- 重要文档提前备份
- 敏感内容手动脱敏处理
7. 开源项目维护
项目已发布在GitHub: feishu-exporter
7.1 贡献指南
欢迎通过以下方式参与:
- 提交Issue报告问题
- Fork仓库提交PR
- 完善文档和测试用例
7.2 路线图
- [ ] 支持多维表格导出
- [ ] 添加GUI界面
- [ ] 实现增量同步功能
8. 替代方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 本工具 | 完整保留格式 | 需技术基础 | 批量导出 |
| 手动复制 | 无需工具 | 效率极低 | 少量文档 |
| 官方导出 | 合规安全 | 权限受限 | 企业协作 |
实际测试数据(500篇文档):
- 手动导出:约25小时
- 本工具:约18分钟(并发10)
9. 常见问题排查
9.1 登录失败
症状:403 Forbidden解决方法:
- 检查Cookie是否过期(有效期通常2小时)
- 尝试从浏览器复制最新Cookie
- 关闭账号二次验证
9.2 内容缺失
症状:导出文档不完整 排查步骤:
- 确认有文档阅读权限
- 检查DOM元素选择器是否更新
- 尝试降低并发数
9.3 请求被封禁
症状:429 Too Many Requests应对策略:
- 增加请求间隔时间
- 更换IP地址
- 修改User-Agent
10. 高级技巧
10.1 自动分类存储
// 按文档类型分目录存储 function getOutputPath(doc: DocItem) { const category = doc.obj_type === 'doc' ? 'docs' : 'sheets'; return `./output/${category}/${doc.title}.md`; }10.2 附件同步下载
async function downloadAttachment(url: string) { const writer = fs.createWriteStream(`./attachments/${path.basename(url)}`); const response = await axios({ url, method: 'GET', responseType: 'stream' }); response.data.pipe(writer); return new Promise((resolve, reject) => { writer.on('finish', resolve); writer.on('error', reject); }); }10.3 自动化部署
使用Docker容器化:
FROM node:16-alpine WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build CMD ["node", "dist/index.js"]11. 项目反思与建议
这个项目给我三个重要启示:
- 技术即自由:关键时候能突破平台限制
- 合规边界:技术能力与法律风险的平衡
- 开源价值:社区协作让工具更完善
对于后来者的建议:
- 重要工作文档定期本地备份
- 深入理解所用工具的技术原理
- 保持对系统限制的批判性思考
工具虽已开源,但更希望企业能改进文档管理机制,让知识工作者能更自由地掌控自己的智力成果。