
Qiaomu Anything To Notebooklm
- 188 installs
- 5.7k repo stars
- Updated April 28, 2026
- joeseesun/anything-to-notebooklm
Convert PDFs, URLs, transcripts, and notes into NotebookLM-ready sources so teams can query, summarize, and study material inside Google NotebookLM.
About
qiaomu-anything-to-notebooklm from joeseesun/anything-to-notebooklm turns heterogeneous inputs into NotebookLM-compatible sources, streamlining research pipelines and agent-assisted study workflows for content-heavy products.
- Multi-format ingestion into NotebookLM
- Research corpus normalization
- Agent-friendly source packaging
- NotebookLM workflow automation
- Knowledge base prep
Qiaomu Anything To Notebooklm by the numbers
- 188 all-time installs (skills.sh)
- +3 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #529 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/joeseesun/anything-to-notebooklm --skill qiaomu-anything-to-notebooklmAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 188 |
|---|---|
| repo stars | ★ 5.7k |
| Last updated | April 28, 2026 |
| Repository | joeseesun/anything-to-notebooklm ↗ |
What it does
Convert PDFs, URLs, transcripts, and notes into NotebookLM-ready sources so teams can query, summarize, and study material inside Google NotebookLM.
Files
多源内容 → NotebookLM 智能处理器
自动从多种来源获取内容,上传到 NotebookLM,并根据自然语言指令生成播客、PPT、思维导图等多种格式。
支持的内容源
1. 微信公众号文章
通过 MCP 服务器自动抓取微信公众号文章内容(绕过反爬虫)
2. 任意网页链接
支持任何公开可访问的网页(新闻、博客、文档等)
3. 播客(小宇宙/喜马拉雅)+ B站视频
通过 Get笔记 API 获取完整转写文本(带时间戳),支持小宇宙、喜马拉雅、B站视频等音频/视频平台
4. X/Twitter 帖子
通过内置代理级联(r.jina.ai → defuddle.md → agent-fetch)抓取推文内容(含长推文线程),转为 Markdown
5. 付费墙网站自动绕过
自动检测并绕过 NYT、WSJ、FT、Economist、Bloomberg、Medium 等 300+ 付费网站的付费墙。策略:UA 伪装(Googlebot/Bingbot)→ Referer 伪装(Google/Facebook)→ AMP 页面 → archive.today 存档
5. YouTube 视频
直接传递给 NotebookLM! NotebookLM 原生支持 YouTube 链接,会自动提取视频字幕和元数据,无需手动下载字幕或转写。禁止使用 yt-dlp 或浏览器自动化提取字幕。
4. Office 文档
- Word (DOCX) - 保留表格和格式
- PowerPoint (PPTX) - 提取幻灯片和备注
- Excel (XLSX) - 表格数据
5. 播客/音频平台
- 小宇宙 (xiaoyuzhoufm.com) - 通过 Get笔记 API 获取完整转写
- 喜马拉雅 (ximalaya.com) - 通过 Get笔记 API 获取完整转写
- B站视频 (bilibili.com) - 通过 Get笔记 API 获取完整转写
- 其他音频链接 - 通过 Get笔记 API 获取转写
5. 电子书与文档
- PDF - 全文提取
- EPUB - 电子书全文提取
- Markdown (.md) - 原生支持
6. 图片与扫描件
- Images (JPEG, PNG, GIF, WebP) - OCR 识别文字
- 扫描的 PDF 文档 - OCR 提取文字
7. 音频文件
- Audio (WAV, MP3) - 语音转文字
8. 结构化数据
- CSV - 逗号分隔数据
- JSON - JSON 数据
- XML - XML 文档
9. 压缩包
- ZIP - 自动解压并处理所有支持的文件
10. 纯文本
直接输入或粘贴的文本内容
11. 搜索关键词
通过 Web Search 搜索关键词,汇总多个来源的信息
前置条件
1. 安装 wexin-read-mcp
MCP 服务器已安装在:~/.claude/skills/qiaomu-anything-to-notebooklm/wexin-read-mcp/
配置 MCP(需要手动添加到 Claude 配置文件):
macOS: 编辑 ~/.claude/config.json
{
"primaryApiKey": "any",
"mcpServers": {
"weixin-reader": {
"command": "python",
"args": [
"/Users/joe/.claude/skills/qiaomu-anything-to-notebooklm/wexin-read-mcp/src/server.py"
]
}
}
}配置后需要重启 Claude Code。
2. notebooklm 认证
首次使用前必须认证:
notebooklm login
notebooklm list # 验证认证成功触发方式
微信公众号文章
/qiaomu-anything-to-notebooklm [微信文章链接]- "把这篇微信文章传到NotebookLM"
- "把这篇微信文章生成播客"
网页链接
- "把这个网页做成播客 [URL]"
- "这篇文章帮我做成PPT [URL]"
- "帮我分析这个网页 [URL]"
播客(小宇宙/喜马拉雅/B站)
- "把这个播客生成播客 [小宇宙链接]"
- "这个小宇宙节目帮我做成PPT [链接]"
- "深度解读这期播客 [喜马拉雅链接]"
- "把这个B站视频转写传到NotebookLM [bilibili链接]"
- "B站视频帮我生成思维导图 [bilibili链接]"
X/Twitter 帖子
- "把这条推文传到NotebookLM [x.com链接]"
- "这篇推文线程帮我生成报告 [x.com链接]"
- "深度分析这条推文 [twitter.com链接]"
YouTube 视频
- 直接传 URL 给 NotebookLM,不下载字幕! NotebookLM 原生支持 YouTube
- "把这个YouTube视频做成播客 [YouTube URL]"
- "这个视频帮我生成思维导图 [YouTube URL]"
本地文件
- "把这个PDF上传到NotebookLM /path/to/file.pdf"
- "这个Markdown文件生成PPT /path/to/file.md"
- "这个EPUB电子书生成播客 /path/to/book.epub"
- "把这个Word文档做成思维导图 /path/to/doc.docx"
- "这个PowerPoint生成Quiz /path/to/slides.pptx"
- "把这个扫描PDF做成报告 /path/to/scan.pdf"(自动OCR)
搜索关键词
- "搜索 'AI发展趋势' 并生成报告"
- "搜索关于'量子计算'的资料做成播客"
混合使用
- "把这篇文章、这个视频和这个PDF一起上传,生成一份报告"
深度分析模式(递归提问)
- "深度分析这本书 /path/to/book.epub"
- "提炼这篇文章的核心观点 [URL]"
- "递归提问分析这个PDF /path/to/file.pdf"
- "帮我深度解读这个视频 [YouTube URL]"
深度分析 + 飞书文档
- "深度分析这本书并写入飞书 /path/to/book.epub"
- "分析这篇文章后创建飞书文档 [URL]"
- "递归提问并生成飞书文档 /path/to/file.pdf"
自然语言 → NotebookLM 功能映射
| 用户说的话 | 识别意图 | NotebookLM 命令 |
|---|---|---|
| "生成播客" / "做成音频" / "转成语音" | audio | generate audio |
| "做成PPT" / "生成幻灯片" / "做个演示" | slide-deck | generate slide-deck |
| "画个思维导图" / "生成脑图" / "做个导图" | mind-map | generate mind-map |
| "生成Quiz" / "出题" / "做个测验" | quiz | generate quiz |
| "做个视频" / "生成视频" | video | generate video |
| "生成报告" / "写个总结" / "整理成文档" | report | generate report |
| "做个信息图" / "可视化" | infographic | generate infographic |
| "生成数据表" / "做个表格" | data-table | generate data-table |
| "做成闪卡" / "生成记忆卡片" | flashcards | generate flashcards |
| "深度分析" / "提炼核心观点" / "递归提问" / "深度解读" | deep-analysis | 自动生成10个问题并递归提问 |
| "写入飞书" / "创建飞书文档" / "生成飞书文档" / "保存到飞书" | feishu | 创建飞书文档并写入内容 |
如果没有明确指令,默认只上传不生成任何内容,等待用户后续指令。
工作流程
Step 1: 识别内容源类型
Claude 自动识别输入类型:
| 输入特征 | 识别为 | 处理方式 |
|---|---|---|
https://mp.weixin.qq.com/s/ | 微信公众号 | MCP 工具抓取 |
https://youtube.com/... 或 https://youtu.be/... | YouTube | 直接传递给 NotebookLM |
xiaoyuzhoufm.com 或 ximalaya.com 或 bilibili.com | 播客/视频 | Get笔记 API 转写 → TXT |
x.com 或 twitter.com | X/Twitter 帖子 | 内置代理级联抓取 → TXT |
https:// 或 http://(付费网站) | 付费墙网页 | 内置付费墙绕过(UA伪装+archive.today)→ TXT |
https:// 或 http:// | 网页 | 直接传递给 NotebookLM |
/path/to/file.pdf | PDF 文件 | markitdown 转 Markdown → TXT |
/path/to/file.epub | EPUB 电子书 | Python ebooklib 提取文本 → TXT(避免 Calibre) |
/path/to/file.docx | Word 文档 | markitdown 转 Markdown → TXT |
/path/to/file.pptx | PowerPoint | markitdown 转 Markdown → TXT |
/path/to/file.xlsx | Excel | markitdown 转 Markdown → TXT |
/path/to/file.md | Markdown | 直接上传 |
/path/to/image.jpg | 图片(OCR) | markitdown OCR → TXT |
/path/to/audio.mp3 | 音频 | markitdown 转录 → TXT |
/path/to/file.zip | ZIP 压缩包 | 解压 → markitdown 批量转换 |
| 关键词(无URL,无路径) | 搜索查询 | WebSearch → 汇总 → TXT |
Step 2: 获取内容
微信公众号:
- 使用 MCP 工具
read_weixin_article - 返回:title, author, publish_time, content
- 保存为 TXT:
/tmp/weixin_{title}_{timestamp}.txt
播客/视频(小宇宙/喜马拉雅/B站):
- 通过 Get笔记 API 获取完整转写文本
- 调用
python3 ~/.claude/skills/qiaomu-anything-to-notebooklm/scripts/get_podcast_transcript.py <URL> - 脚本自动执行:创建链接笔记 → 等待转写 → 获取全文 → 保存 TXT
- 返回 TXT 路径和标题
- 依赖:Get笔记 API Key(环境变量
GETNOTE_API_KEY、GETNOTE_CLIENT_ID)+ Web Token(~/.claude/skills/getnote/tokens.json)
X/Twitter 帖子:
- 通过内置代理级联抓取推文内容(r.jina.ai → defuddle.md → agent-fetch)
- 调用
bash ~/.claude/skills/qiaomu-anything-to-notebooklm/scripts/fetch_url.sh "https://x.com/..."获取 Markdown 内容 - 自动处理 X 登录墙和错误页面
- 保存为 TXT 后上传到 NotebookLM
网页:
- 直接使用
notebooklm source add <URL> - NotebookLM 自动提取内容
- 付费墙绕过:遇到付费网站时,
fetch_url.sh自动启用多重绕过策略
YouTube 🔴 特殊规则(最重要!):
- 直接传递 URL 给 NotebookLM!
notebooklm source add <YouTube_URL> - 禁止使用 yt-dlp、yt-search-download、whisper、浏览器自动化等手段下载字幕
- NotebookLM 原生支持 YouTube,会自动提取字幕和元数据
- 这是最快速、最高效的方式,不需要任何中间步骤
1. r.jina.ai — 通常能绕过软付费墙 2. Googlebot/Bingbot UA 伪装 — 模拟搜索引擎爬虫(网站为了 SEO 通常给爬虫全文) 3. Referer 伪装 — 伪装来自 Google/Facebook(社交引流豁免) 4. AMP 页面 — AMP 版本通常没有付费墙 5. archive.today — 从网页存档获取全文
- 支持的付费网站:NYT、WSJ、FT、Economist、Bloomberg、Washington Post、New Yorker、Wired、The Atlantic、Medium、MIT Technology Review、SCMP 等 300+ 站点
Office 文档/电子书/PDF:
- EPUB:使用 Python ebooklib + BeautifulSoup 直接提取文本(避免 Calibre 架构问题)
- 其他格式:使用 markitdown 转换为 Markdown
- 命令:
markitdown /path/to/file.docx -o /tmp/converted.md - 保存为 TXT:
/tmp/{filename}_converted_{timestamp}.txt
本地 Markdown:
- 直接上传:
notebooklm source add /path/to/file.md
图片(OCR):
- markitdown 自动 OCR 识别文字
- 提取 EXIF 元数据
- 保存为 TXT
音频文件:
- markitdown 自动转录语音为文字
- 提取音频元数据
- 保存为 TXT
ZIP 压缩包:
- 自动解压到临时目录
- 遍历所有支持的文件
- 批量使用 markitdown 转换
- 合并为单个 TXT 或多个 Source
搜索关键词:
- 使用 WebSearch 工具搜索关键词
- 汇总前 3-5 条结果
- 保存为 TXT:
/tmp/search_{keyword}_{timestamp}.txt
Step 3: 上传到 NotebookLM
调用 notebooklm skill:
notebooklm create "{title}" # 创建新笔记本
notebooklm source add /tmp/weixin_xxx.txt --title "{title}" # 上传文件注意:NotebookLM 会自动处理上传的文件,无需手动等待。
Step 4: 深度分析模式(可选)
如果用户指定了"深度分析"、"递归提问"等意图,自动执行:
# 仅深度分析
python ~/.claude/skills/qiaomu-anything-to-notebooklm/main.py \
/path/to/file.epub --deep-analysis
# 深度分析 + 自动创建飞书文档
python ~/.claude/skills/qiaomu-anything-to-notebooklm/main.py \
/path/to/file.epub --deep-analysis --to-feishu深度分析流程: 1. 上传内容到 NotebookLM 2. 根据内容类型自动生成 10 个深度问题 3. 依次向 NotebookLM 提问并收集答案 4. 返回结构化 JSON 数据(包含问题、答案、统计信息) 5. (可选)如果指定 --to-feishu,自动创建飞书文档并写入问答内容
问题类型:
- 书籍/文档:核心观点、金句、论证逻辑、实践建议、局限性等
- 视频:目标受众、关键数据、叙事结构、精华版内容等
- 文章/网页:写作目的、数据支撑、作者立场、个人启发等
输出格式:
{
"status": "success",
"title": "书名/标题",
"content_type": "epub/document/url",
"questions": ["问题1", "问题2", ...],
"answers": ["答案1", "答案2", ...],
"total_questions": 10,
"answered": 10
}Step 5: 根据意图生成内容(可选)
如果用户指定了处理意图,自动调用对应命令:
| 意图 | 命令 | 等待 | 下载 |
|---|---|---|---|
| audio | notebooklm generate audio | artifact wait | download audio ./output.mp3 |
| slide-deck | notebooklm generate slide-deck | artifact wait | download slide-deck ./output.pdf |
| mind-map | notebooklm generate mind-map | artifact wait | download mind-map ./map.json |
| quiz | notebooklm generate quiz | artifact wait | download quiz ./quiz.md --format markdown |
| video | notebooklm generate video | artifact wait | download video ./output.mp4 |
| report | notebooklm generate report | artifact wait | download report ./report.md |
| infographic | notebooklm generate infographic | artifact wait | download infographic ./infographic.png |
| flashcards | notebooklm generate flashcards | artifact wait | download flashcards ./cards.md --format markdown |
生成流程: 1. 发起生成请求(返回 task_id) 2. 等待生成完成(artifact wait <task_id>) 3. 下载生成的文件到本地 4. 告知用户文件路径
完整示例
示例 1:微信公众号文章 → 播客
用户输入:
把这篇文章生成播客 https://mp.weixin.qq.com/s/abc123xyz执行流程: 1. 识别为微信公众号链接 2. MCP 工具抓取文章内容 3. 创建 TXT 文件 4. 上传到 NotebookLM 5. 生成播客(generate audio) 6. 下载播客到本地
输出:
✅ 微信文章已转换为播客!
📄 文章:深度学习的未来趋势
👤 作者:张三
📅 发布:2026-01-20
🎙️ 播客已生成:
📁 文件:/tmp/weixin_深度学习的未来趋势_podcast.mp3
⏱️ 时长:约 8 分钟
📊 大小:12.3 MB示例 2:YouTube 视频 → 思维导图
用户输入:
这个视频帮我画个思维导图 https://www.youtube.com/watch?v=abc123执行流程: 1. 识别为 YouTube 链接 2. 直接传递给 NotebookLM(自动提取字幕) 3. 生成思维导图(generate mind-map) 4. 下载思维导图
输出:
✅ YouTube 视频已转换为思维导图!
🎬 视频:Understanding Quantum Computing
⏱️ 时长:23 分钟
🗺️ 思维导图已生成:
📁 文件:/tmp/youtube_quantum_computing_mindmap.json
📊 节点数:45 个示例 3:搜索关键词 → 报告
用户输入:
搜索 'AI发展趋势 2026' 并生成报告执行流程: 1. 识别为搜索查询 2. WebSearch 搜索关键词 3. 汇总前 5 条结果 4. 创建 TXT 文件 5. 上传到 NotebookLM 6. 生成报告(generate report)
输出:
✅ 搜索结果已生成报告!
🔍 关键词:AI发展趋势 2026
📊 来源:5 篇文章
📄 报告已生成:
📁 文件:/tmp/search_AI发展趋势2026_report.md
📝 章节:7 个
📊 大小:15.2 KB示例 4:混合多源 → PPT
用户输入:
把这篇文章、这个视频和这个PDF一起做成PPT:
- https://example.com/article
- https://youtube.com/watch?v=xyz
- /Users/joe/Documents/research.pdf执行流程: 1. 创建新 Notebook 2. 依次添加 3 个 Source 3. 基于所有 Source 生成 PPT
输出:
✅ 多源内容已整合为PPT!
📚 内容源:
1. 网页文章:AI in 2026
2. YouTube:Future of AI
3. PDF:Research Notes (12 页)
📊 PPT 已生成:
📁 文件:/tmp/multi_source_slides.pdf
📄 页数:25 页
📦 大小:3.8 MB示例 5: EPUB 电子书 → 播客
用户输入:
把这本电子书做成播客 /Users/joe/Books/sapiens.epub执行流程: 1. 识别为 EPUB 文件 2. markitdown 转换为 Markdown 3. 保存为 TXT 4. 上传到 NotebookLM 5. 生成播客
输出:
✅ EPUB 电子书已转换为播客!
📚 电子书:Sapiens: A Brief History of Humankind
📄 页数:约 450 页
📊 字数:约 15 万字
🎙️ 播客已生成:
📁 文件:/tmp/sapiens_podcast.mp3
⏱️ 时长:约 45 分钟(精华版)
📊 大小:48.2 MB示例 6:Word 文档 → Quiz
用户输入:
这个Markdown生成Quiz /Users/joe/notes/machine_learning.md执行流程: 1. 识别为本地 Markdown 文件 2. 直接上传到 NotebookLM 3. 生成 Quiz(generate quiz)
输出:
✅ Markdown 已转换为Quiz!
📄 文件:machine_learning.md
📊 大小:8.5 KB
📝 Quiz 已生成:
📁 文件:/tmp/machine_learning_quiz.md
❓ 题目:15 道(10选择 + 5简答)错误处理
URL 格式错误
❌ 错误:URL 格式不正确
必须是微信公众号文章链接:
https://mp.weixin.qq.com/s/xxx
你提供的链接:https://example.com文章获取失败
❌ 错误:无法获取文章内容
可能原因:
1. 文章已被删除
2. 文章需要登录查看(暂不支持)
3. 网络连接问题
4. 微信反爬虫拦截(请稍后重试)
建议:
- 检查链接是否正确
- 等待 2-3 秒后重试
- 或手动复制文章内容NotebookLM 认证失败
❌ 错误:NotebookLM 认证失败
请运行以下命令重新登录:
notebooklm login
然后验证:
notebooklm list生成任务失败
❌ 错误:播客生成失败
可能原因:
1. 文章内容太短(< 100 字)
2. 文章内容太长(> 50万字)
3. NotebookLM 服务异常
建议:
- 检查文章长度是否适中
- 稍后重试
- 或尝试其他格式(如生成报告)高级功能
1. 多意图处理
用户可以一次性指定多个处理任务:
这篇文章帮我生成播客和PPT https://mp.weixin.qq.com/s/abc123Skill 会依次执行: 1. 生成播客 2. 生成 PPT
2. 自定义 Notebook
默认每篇文章创建新 Notebook,也可以指定已有 Notebook:
把这篇文章加到我的【AI研究】笔记本 https://mp.weixin.qq.com/s/abc123Skill 会: 1. 搜索名为"AI研究"的 Notebook 2. 将文章添加为新 Source 3. 基于所有 Sources 生成内容
3. 自定义生成指令
为生成任务添加具体要求:
这篇文章生成播客,要求:轻松幽默的风格,时长控制在5分钟Skill 会将要求作为 instructions 传给 NotebookLM。
注意事项
1. 频率限制:
- 每次请求间隔 > 2 秒,避免被微信封禁
- NotebookLM 生成任务有并发限制(最多 3 个同时进行)
2. 内容长度:
- 微信文章通常 1000-5000 字,适合生成播客(3-8 分钟)
- 超过 10000 字的长文可能需要更长生成时间
- 少于 500 字的短文可能生成效果不佳
3. 版权遵守:
- 仅用于个人学习研究
- 遵守微信公众号的版权规定
- 生成的内容不得用于商业用途
4. 生成时间:
- 播客:2-5 分钟
- 视频:3-8 分钟
- PPT:1-3 分钟
- 思维导图:1-2 分钟
- Quiz/闪卡:1-2 分钟
5. 文件清理:
- TXT 源文件保存在
/tmp/,系统重启后自动清理 - 生成的文件(MP3/PDF/MD 等)默认保存在
/tmp/ - 可以指定自定义保存路径
相关 Skills
notebooklm- NotebookLM 核心功能notebooklm-deep-analyzer- 深度分析 NotebookLM 内容markitdown- 转换其他格式文档
配置 MCP(重要)
⚠️ 第一次使用前必须配置
编辑 ~/.claude/config.json:
{
"primaryApiKey": "any",
"mcpServers": {
"weixin-reader": {
"command": "python",
"args": [
"/Users/joe/.claude/skills/qiaomu-anything-to-notebooklm/wexin-read-mcp/src/server.py"
]
}
}
}配置后重启 Claude Code!
故障排查
1. MCP 工具未找到
# 测试 MCP 服务器
python ~/.claude/skills/qiaomu-anything-to-notebooklm/wexin-read-mcp/src/server.py
# 如果报错,检查依赖
cd ~/.claude/skills/qiaomu-anything-to-notebooklm/wexin-read-mcp
pip install -r requirements.txt
playwright install chromium2. NotebookLM 命令失败
# 检查认证状态
notebooklm status
# 重新登录
notebooklm login
# 验证
notebooklm list3. 文件权限问题
# 确保临时目录可写
chmod 755 /tmp
# 测试写入
touch /tmp/test.txt && rm /tmp/test.txt4. 生成任务卡住
# 检查任务状态
notebooklm artifact list
# 如果显示 "pending" 超过 10 分钟,取消重试
# (目前 CLI 不支持取消,需要在网页端操作)典型使用场景
场景 1:快速学习
我想学习这篇文章,帮我生成播客,上下班路上听
链接:https://mp.weixin.qq.com/s/abc123→ 生成 8 分钟播客,通勤时间听完
场景 2:分享给团队
这篇文章不错,做成PPT分享给团队
https://mp.weixin.qq.com/s/abc123→ 生成 15 页 PPT,直接用于团队分享
场景 3:复习巩固
这篇技术文章帮我出题,想测试一下掌握程度
https://mp.weixin.qq.com/s/abc123→ 生成 10 道选择题 + 5 道简答题
场景 4:可视化理解
这篇文章概念比较多,画个思维导图帮我理清结构
https://mp.weixin.qq.com/s/abc123→ 生成思维导图,一目了然
---
Skill 创建时间:2026-01-25 最后更新:2026-01-25 版本:v1.0.0
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# MCP Server (will be cloned during install)
# Keep the directory but ignore its contents
wexin-read-mcp/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Logs
*.log
# Environment
.env
.env.local
# Temporary files
tmp/
temp/
*.tmp
# macOS
.DS_Store
.AppleDouble
.LSOverride
#!/usr/bin/env python3
"""
环境检查脚本 - 验证 qiaomu-anything-to-notebooklm skill 所有依赖
"""
import sys
import os
import json
from pathlib import Path
# 颜色输出
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
NC = '\033[0m'
def print_status(status, message):
"""打印状态信息"""
if status == "ok":
print(f"{GREEN}✅ {message}{NC}")
elif status == "warning":
print(f"{YELLOW}⚠️ {message}{NC}")
elif status == "error":
print(f"{RED}❌ {message}{NC}")
else:
print(f"{BLUE}ℹ️ {message}{NC}")
def check_python_version():
"""检查 Python 版本"""
version = sys.version_info
version_str = f"{version.major}.{version.minor}.{version.micro}"
if version.major >= 3 and version.minor >= 9:
print_status("ok", f"Python {version_str}")
return True
else:
print_status("error", f"Python {version_str} (需要 3.9+)")
return False
def check_module(module_name, import_name=None):
"""检查 Python 模块是否已安装"""
if import_name is None:
import_name = module_name
try:
__import__(import_name)
print_status("ok", f"{module_name} 已安装")
return True
except ImportError:
print_status("error", f"{module_name} 未安装")
return False
def check_command(cmd):
"""检查命令是否可用"""
import shutil
if shutil.which(cmd):
# 尝试获取版本
import subprocess
try:
result = subprocess.run([cmd, "--version"],
capture_output=True,
text=True,
timeout=5)
version = result.stdout.split('\n')[0] if result.stdout else "unknown"
print_status("ok", f"{cmd} 已安装 ({version})")
except:
print_status("ok", f"{cmd} 已安装")
return True
else:
print_status("error", f"{cmd} 未找到")
return False
def check_mcp_config():
"""检查 MCP 配置"""
config_path = Path.home() / ".claude" / "config.json"
if not config_path.exists():
print_status("error", f"未找到 Claude 配置文件: {config_path}")
return False
try:
with open(config_path, 'r') as f:
config = json.load(f)
if "mcpServers" in config and "weixin-reader" in config["mcpServers"]:
print_status("ok", "MCP 服务器已配置")
return True
else:
print_status("warning", "MCP 服务器未配置(需要手动添加)")
return False
except Exception as e:
print_status("error", f"无法读取配置文件: {e}")
return False
def check_mcp_server():
"""检查 MCP 服务器文件"""
skill_dir = Path(__file__).parent
mcp_server = skill_dir / "wexin-read-mcp" / "src" / "server.py"
if mcp_server.exists():
print_status("ok", f"MCP 服务器文件存在")
return True
else:
print_status("error", f"MCP 服务器文件不存在: {mcp_server}")
return False
def check_notebooklm_auth():
"""检查 NotebookLM 认证状态"""
import subprocess
try:
result = subprocess.run(["notebooklm", "list"],
capture_output=True,
text=True,
timeout=10)
if result.returncode == 0:
print_status("ok", "NotebookLM 已认证")
return True
else:
print_status("warning", "NotebookLM 未认证(请运行 notebooklm login)")
return False
except subprocess.TimeoutExpired:
print_status("warning", "NotebookLM 认证检查超时")
return False
except Exception as e:
print_status("error", f"NotebookLM 认证检查失败: {e}")
return False
def main():
print(f"\n{BLUE}========================================{NC}")
print(f"{BLUE} 环境检查 - qiaomu-anything-to-notebooklm{NC}")
print(f"{BLUE}========================================{NC}\n")
results = []
# 1. Python 版本
print(f"{YELLOW}[1/8] Python 版本{NC}")
results.append(check_python_version())
print()
# 2. 核心依赖
print(f"{YELLOW}[2/9] 核心 Python 依赖{NC}")
results.append(check_module("fastmcp"))
results.append(check_module("playwright"))
results.append(check_module("beautifulsoup4", "bs4"))
results.append(check_module("lxml"))
results.append(check_module("markitdown"))
print()
# 3. Playwright 浏览器
print(f"{YELLOW}[3/9] Playwright 可导入性{NC}")
try:
from playwright.sync_api import sync_playwright
print_status("ok", "Playwright 可以正常导入")
results.append(True)
except Exception as e:
print_status("error", f"Playwright 导入失败: {e}")
results.append(False)
print()
# 4. NotebookLM CLI
print(f"{YELLOW}[4/9] NotebookLM CLI{NC}")
results.append(check_command("notebooklm"))
print()
# 5. markitdown CLI
print(f"{YELLOW}[5/9] markitdown CLI{NC}")
results.append(check_command("markitdown"))
print()
# 6. Git 命令
print(f"{YELLOW}[6/9] Git 命令{NC}")
results.append(check_command("git"))
print()
# 7. MCP 服务器文件
print(f"{YELLOW}[7/9] MCP 服务器文件{NC}")
results.append(check_mcp_server())
print()
# 8. MCP 配置
print(f"{YELLOW}[8/9] MCP 配置{NC}")
results.append(check_mcp_config())
print()
# 9. NotebookLM 认证
print(f"{YELLOW}[9/9] NotebookLM 认证{NC}")
results.append(check_notebooklm_auth())
print()
# 总结
print(f"{BLUE}========================================{NC}")
passed = sum(results)
total = len(results)
if passed == total:
print(f"{GREEN}✅ 所有检查通过 ({passed}/{total})!环境配置完整。{NC}")
elif passed >= total * 0.8:
print(f"{YELLOW}⚠️ 大部分检查通过 ({passed}/{total}),但有些问题需要修复。{NC}")
else:
print(f"{RED}❌ 检查失败 ({passed}/{total}),请运行 install.sh 重新安装。{NC}")
print(f"{BLUE}========================================{NC}\n")
if passed < total:
print("💡 修复建议:")
print(" 1. 运行安装脚本:./install.sh")
print(" 2. 配置 MCP:编辑 ~/.claude/config.json")
print(" 3. 认证 NotebookLM:notebooklm login")
print()
sys.exit(0 if passed == total else 1)
if __name__ == "__main__":
main()
#!/bin/bash
# 飞书文档读取 MCP 服务器安装脚本
set -e
echo "======================================"
echo " 飞书文档读取 MCP 服务器安装"
echo "======================================"
echo ""
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 检查 Python
if ! command -v python3 &> /dev/null; then
echo -e "${RED}❌ Python3 未安装${NC}"
exit 1
fi
echo -e "${GREEN}✓${NC} Python 版本: $(python3 --version)"
# 检查 pip
if ! command -v pip3 &> /dev/null; then
echo -e "${RED}❌ pip3 未安装${NC}"
exit 1
fi
echo -e "${GREEN}✓${NC} pip 版本: $(pip3 --version)"
# 1. 安装 Python 依赖
echo ""
echo -e "${YELLOW}[1/3]${NC} 安装 Python 依赖..."
pip3 install -r requirements.txt
echo -e "${GREEN}✓${NC} Python 依赖安装完成"
# 2. 安装 Playwright 浏览器
echo ""
echo -e "${YELLOW}[2/3]${NC} 安装 Playwright 浏览器..."
if ! command -v playwright &> /dev/null; then
echo "安装 Playwright CLI..."
pip3 install playwright
echo "安装浏览器..."
playwright install chromium
else
echo "Playwright 已安装,更新浏览器..."
playwright install chromium
fi
echo -e "${GREEN}✓${NC} Playwright 浏览器安装完成"
# 3. 创建临时目录
echo ""
echo -e "${YELLOW}[3/3]${NC} 创建临时目录..."
mkdir -p /tmp/feishu_docs
mkdir -p /tmp/feishu_images
echo -e "${GREEN}✓${NC} 临时目录创建完成"
# 测试安装
echo ""
echo -e "${YELLOW}测试安装...${NC}"
python3 -c "
import sys
sys.path.insert(0, 'src')
try:
from scraper import FeishuScraper
from parser import FeishuParser
from image_handler import ImageHandler
print('✓ 所有模块导入成功')
except ImportError as e:
print(f'✗ 模块导入失败: {e}')
sys.exit(1)
"
echo ""
echo -e "${GREEN}======================================${NC}"
echo -e "${GREEN}安装完成!${NC}"
echo -e "${GREEN}======================================${NC}"
echo ""
echo "下一步:"
echo "1. 配置 MCP 服务器(在 Claude Code 配置文件中)"
echo "2. 重启 Claude Code"
echo "3. 开始使用!"
echo ""
echo "配置文件示例:"
cat << 'EOF'
{
"mcpServers": {
"feishu-reader": {
"command": "python",
"args": [
"/path/to/feishu-read-mcp/src/server.py"
]
}
}
}
EOF
echo ""
echo "详细文档请查看 README.md"
飞书文档读取 MCP 服务器
一个 MCP(Model Context Protocol)服务器,用于读取飞书文档并转换为 Markdown 格式,支持图片下载。
✨ 特性
- 📖 读取飞书文档 - 支持所有飞书文档格式
- 🖼️ 图片下载 - 自动下载文档中的图片并保存到本地
- 📝 Markdown 输出 - 完美保留文档结构和格式
- ⚡ 高性能 - 异步处理,支持并发
- 🔧 易于集成 - 符合 MCP 标准,可直接集成到 Claude Code
🚀 安装
1. 安装依赖
pip install -r requirements.txt2. 安装 Playwright 浏览器
playwright install chromium3. 配置 MCP
在 Claude Code 配置文件中添加:
{
"mcpServers": {
"feishu-reader": {
"command": "python",
"args": [
"/path/to/feishu-read-mcp/src/server.py"
]
}
}
}📚 使用方法
作为 MCP 工具使用
1. 读取飞书文档
result = await read_feishu_doc("https://bytedance.feishu.cn/docs/docc/xxx")返回结果:
{
"success": True,
"title": "文档标题",
"author": "作者",
"content": "# 文档标题\n\n这是文档内容...",
"images": ["/tmp/feishu_images/xxx1.png", "/tmp/feishu_images/xxx2.png"],
"word_count": 1500,
"image_count": 2,
"url": "原始URL"
}2. 获取文档信息
info = await get_doc_info("https://bytedance.feishu.cn/docs/docc/xxx")返回结果:
{
"success": True,
"title": "文档标题",
"author": "作者",
"word_count": 1500,
"image_count": 2
}直接作为 Python 库使用
from src.scraper import FeishuScraper
# 读取文档
async with FeishuScraper() as scraper:
result = await scraper.fetch_doc("https://bytedance.feishu.cn/docs/docc/xxx")
print(result["content"])
print(f"下载了 {len(result['images'])} 张图片")📦 支持的格式
输入格式
- ✅ 飞书文档(feishu.cn / feishu.com)
- ✅ 公开文档
- ✅ 需要登录的文档(支持 Cookie)
输出格式
- ✅ Markdown(.md)
- ✅ 保留标题层级
- ✅ 保留列表结构
- ✅ 保留表格
- ✅ 保留代码块
- ✅ 保留图片(本地路径)
- ✅ 保留链接
- ✅ 保留引用
图片处理
- ✅ 自动下载图片
- ✅ 支持 JPG、PNG、GIF、WebP 等格式
- ✅ 图片优化(压缩、格式转换)
- ✅ 本地存储(/tmp/feishu_images/)
🔧 高级配置
图片存储路径
默认图片存储在 /tmp/feishu_images/,可以通过修改 ImageHandler 类来更改:
image_handler = ImageHandler()
image_handler.image_dir = Path("/custom/path/to/images")并发控制
默认最多并发 5 个图片下载任务,可以通过修改 scraper.py 中的 semaphore 来调整:
semaphore = asyncio.Semaphore(10) # 改为 10超时设置
默认下载超时 30 秒,可以通过修改 image_handler.py 中的 timeout 来调整:
self.timeout = ClientTimeout(total=60, connect=20) # 改为 60 秒📝 示例
示例 1:读取简单的飞书文档
from src.scraper import fetch_feishu_doc
result = await fetch_feishu_doc(
"https://bytedance.feishu.cn/docs/docc/abc123"
)
if result["success"]:
print(f"标题: {result['title']}")
print(f"作者: {result['author']}")
print(f"内容:\n{result['content']}")
print(f"图片: {result['images']}")
else:
print(f"错误: {result['error']}")示例 2:批量处理多个文档
from src.scraper import FeishuScraper
async def batch_process(urls):
async with FeishuScraper() as scraper:
for url in urls:
result = await scraper.fetch_doc(url)
# 处理结果...示例 3:自定义图片处理
from src.image_handler import ImageHandler
async def download_with_custom_handler(urls):
handler = ImageHandler()
handler.image_dir = Path("./my_images") # 自定义目录
image_map = await handler.download_images(urls)
return image_map🐛 故障排除
问题 1:Playwright 安装失败
# 卸载并重新安装
pip uninstall playwright
pip install playwright
# 安装浏览器
playwright install chromium问题 2:图片下载失败
检查:
- 网络连接
- 图片 URL 是否可访问
- 临时目录权限
ls -la /tmp/feishu_images/问题 3:文档加载超时
增加超时时间:
await page.goto(url, wait_until='networkidle', timeout=120000) # 改为 120 秒📄 许可证
MIT License
🤝 贡献
欢迎提交 Issue 和 Pull Request!
📧 联系方式
- GitHub: https://github.com/joeseesun/qiaomu-anything-to-notebooklm
- Email: joe@example.com
---
由 [joeseesun](https://github.com/joeseesun) 开发
# MCP 服务器核心依赖
fastmcp>=0.2.0
# 浏览器自动化
playwright>=1.40.0
# HTTP 请求
aiohttp>=3.9.0
aiofiles>=23.0.0
# HTML 解析
beautifulsoup4>=4.12.0
lxml>=4.9.0
# 图片处理
Pillow>=10.0.0
# 其他工具
aiofiles>=23.0.0
aiohttp-client-cache>=0.6.0
"""飞书文档读取 MCP 服务器"""
__version__ = "1.0.0"
__author__ = "joeseesun"
from .server import mcp
from .scraper import FeishuScraper
from .parser import FeishuParser
from .image_handler import ImageHandler
__all__ = [
"mcp",
"FeishuScraper",
"FeishuParser",
"ImageHandler"
]
"""图片处理器"""
import asyncio
import hashlib
import logging
from pathlib import Path
from typing import List, Dict, Optional
from urllib.parse import urljoin, urlparse
import aiohttp
from aiohttp import ClientTimeout
import io
from PIL import Image
import json
logger = logging.getLogger(__name__)
class ImageHandler:
"""飞书文档图片处理器"""
def __init__(self, cookies_str: Optional[str] = None):
"""
初始化图片处理器
Args:
cookies_str: 可选的cookie字符串
"""
self.image_dir = Path("/tmp/feishu_images")
self.image_dir.mkdir(parents=True, exist_ok=True)
self.timeout = ClientTimeout(total=30, connect=10)
self.cookies_str = cookies_str
async def download_images(self, image_urls: List[str]) -> Dict[str, str]:
"""
批量下载图片
Args:
image_urls: 图片URL列表
Returns:
Dict[str, str]: {原始URL: 本地路径} 的映射
"""
if not image_urls:
return {}
logger.info(f"Starting to download {len(image_urls)} images...")
# 准备headers
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
# 准备cookies(如果提供)
cookies = None
if self.cookies_str:
cookie_list = []
for cookie_pair in self.cookies_str.split(';'):
if '=' in cookie_pair:
key, value = cookie_pair.strip().split('=', 1)
cookie_list.append({
'name': key,
'value': value,
'domain': '.feishu.cn'
})
cookies = cookie_list
# 创建会话
async with aiohttp.ClientSession(timeout=self.timeout, headers=headers, cookies=cookies) as session:
# 并发下载(限制并发数为5)
semaphore = asyncio.Semaphore(5)
async def download_single(url: str) -> tuple[str, str]:
"""下载单个图片"""
async with semaphore:
return await self._download_image(session, url)
# 执行并发下载
results = await asyncio.gather(
*[download_single(url) for url in image_urls],
return_exceptions=True
)
# 过滤结果
downloaded = {}
for result in results:
if isinstance(result, tuple):
original_url, local_path = result
downloaded[original_url] = local_path
logger.info(f"Successfully downloaded {len(downloaded)} images")
return downloaded
async def _download_image(self, session: aiohttp.ClientSession, url: str) -> tuple[str, str]:
"""
下载单个图片
Args:
session: aiohttp 会话
url: 图片URL
Returns:
tuple: (原始URL, 本地路径)
"""
try:
# 生成文件名
file_hash = hashlib.md5(url.encode()).hexdigest()
file_ext = self._get_file_extension(url)
file_path = self.image_dir / f"{file_hash}{file_ext}"
# 如果文件已存在,直接返回
if file_path.exists():
logger.debug(f"Image already exists: {file_path}")
return url, str(file_path)
# 发起请求
async with session.get(url) as response:
# 跳过认证错误和其他客户端错误
if response.status in [401, 403]:
logger.warning(f"Skipping image {url}: Authentication required (HTTP {response.status})")
return url, url # 返回原始URL
if response.status != 200:
logger.warning(f"Failed to download image {url}: HTTP {response.status}")
return url, url # 返回原始URL
# 读取图片数据
content = await response.read()
# 验证图片格式
if not self._is_valid_image(content):
logger.warning(f"Invalid image format for {url}")
return url, url
# 优化图片(可选)
optimized_content = await self._optimize_image(content)
# 保存到本地
with open(file_path, 'wb') as f:
f.write(optimized_content)
logger.debug(f"Downloaded image: {file_path}")
return url, str(file_path)
except asyncio.TimeoutError:
logger.error(f"Timeout downloading image: {url}")
return url, url
except Exception as e:
logger.error(f"Error downloading image {url}: {e}")
return url, url
def _get_file_extension(self, url: str) -> str:
"""
从 URL 获取文件扩展名
Args:
url: 图片URL
Returns:
str: 文件扩展名
"""
parsed = urlparse(url)
path = parsed.path
# 常见的图片扩展名
extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp']
for ext in extensions:
if path.lower().endswith(ext):
return ext
# 默认扩展名
return '.png'
def _is_valid_image(self, content: bytes) -> bool:
"""
验证是否为有效的图片
Args:
content: 图片二进制数据
Returns:
bool: 是否为有效图片
"""
# 检查文件大小(太小或太大都认为无效)
if len(content) < 100 or len(content) > 10 * 1024 * 1024:
return False
# 检查文件头
image_signatures = {
b'\x89PNG': 'png',
b'\xff\xd8\xff': 'jpeg',
b'GIF8': 'gif',
b'RIFF': 'webp', # WebP 文件头
b'BM': 'bmp'
}
for signature in image_signatures:
if content.startswith(signature):
return True
return False
async def _optimize_image(self, content: bytes) -> bytes:
"""
优化图片(压缩、转换格式等)
Args:
content: 原始图片数据
Returns:
bytes: 优化后的图片数据
"""
try:
# 如果内容太大,进行压缩
if len(content) > 1024 * 1024: # 大于1MB
image = Image.open(io.BytesIO(content))
# 如果是 RGBA,转换为 RGB(JPEG 不支持透明度)
if image.mode == 'RGBA':
# 创建白色背景
background = Image.new('RGB', image.size, (255, 255, 255))
background.paste(image, mask=image.split()[-1])
image = background
elif image.mode != 'RGB':
image = image.convert('RGB')
# 限制最大尺寸
max_size = (1920, 1080)
image.thumbnail(max_size, Image.Resampling.LANCZOS)
# 保存为 JPEG(质量85)
output = io.BytesIO()
image.save(output, format='JPEG', quality=85, optimize=True)
return output.getvalue()
return content
except Exception as e:
logger.warning(f"Failed to optimize image: {e}")
return content
async def download_single_image(self, url: str, session: aiohttp.ClientSession) -> str:
"""
下载单个图片并返回本地路径
Args:
url: 图片URL
session: aiohttp 会话
Returns:
str: 本地文件路径
"""
_, local_path = await self._download_image(session, url)
return local_path
def get_image_info(self, image_path: str) -> Dict:
"""
获取图片信息
Args:
image_path: 图片路径
Returns:
Dict: 图片信息(尺寸、大小等)
"""
try:
path = Path(image_path)
if not path.exists():
return {}
image = Image.open(path)
return {
"path": str(path),
"width": image.width,
"height": image.height,
"mode": image.mode,
"format": image.format,
"size": path.stat().st_size
}
except Exception as e:
logger.error(f"Error getting image info for {image_path}: {e}")
return {}
async def cleanup(self):
"""清理临时图片文件"""
try:
# 删除7天前的临时图片
import time
now = time.time()
seven_days_ago = now - 7 * 24 * 60 * 60
for file_path in self.image_dir.glob('*'):
if file_path.is_file():
mtime = file_path.stat().st_mtime
if mtime < seven_days_ago:
file_path.unlink()
logger.debug(f"Cleaned up old image: {file_path}")
except Exception as e:
logger.error(f"Error cleaning up images: {e}")
"""飞书文档内容解析器"""
import re
import logging
from typing import List, Dict, Optional, Tuple
from bs4 import BeautifulSoup, Tag, NavigableString
from urllib.parse import urljoin, urlparse
logger = logging.getLogger(__name__)
class FeishuParser:
"""飞书文档 HTML 解析器"""
def __init__(self):
# 映射图片URL到本地路径
self.image_map: Dict[str, str] = {}
def parse_html(self, html: str) -> List[Dict]:
"""
解析 HTML 为结构化数据
Args:
html: HTML 字符串
Returns:
List[Dict]: 内容块列表
"""
soup = BeautifulSoup(html, 'html.parser')
blocks = []
# 处理所有内容块
for element in soup.find_all(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'blockquote', 'pre', 'table']):
block = self._parse_element(element)
if block:
blocks.append(block)
return blocks
def _parse_element(self, element) -> Optional[Dict]:
"""
解析单个元素
Args:
element: BeautifulSoup 元素
Returns:
Dict: 解析后的内容块
"""
tag_name = element.name.lower()
# 标题
if tag_name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']:
level = int(tag_name[1])
text = self._clean_text(element)
return {
'type': 'heading',
'level': level,
'text': text
}
# 段落
if tag_name == 'p':
text = self._clean_text(element)
if text.strip():
return {
'type': 'paragraph',
'text': text
}
# 列表
if tag_name in ['ul', 'ol']:
items = []
for li in element.find_all('li', recursive=False):
item_text = self._clean_text(li)
if item_text.strip():
items.append(item_text)
return {
'type': 'list',
'ordered': tag_name == 'ol',
'items': items
}
# 引用
if tag_name == 'blockquote':
text = self._clean_text(element)
return {
'type': 'quote',
'text': text
}
# 代码块
if tag_name == 'pre':
code_text = element.get_text(strip=False)
return {
'type': 'code',
'language': self._detect_language(element),
'text': code_text
}
# 表格
if tag_name == 'table':
return self._parse_table(element)
# 图片
if tag_name == 'img':
src = element.get('src', '')
alt = element.get('alt', '')
return {
'type': 'image',
'src': src,
'alt': alt
}
# 链接
if tag_name == 'a':
href = element.get('href', '')
text = self._clean_text(element)
return {
'type': 'link',
'href': href,
'text': text
}
return None
def _parse_table(self, table) -> Dict:
"""
解析表格
Args:
table: 表格元素
Returns:
Dict: 表格数据
"""
rows = []
# 获取表头
thead = table.find('thead')
header_row = None
if thead:
header_row = thead.find('tr')
if header_row:
headers = [self._clean_text(th) for th in header_row.find_all(['th', 'td'])]
rows.append(headers)
# 获取表格内容
tbody = table.find('tbody')
if tbody:
for tr in tbody.find_all('tr'):
row = [self._clean_text(td) for td in tr.find_all(['td', 'th'])]
if row:
rows.append(row)
# 如果没有 tbody,直接解析所有行
if not tbody:
for tr in table.find_all('tr'):
row = [self._clean_text(td) for td in tr.find_all(['td', 'th'])]
if row:
rows.append(row)
return {
'type': 'table',
'rows': rows
}
def _clean_text(self, element) -> str:
"""
清理元素文本
Args:
element: BeautifulSoup 元素
Returns:
str: 清理后的文本
"""
# 获取纯文本,保留换行
text = element.get_text(separator=' ', strip=True)
# 清理多余的空白字符
text = re.sub(r'\s+', ' ', text)
return text
def _detect_language(self, element) -> str:
"""
检测代码语言
Args:
element: 代码块元素
Returns:
str: 语言名称
"""
# 尝试从 class 或其他属性获取语言信息
classes = element.get('class', [])
for cls in classes:
if 'language-' in cls:
return cls.replace('language-', '')
# 尝试从父元素获取
parent = element.parent
if parent:
classes = parent.get('class', [])
for cls in classes:
if 'language-' in cls:
return cls.replace('language-', '')
return ''
def generate_markdown(self, blocks: List[Dict], image_map: Dict[str, str]) -> str:
"""
生成 Markdown
Args:
blocks: 内容块列表
image_map: 图片映射 {URL: 本地路径}
Returns:
str: Markdown 字符串
"""
markdown_parts = []
self.image_map = image_map
for block in blocks:
md = self._block_to_markdown(block)
if md:
markdown_parts.append(md)
return '\n\n'.join(markdown_parts)
def _block_to_markdown(self, block: Dict) -> str:
"""
将内容块转换为 Markdown
Args:
block: 内容块
Returns:
str: Markdown 字符串
"""
block_type = block.get('type')
if block_type == 'heading':
level = block.get('level', 1)
text = block.get('text', '')
return f"{'#' * level} {text}"
if block_type == 'paragraph':
return block.get('text', '')
if block_type == 'list':
items = block.get('items', [])
ordered = block.get('ordered', False)
md_items = []
for i, item in enumerate(items):
if ordered:
md_items.append(f"{i + 1}. {item}")
else:
md_items.append(f"- {item}")
return '\n'.join(md_items)
if block_type == 'quote':
text = block.get('text', '')
# 添加 > 前缀
lines = text.split('\n')
quoted_lines = [f"> {line}" if line.strip() else ">" for line in lines]
return '\n'.join(quoted_lines)
if block_type == 'code':
language = block.get('language', '')
text = block.get('text', '')
if language:
return f"```{language}\n{text}\n```"
else:
return f"```\n{text}\n```"
if block_type == 'table':
return self._table_to_markdown(block)
if block_type == 'image':
src = block.get('src', '')
alt = block.get('alt', '')
# 获取本地路径
local_path = self.image_map.get(src, src)
if alt:
return f""
else:
return f""
if block_type == 'link':
href = block.get('href', '')
text = block.get('text', '')
if text == href:
return f"<{href}>"
else:
return f"[{text}]({href})"
return ''
def _table_to_markdown(self, block: Dict) -> str:
"""
将表格转换为 Markdown
Args:
block: 表格数据
Returns:
str: Markdown 表格
"""
rows = block.get('rows', [])
if not rows:
return ''
markdown_lines = []
# 表头
if rows:
markdown_lines.append('| ' + ' | '.join(rows[0]) + ' |')
# 分隔线
if rows:
markdown_lines.append('| ' + ' | '.join(['---'] * len(rows[0])) + ' |')
# 表格内容
for row in rows[1:]:
markdown_lines.append('| ' + ' | '.join(row) + ' |')
return '\n'.join(markdown_lines)
def extract_text(self, blocks: List[Dict]) -> str:
"""
提取纯文本
Args:
blocks: 内容块列表
Returns:
str: 纯文本
"""
text_parts = []
for block in blocks:
block_type = block.get('type')
if block_type == 'heading':
text_parts.append(block.get('text', ''))
elif block_type == 'paragraph':
text_parts.append(block.get('text', ''))
elif block_type == 'list':
items = block.get('items', [])
text_parts.extend(items)
elif block_type == 'quote':
text_parts.append(block.get('text', ''))
elif block_type == 'code':
text_parts.append(block.get('text', ''))
elif block_type == 'table':
rows = block.get('rows', [])
for row in rows:
text_parts.extend(row)
elif block_type == 'image':
alt = block.get('alt', '')
if alt:
text_parts.append(alt)
return ' '.join(text_parts)
def extract_images(self, blocks: List[Dict]) -> List[str]:
"""
提取所有图片URL
Args:
blocks: 内容块列表
Returns:
List[str]: 图片URL列表
"""
images = []
for block in blocks:
if block.get('type') == 'image':
src = block.get('src', '')
if src:
images.append(src)
return images
"""飞书文档抓取器"""
import asyncio
import re
import json
from typing import List, Dict, Optional, Tuple
from pathlib import Path
from urllib.parse import urljoin, urlparse
import logging
try:
from playwright.async_api import async_playwright, Browser, Page, BrowserContext
except ImportError:
logging.error("playwright not installed. Run: pip install playwright")
raise
try:
from .image_handler import ImageHandler
from .parser import FeishuParser
except ImportError:
from image_handler import ImageHandler
from parser import FeishuParser
logger = logging.getLogger(__name__)
class FeishuScraper:
"""飞书文档抓取器"""
def __init__(self, cookies_str: Optional[str] = None):
"""
初始化飞书抓取器
Args:
cookies_str: 可选的cookie字符串,格式: "key1=value1; key2=value2"
"""
self.playwright = None
self.browser = None
self.context = None
self.image_handler = ImageHandler()
self.parser = FeishuParser()
self.temp_dir = Path("/tmp/feishu_docs")
self.temp_dir.mkdir(parents=True, exist_ok=True)
self.cookies_str = cookies_str
async def __aenter__(self):
"""异步上下文管理器入口"""
await self.init()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""异步上下文管理器退出"""
await self.cleanup()
async def init(self):
"""初始化 Playwright"""
if not self.playwright:
self.playwright = await async_playwright().start()
self.browser = await self.playwright.chromium.launch(
headless=True,
args=[
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu'
]
)
# 准备cookies(如果提供)
cookies = None
if self.cookies_str:
cookies = []
for cookie_pair in self.cookies_str.split(';'):
if '=' in cookie_pair:
key, value = cookie_pair.strip().split('=', 1)
cookies.append({
'name': key,
'value': value,
'domain': '.feishu.cn'
})
self.context = await self.browser.new_context(
viewport={'width': 1920, 'height': 1080},
extra_http_headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
},
cookies=cookies
)
async def cleanup(self):
"""清理资源"""
if self.context:
await self.context.close()
if self.browser:
await self.browser.close()
if self.playwright:
await self.playwright.stop()
async def fetch_doc(self, url: str) -> dict:
"""
获取飞书文档并转换为 Markdown
Args:
url: 飞书文档URL
Returns:
dict: 包含标题、内容、作者、图片等信息的字典
"""
page = await self.context.new_page()
try:
# 1. 打开页面
logger.info(f"Navigating to: {url}")
await page.goto(url, wait_until='domcontentloaded', timeout=60000)
# 2. 等待内容加载
logger.info("Waiting for content to load...")
# 初始等待5秒
await page.wait_for_timeout(5000)
# 等待加载指示器消失
try:
await page.wait_for_function(
"""() => {
const loaders = document.querySelectorAll('.loading, .spinner, [class*="loading"]');
return loaders.length === 0;
}""",
timeout=10000
)
except:
logger.warning("Could not find/disable loading indicator, continuing...")
# 3. 获取基本信息
title = await self._extract_title(page)
author = await self._extract_author(page)
# 4. 等待文档内容加载
logger.info("Waiting for document content to appear...")
# 等待动态渲染完成 - 使用 body 文本长度作为指标
max_wait_time = 30000 # 30秒
check_interval = 1000 # 每秒检查
waited_time = 0
while waited_time < max_wait_time:
body_text = await page.evaluate("() => document.body.innerText.trim()")
if len(body_text) > 500: # 如果body文本超过500字符,认为内容已加载
logger.info(f"Document content detected, body text length: {len(body_text)}")
break
await page.wait_for_timeout(check_interval)
waited_time += check_interval
if waited_time >= max_wait_time:
logger.warning("Document content not fully loaded within timeout, proceeding with available content")
# 5. 提取内容
content_html = await page.evaluate("""
() => {
// 首先尝试直接提取 body.innerText 作为备用
const bodyText = document.body.innerText.trim();
if (bodyText.length > 500) {
console.log('Using body.innerText as primary source, length:', bodyText.length);
// 注意:不进行内容过滤,保留所有原始文本
// 如果需要清理噪音内容,应该在前端或后处理阶段根据实际需求处理
const cleanText = bodyText;
// 将清理后的文本转换为HTML,更好地保留段落结构
let paragraphs = [];
// 先按双换行分割
const parts = cleanText.split('\\n\\n');
for (let part of parts) {
part = part.trim();
if (part.length === 0) continue;
// 过滤掉太短的段落
if (part.length < 10) continue;
// 如果这个部分太长,进一步按单换行分割
if (part.length > 300) {
const subParts = part.split('\\n');
let currentPara = '';
for (let subPart of subParts) {
subPart = subPart.trim();
if (subPart.length === 0) {
if (currentPara.length > 0) {
paragraphs.push(currentPara);
currentPara = '';
}
} else {
currentPara += (currentPara ? ' ' : '') + subPart;
}
}
if (currentPara.length > 0) {
paragraphs.push(currentPara);
}
} else {
paragraphs.push(part);
}
}
if (paragraphs.length > 0) {
return paragraphs.map(p => '<p>' + p.replace(/\\n/g, ' ').replace(/\\s+/g, ' ').trim() + '</p>').join('\\n');
}
}
// 查找文档内容容器
const contentSelectors = [
'div[contenteditable="true"]',
'.larkui-theme-default',
'.document',
'.feishu-docs-content',
'.page-container',
'.wiki-page',
'main',
'[data-docx-id]'
];
for (const selector of contentSelectors) {
const elements = document.querySelectorAll(selector);
for (const element of elements) {
if (element.innerText.trim().length > 100) { // 降低阈值到100字符
console.log('Found content with selector:', selector, 'length:', element.innerText.length);
return element.innerHTML;
}
}
}
// 如果都找不到,使用包含最多文本的容器
const allElements = document.querySelectorAll('body > *');
let maxText = 0;
let result = '';
for (const element of allElements) {
const text = element.innerText.trim();
if (text.length > maxText && text.length > 100) {
maxText = text.length;
result = element.innerHTML;
}
}
console.log('Using fallback, max text length:', maxText);
return result || document.body.innerHTML;
}
""")
# 6. 提取图片URL
logger.info("Extracting images...")
image_urls = await self._extract_image_urls(page)
logger.info(f"Found {len(image_urls)} image URLs")
# 7. 解析内容为结构化数据
content_blocks = self.parser.parse_html(content_html)
# 8. 下载图片
logger.info(f"Downloading {len(image_urls)} images...")
local_images = await self.image_handler.download_images(image_urls)
# 9. 生成 Markdown
markdown_content = self.parser.generate_markdown(
content_blocks,
local_images
)
# 10. 计算字数
word_count = len(markdown_content.replace('\n', '').replace(' ', ''))
logger.info(f"Successfully processed document: {title}")
logger.info(f"Word count: {word_count}, Images: {len(local_images)}")
return {
"success": True,
"title": title,
"author": author or "Unknown",
"content": markdown_content,
"images": local_images,
"word_count": word_count,
"image_count": len(local_images),
"url": url
}
except Exception as e:
logger.error(f"Error fetching document: {e}", exc_info=True)
return {
"success": False,
"error": str(e)
}
finally:
await page.close()
async def get_doc_info(self, url: str) -> dict:
"""
获取文档基本信息(不下载完整内容)
Args:
url: 飞书文档URL
Returns:
dict: 基本信息
"""
page = await self.context.new_page()
try:
await page.goto(url, wait_until='networkidle', timeout=30000)
await page.wait_for_timeout(2000)
title = await self._extract_title(page)
author = await self._extract_author(page)
# 简单估算字数
word_count = await page.evaluate("""
() => {
const text = document.body.innerText || '';
return text.replace(/\\s+/g, '').length;
}
""")
# 简单估算图片数
image_count = await page.evaluate("""
() => {
return document.querySelectorAll('img').length;
}
""")
return {
"success": True,
"title": title,
"author": author or "Unknown",
"word_count": word_count,
"image_count": image_count
}
except Exception as e:
logger.error(f"Error getting doc info: {e}", exc_info=True)
return {
"success": False,
"error": str(e)
}
finally:
await page.close()
async def _extract_title(self, page: Page) -> str:
"""提取文档标题"""
try:
title = await page.title()
if title:
return title.strip()
except:
pass
try:
title = await page.evaluate("""
() => {
// 尝试多种方式获取标题
const titleSelectors = [
'h1',
'.document-title',
'.title',
'[data-testid="title"]'
];
for (const selector of titleSelectors) {
const element = document.querySelector(selector);
if (element && element.innerText.trim()) {
return element.innerText.trim();
}
}
// 如果都找不到,使用页面标题
return document.title || 'Unknown Title';
}
""")
return title if title else "Unknown Title"
except:
return "Unknown Title"
async def _extract_author(self, page: Page) -> Optional[str]:
"""提取作者"""
try:
author = await page.evaluate("""
() => {
// 尝试多种方式获取作者
const authorSelectors = [
'.author',
'.creator',
'[data-testid="author"]',
'.user-avatar'
];
for (const selector of authorSelectors) {
const element = document.querySelector(selector);
if (element) {
const text = element.innerText || element.getAttribute('title');
if (text && text.trim()) {
return text.trim();
}
}
}
return null;
}
""")
return author if author else None
except:
return None
async def _extract_image_urls(self, page: Page) -> List[str]:
"""提取页面中的所有图片URL"""
try:
urls = await page.evaluate("""
() => {
const images = document.querySelectorAll('img');
const urls = [];
for (const img of images) {
let src = img.getAttribute('src') || img.getAttribute('data-src') || img.getAttribute('data-lazy-src');
if (src) {
// 处理相对路径
if (src.startsWith('/')) {
src = window.location.origin + src;
}
// 过滤掉小图标和头像
const lowerSrc = src.toLowerCase();
if (!lowerSrc.includes('icon') &&
!lowerSrc.includes('avatar') &&
!lowerSrc.includes('logo') &&
!lowerSrc.includes('favicon')) {
urls.push(src);
}
}
}
// 去重
return [...new Set(urls)];
}
""")
logger.info(f"Extracted {len(urls)} image URLs")
return urls if urls else []
except Exception as e:
logger.error(f"Error extracting image URLs: {e}")
return []
# 便捷函数
async def fetch_feishu_doc(url: str, cookies_str: Optional[str] = None) -> dict:
"""
便捷函数:直接获取飞书文档
Args:
url: 飞书文档URL
cookies_str: 可选的cookie字符串,格式: "key1=value1; key2=value2"
"""
async with FeishuScraper(cookies_str=cookies_str) as scraper:
return await scraper.fetch_doc(url)
async def get_feishu_doc_info(url: str, cookies_str: Optional[str] = None) -> dict:
"""
便捷函数:获取文档基本信息
Args:
url: 飞书文档URL
cookies_str: 可选的cookie字符串
"""
async with FeishuScraper(cookies_str=cookies_str) as scraper:
return await scraper.get_doc_info(url)
"""MCP服务器主入口"""
import sys
from pathlib import Path
# 添加src目录到Python路径,支持直接运行
if __name__ == "__main__":
src_dir = Path(__file__).parent
if str(src_dir) not in sys.path:
sys.path.insert(0, str(src_dir))
from fastmcp import FastMCP
import logging
# 支持相对导入和绝对导入
try:
from .scraper import FeishuScraper
except ImportError:
from scraper import FeishuScraper
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 初始化MCP服务
mcp = FastMCP("feishu-reader")
# 初始化爬虫(全局单例)
scraper = FeishuScraper()
@mcp.tool()
async def read_feishu_doc(url: str, cookies_str: Optional[str] = None) -> dict:
"""
读取飞书文档内容并转换为 Markdown(支持图片)
Args:
url: 飞书文档URL,格式:
- https://bytedance.feishu.cn/docs/docc/xxx
- https://xxx.feishu.cn/docs/docc/xxx
cookies_str: 可选的cookie字符串,格式: "key1=value1; key2=value2"
如果提供,可以下载需要认证的图片
Returns:
dict: {
"success": bool,
"title": str,
"author": str,
"content": str, # Markdown 格式
"images": List[str], # 本地图片路径列表
"word_count": int,
"error": str | None
}
"""
try:
# URL验证
if not ("feishu.cn" in url or "feishu.com" in url):
return {
"success": False,
"error": "Invalid URL format. Must be a Feishu document URL."
}
logger.info(f"Fetching Feishu document: {url}")
# 使用cookie创建新的scraper实例
scraper_instance = FeishuScraper(cookies_str=cookies_str)
await scraper_instance.init()
try:
# 调用爬虫获取内容
result = await scraper_instance.fetch_doc(url)
if result.get("success"):
logger.info(f"Successfully fetched: {result.get('title', 'Unknown')}")
logger.info(f"Extracted {result.get('word_count', 0)} words and {len(result.get('images', []))} images")
else:
logger.error(f"Failed to fetch: {result.get('error')}")
return result
finally:
await scraper_instance.cleanup()
except Exception as e:
logger.error(f"Error fetching Feishu document: {e}", exc_info=True)
return {
"success": False,
"error": str(e)
}
@mcp.tool()
async def get_doc_info(url: str) -> dict:
"""
获取飞书文档的基本信息(不下载内容)
Args:
url: 飞书文档URL
Returns:
dict: {
"success": bool,
"title": str,
"author": str,
"word_count": int,
"image_count": int,
"error": str | None
}
"""
try:
result = await scraper.get_doc_info(url)
return result
except Exception as e:
logger.error(f"Error getting doc info: {e}", exc_info=True)
return {
"success": False,
"error": str(e)
}
# 清理函数
async def cleanup():
"""清理资源"""
await scraper.cleanup()
if __name__ == "__main__":
# 支持命令行直接运行
mcp.run()
#!/usr/bin/env python3
"""测试脚本"""
import asyncio
import sys
from pathlib import Path
# 添加 src 目录到路径
src_dir = Path(__file__).parent / 'src'
sys.path.insert(0, str(src_dir))
from scraper import FeishuScraper
async def test_basic():
"""测试基本功能"""
print("=" * 50)
print("测试 1:测试基本导入")
print("=" * 50)
try:
from scraper import FeishuScraper
from parser import FeishuParser
from image_handler import ImageHandler
print("✓ 所有模块导入成功")
return True
except ImportError as e:
print(f"✗ 模块导入失败: {e}")
return False
async def test_scraper_init():
"""测试爬虫初始化"""
print("\n" + "=" * 50)
print("测试 2:测试爬虫初始化")
print("=" * 50)
try:
scraper = FeishuScraper()
await scraper.init()
print("✓ 爬虫初始化成功")
await scraper.cleanup()
return True
except Exception as e:
print(f"✗ 爬虫初始化失败: {e}")
return False
async def test_parser():
"""测试解析器"""
print("\n" + "=" * 50)
print("测试 3:测试解析器")
print("=" * 50)
try:
from parser import FeishuParser
parser = FeishuParser()
# 测试 HTML 解析
html = """
<h1>测试标题</h1>
<p>这是一个测试段落。</p>
<ul>
<li>列表项 1</li>
<li>列表项 2</li>
</ul>
"""
blocks = parser.parse_html(html)
print(f"✓ 解析了 {len(blocks)} 个内容块")
# 测试 Markdown 生成
markdown = parser.generate_markdown(blocks, {})
print("✓ Markdown 生成成功")
print("\n生成的 Markdown:")
print(markdown)
return True
except Exception as e:
print(f"✗ 解析器测试失败: {e}")
import traceback
traceback.print_exc()
return False
async def test_image_handler():
"""测试图片处理器"""
print("\n" + "=" * 50)
print("测试 4:测试图片处理器")
print("=" * 50)
try:
from image_handler import ImageHandler
handler = ImageHandler()
print(f"✓ 图片处理器初始化成功,目录: {handler.image_dir}")
return True
except Exception as e:
print(f"✗ 图片处理器测试失败: {e}")
return False
async def main():
"""主测试函数"""
print("\n" + "=" * 50)
print("飞书文档读取 MCP 服务器测试")
print("=" * 50)
tests = [
test_basic,
test_scraper_init,
test_parser,
test_image_handler
]
passed = 0
failed = 0
for test in tests:
try:
if await test():
passed += 1
else:
failed += 1
except Exception as e:
print(f"\n✗ 测试异常: {e}")
import traceback
traceback.print_exc()
failed += 1
print("\n" + "=" * 50)
print("测试总结")
print("=" * 50)
print(f"✓ 通过: {passed}")
print(f"✗ 失败: {failed}")
print(f"总计: {passed + failed}")
if failed == 0:
print("\n🎉 所有测试通过!")
return 0
else:
print(f"\n⚠️ {failed} 个测试失败")
return 1
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)
#!/bin/bash
# qiaomu-anything-to-notebooklm Skill Installer
# 自动安装所有依赖并配置环境
set -e # 遇到错误立即退出
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_NAME="qiaomu-anything-to-notebooklm"
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} 多源内容 → NotebookLM 安装程序${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
# 1. 检查 Python 版本
echo -e "${YELLOW}[1/6] 检查 Python 环境...${NC}"
if ! command -v python3 &> /dev/null; then
echo -e "${RED}❌ 未找到 Python3,请先安装 Python 3.9+${NC}"
exit 1
fi
PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
REQUIRED_VERSION="3.9"
if [ "$(printf '%s\n' "$REQUIRED_VERSION" "$PYTHON_VERSION" | sort -V | head -n1)" != "$REQUIRED_VERSION" ]; then
echo -e "${RED}❌ Python 版本过低(当前 $PYTHON_VERSION,需要 3.9+)${NC}"
exit 1
fi
echo -e "${GREEN}✅ Python $PYTHON_VERSION${NC}"
# 2. 检查并克隆 wexin-read-mcp
echo ""
echo -e "${YELLOW}[2/6] 安装 MCP 服务器...${NC}"
MCP_DIR="$SKILL_DIR/wexin-read-mcp"
if [ -d "$MCP_DIR" ]; then
echo -e "${GREEN}✅ MCP 服务器已存在${NC}"
else
echo "正在克隆 wexin-read-mcp..."
git clone https://github.com/Bwkyd/wexin-read-mcp.git "$MCP_DIR"
echo -e "${GREEN}✅ MCP 服务器克隆完成${NC}"
fi
# 3. 安装 Python 依赖
echo ""
echo -e "${YELLOW}[3/6] 安装 Python 依赖...${NC}"
# 安装 MCP 服务器依赖
if [ -f "$MCP_DIR/requirements.txt" ]; then
echo "安装 MCP 依赖..."
pip3 install -r "$MCP_DIR/requirements.txt" -q
echo -e "${GREEN}✅ MCP 依赖安装完成${NC}"
fi
# 安装 Skill 依赖(包括 markitdown)
if [ -f "$SKILL_DIR/requirements.txt" ]; then
echo "安装 Skill 依赖(包括 markitdown 文件转换工具)..."
pip3 install -r "$SKILL_DIR/requirements.txt" -q
echo -e "${GREEN}✅ Skill 依赖安装完成${NC}"
echo -e "${GREEN}✅ markitdown 已安装(支持 15+ 文件格式转换)${NC}"
fi
# 4. 安装 Playwright 浏览器
echo ""
echo -e "${YELLOW}[4/6] 安装 Playwright 浏览器...${NC}"
echo "这可能需要几分钟,请耐心等待..."
if python3 -c "from playwright.sync_api import sync_playwright" 2>/dev/null; then
playwright install chromium
echo -e "${GREEN}✅ Playwright 浏览器安装完成${NC}"
else
echo -e "${RED}❌ Playwright 导入失败,请检查安装${NC}"
exit 1
fi
# 5. 检查并安装 notebooklm
echo ""
echo -e "${YELLOW}[5/6] 检查 NotebookLM CLI...${NC}"
if command -v notebooklm &> /dev/null; then
NOTEBOOKLM_VERSION=$(notebooklm --version 2>/dev/null || echo "unknown")
echo -e "${GREEN}✅ NotebookLM CLI 已安装 ($NOTEBOOKLM_VERSION)${NC}"
else
echo "正在安装 notebooklm-py..."
pip3 install git+https://github.com/teng-lin/notebooklm-py.git -q
if command -v notebooklm &> /dev/null; then
echo -e "${GREEN}✅ NotebookLM CLI 安装完成${NC}"
else
echo -e "${RED}❌ NotebookLM CLI 安装失败${NC}"
echo "请手动安装:pip3 install git+https://github.com/teng-lin/notebooklm-py.git"
exit 1
fi
fi
# 6. 配置指导
echo ""
echo -e "${YELLOW}[6/6] 配置指导${NC}"
echo ""
CLAUDE_CONFIG="$HOME/.claude/config.json"
CONFIG_SNIPPET=" \"weixin-reader\": {
\"command\": \"python\",
\"args\": [
\"$MCP_DIR/src/server.py\"
]
}"
echo -e "${BLUE}📝 下一步:配置 MCP 服务器${NC}"
echo ""
echo "请编辑 $CLAUDE_CONFIG"
echo ""
echo "在 \"mcpServers\" 中添加:"
echo -e "${GREEN}$CONFIG_SNIPPET${NC}"
echo ""
echo "完整配置示例:"
echo -e "${GREEN}{
\"primaryApiKey\": \"any\",
\"mcpServers\": {
$CONFIG_SNIPPET
}
}${NC}"
echo ""
# 检查是否已配置
if [ -f "$CLAUDE_CONFIG" ]; then
if grep -q "weixin-reader" "$CLAUDE_CONFIG"; then
echo -e "${GREEN}✅ 检测到已有 weixin-reader 配置${NC}"
else
echo -e "${YELLOW}⚠️ 未检测到 weixin-reader 配置,请手动添加${NC}"
fi
else
echo -e "${YELLOW}⚠️ 未找到 Claude 配置文件,请手动创建${NC}"
fi
echo ""
echo -e "${BLUE}🔐 NotebookLM 认证${NC}"
echo ""
echo "首次使用前,请运行:"
echo -e "${GREEN} notebooklm login${NC}"
echo -e "${GREEN} notebooklm list # 验证认证成功${NC}"
echo ""
# 最终检查
echo ""
echo -e "${BLUE}========================================${NC}"
echo -e "${GREEN}✅ 安装完成!${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
echo "📦 安装位置:$SKILL_DIR"
echo ""
echo "⚠️ 重要提醒:"
echo " 1. 配置 MCP 服务器后需要重启 Claude Code"
echo " 2. 首次使用前运行 notebooklm login"
echo ""
echo "🚀 使用示例:"
echo " 把这篇文章生成播客 https://mp.weixin.qq.com/s/xxx"
echo ""
MIT License
Copyright (c) 2026 Joe
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
#!/usr/bin/env python3
"""qiaomu-anything-to-notebooklm - 多源内容智能处理器
自动识别输入类型,上传到 NotebookLM 并生成指定格式
支持深度分析模式:三轮递进提问(概览→深度挖掘→综合反刍)
"""
import sys
import os
import subprocess
import tempfile
import json
import time
import re
from pathlib import Path
def detect_input_type(input_path):
"""检测输入类型"""
if input_path.startswith('http'):
if 'mp.weixin.qq.com' in input_path:
return 'weixin'
elif 'youtube.com' in input_path or 'youtu.be' in input_path:
return 'youtube'
elif 'xiaoyuzhoufm.com' in input_path or 'ximalaya.com' in input_path or 'bilibili.com' in input_path:
return 'podcast'
elif 'x.com' in input_path or 'twitter.com' in input_path:
return 'x_twitter'
else:
return 'url'
path = Path(input_path).expanduser()
if not path.exists():
return 'search' # 不是文件路径,当作搜索关键词
suffix = path.suffix.lower()
if suffix == '.epub':
return 'epub'
elif suffix in ['.pdf', '.txt', '.md']:
return 'document'
elif suffix in ['.docx', '.pptx', '.xlsx']:
return 'office'
elif suffix in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
return 'image'
elif suffix in ['.mp3', '.wav']:
return 'audio'
elif suffix == '.zip':
return 'zip'
else:
return 'unknown'
def extract_epub_to_txt(epub_path):
"""提取 EPUB 到 TXT"""
import ebooklib
from ebooklib import epub
from bs4 import BeautifulSoup
book = epub.read_epub(str(epub_path))
content = []
for item in book.get_items():
if item.get_type() == ebooklib.ITEM_DOCUMENT:
soup = BeautifulSoup(item.get_content(), 'html.parser')
content.append(soup.get_text())
# 保存到临时文件
txt_path = tempfile.mktemp(suffix='.txt', prefix='epub_')
with open(txt_path, 'w', encoding='utf-8') as f:
f.write('\n\n'.join(content))
return txt_path
def upload_to_notebooklm(file_path, title):
"""上传文件到 NotebookLM"""
# 创建笔记本
result = subprocess.run(
['notebooklm', 'create', title],
capture_output=True,
text=True
)
if result.returncode != 0:
print(f"❌ 创建笔记本失败: {result.stderr}", file=sys.stderr)
return False
# 上传文件
result = subprocess.run(
['notebooklm', 'source', 'add', file_path, '--title', title],
capture_output=True,
text=True
)
if result.returncode != 0:
print(f"❌ 上传文件失败: {result.stderr}", file=sys.stderr)
return False
print(f"✅ 已上传到 NotebookLM: {title}")
return True
def label_for(content_type):
"""根据内容类型返回合适的中文指代词"""
labels = {
'epub': '本书',
'document': '这份文档',
'podcast': '这期播客',
'x_twitter': '这条推文',
'youtube': '这个视频',
'url': '这篇文章',
'weixin': '这篇文章',
'search': '这份内容',
}
return labels.get(content_type, '这份内容')
def generate_questions_progressive(content_type):
"""
生成三轮回合递进的深度问题。
本函数与具体内容解耦,不使用 {title} 等占位符,
统一用 label_for(content_type) 生成的指代词(如"本书""这个视频")。
设计原则:
- 第一轮(4题):建立整体认知框架
- 第二轮(5题):深入挖掘细节与矛盾
- 第三轮(3题):综合反刍与认知升级
- NotebookLM 在同一 conversation 中保持上下文,后续回合受益于前序回答
问题设计技巧:
- "请基于提供的文档内容回答" 防止 NotebookLM 触发网络搜索
- "列出、拆解、指出、提取" 等动作词引导结构化回答
- 避免 yes/no 式问题
"""
name = label_for(content_type)
# ── 第一轮:概览与框架 ──
round1 = [
f"请用一段话概括{name}的核心主题和写作目的。注意:完全基于已上传的文档内容回答,不要搜索网络。",
f"{name}的整体结构是什么?请按章节或逻辑模块逐一列出,每个模块用2-3句话概括核心内容。完全基于文档回答。",
f"{name}提出了哪些核心论点或主张?请逐一列出并用文档中的具体内容支撑每个论点。完全基于文档回答。",
f"{name}中最具颠覆性或反常识的内容是什么?请列出3-5条,并解释每条为什么让人意外。完全基于文档回答。",
]
# ── 第二轮:深度挖掘 ──
if content_type in ['epub', 'document']:
# 书籍/文档类:侧重论证逻辑与文本细读
round2 = [
f"请拆解{name}的核心论证逻辑:作者的前提假设是什么?推理过程是怎样的?最终结论是什么?请引用具体文本段落说明。",
f"{name}中引用了哪些关键案例、数据或文本证据?请逐一列出并说明每个证据在整体论证中起到什么作用。",
f"{name}中是否存在内部矛盾或值得商榷的观点?如果有,请指出并分析矛盾的根源。如果没有,请说明为什么论证站得住脚。",
f"{name}最独特的贡献或核心洞察是什么?如果只能用一句话概括,应该是什么?为什么这句话重要?",
f"如果要对{name}提出一个最尖锐的批评,会是什么?请从论证完整性、证据充分性、视角局限性等角度分析。",
]
elif content_type == 'youtube':
round2 = [
f"这个视频的核心论点是什么?演讲者用哪些论据来支撑?请拆解其论证结构。",
f"视频中提到了哪些具体案例、数据或研究?请逐一列出并说明它们在论证中的作用。",
f"这个视频的立场是否存在偏向或漏洞?哪些观点可能经不起推敲?",
f"这个视频最独特的信息或洞察是什么?有没有在其他地方看不到的内容?",
f"如果请一位持反对立场的专家来回应,他最可能提出的三个反驳点是什么?",
]
else:
# 文章/网页/播客/推文类:侧重叙事与观点分析
round2 = [
f"请拆解{name}的论证或叙事结构:开头如何建立框架?中间如何展开?结尾如何收束?使用了哪些修辞或论证手法?",
f"{name}中引用了哪些关键案例、数据或引用?请逐一列出并评估其可信度和相关性。",
f"{name}的立场或视角是否存在局限?有没有重要的反例或未被讨论的维度?",
f"{name}最令人印象深刻的一个洞察或观点是什么?为什么它具有冲击力?",
f"如果要给{name}的作者写一封简短的反馈信,你会提出哪三个建设性意见或质疑?",
]
# ── 第三轮:综合与反刍 ──
round3 = [
f"读完{name}后,读者最应该带走的一个认知改变是什么?哪些观点可能颠覆读者的既有认知?",
f"从{name}中可以提取出哪些可操作的行动指南、实践建议或决策原则?请列出3-5条。",
f"请用三个最有力的理由,说服一个没接触过{name}的人去认真阅读它。每个理由用一句话概括。",
]
# 合并所有轮次,每轮之间加一个分隔标识(便于后续处理和展示)
all_questions = []
all_questions.append(("【第一轮:概览与框架】", round1))
all_questions.append(("【第二轮:深度挖掘】", round2))
all_questions.append(("【第三轮:综合与反刍】", round3))
return all_questions
def ask_notebooklm(question, max_retries=1):
"""向 NotebookLM 提问并获取答案,带重试机制"""
for attempt in range(max_retries + 1):
result = subprocess.run(
['notebooklm', 'ask', question],
capture_output=True,
text=True
)
if result.returncode == 0:
answer = result.stdout.strip()
if answer and len(answer) > 10: # 有实质内容的回答才算成功
return answer
if attempt < max_retries:
print(f" 重试中...", end=" ")
time.sleep(2)
print(f"⚠️ 提问失败(已重试{max_retries}次)", file=sys.stderr)
return None
def format_feishu_markdown(title, questions, answers):
"""将问答结果格式化为飞书 Markdown"""
lines = [
f"# {title} - 深度解读",
"",
"> 本文档由 NotebookLM 深度分析生成",
"",
]
for i, (q, a) in enumerate(zip(questions, answers), 1):
lines.append(f"## {i}. {q}")
lines.append("")
if a:
lines.append(a)
else:
lines.append("*(未回答)*")
lines.append("")
return "\n".join(lines)
def create_feishu_doc(title, markdown_content):
"""创建飞书文档"""
print("\n📝 创建飞书文档...")
# 调用 lark-cli docs +create
result = subprocess.run(
['lark-cli', 'docs', '+create', '--title', title, '--markdown', markdown_content],
capture_output=True,
text=True
)
if result.returncode != 0:
print(f"❌ 创建飞书文档失败: {result.stderr}", file=sys.stderr)
return None
# 从输出中提取文档 URL(如果有)
output = result.stdout
print(f"✅ 飞书文档已创建")
print(output)
return True
def ask_round(round_label, questions, title):
"""执行一轮提问,返回 (questions, answers) 列表"""
print(f"\n📌 {round_label}")
answers = []
asked = []
for i, q in enumerate(questions, 1):
print(f" [{i}/{len(questions)}] {q[:60]}...")
answer = ask_notebooklm(q)
if answer:
print(f" ✅ 回答长度: {len(answer)} 字符")
answers.append(answer)
else:
print(f" ⚠️ 跳过")
answers.append("")
asked.append(q)
time.sleep(1.5) # 避免请求过快
return asked, answers
def deep_analysis(file_path, title, content_type, to_feishu=False):
"""深度分析模式:三轮递进提问"""
print("\n" + "="*60)
print("🔍 启动深度分析模式")
print("="*60 + "\n")
# 1. 上传到 NotebookLM
print("📤 上传内容到 NotebookLM...")
if not upload_to_notebooklm(file_path, title):
return None
print("⏳ 等待 NotebookLM 处理内容...")
time.sleep(3)
# 2. 生成三轮递进问题
print("\n📝 生成深度分析问题...")
rounds = generate_questions_progressive(content_type)
total_questions = sum(len(qs) for _, qs in rounds)
print(f"✅ 共 {len(rounds)} 轮 {total_questions} 个问题\n")
# 3. 逐轮提问(NotebookLM 保持对话上下文,后轮受益于前轮回答)
print("💬 开始三轮递进提问...\n")
all_questions = []
all_answers = []
for round_label, questions in rounds:
asked, answers = ask_round(round_label, questions, title)
all_questions.extend(asked)
all_answers.extend(answers)
# 4. 返回结构化数据
result = {
"status": "success",
"title": title,
"content_type": content_type,
"rounds": len(rounds),
"questions": all_questions,
"answers": all_answers,
"total_questions": len(all_questions),
"answered": len([a for a in all_answers if a]),
}
# 5. 如果指定了 --to-feishu,创建飞书文档
if to_feishu:
markdown = format_feishu_markdown(title, all_questions, all_answers)
create_feishu_doc(f"{title} - 深度解读", markdown)
return result
def main():
if len(sys.argv) < 2:
print("用法: main.py <输入路径或URL> [--deep-analysis] [--to-feishu]", file=sys.stderr)
sys.exit(1)
input_arg = sys.argv[1]
deep_mode = '--deep-analysis' in sys.argv
to_feishu = '--to-feishu' in sys.argv
input_type = detect_input_type(input_arg)
print(f"📋 检测到输入类型: {input_type}")
# 根据类型处理
if input_type == 'epub':
epub_path = Path(input_arg).expanduser()
print(f"📚 处理 EPUB: {epub_path.name}")
# 提取文本
txt_path = extract_epub_to_txt(epub_path)
print(f"✅ 文本已提取: {txt_path}")
title = epub_path.stem
if deep_mode:
result = deep_analysis(txt_path, title, input_type)
if result:
# 保存结果到文件
output_file = f"/tmp/{title}_analysis.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"\n✅ 分析完成!结果已保存到: {output_file}")
else:
upload_to_notebooklm(txt_path, title)
elif input_type == 'document':
doc_path = Path(input_arg).expanduser()
print(f"📄 处理文档: {doc_path.name}")
title = doc_path.stem
if deep_mode:
result = deep_analysis(str(doc_path), title, input_type)
if result:
output_file = f"/tmp/{title}_analysis.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"\n✅ 分析完成!结果已保存到: {output_file}")
else:
upload_to_notebooklm(str(doc_path), title)
elif input_type == 'podcast':
print(f"🎙️ 处理播客/视频: {input_arg}")
print(" 通过 Get笔记 API 获取转写(可能需要 2-5 分钟)...")
script = os.path.join(os.path.dirname(__file__), 'scripts', 'get_podcast_transcript.py')
result = subprocess.run(
['python3', script, input_arg],
capture_output=True, text=True
)
if result.returncode != 0:
print(f"❌ 获取转写失败: {result.stderr}", file=sys.stderr)
sys.exit(1)
# Parse JSON output from script
try:
data = json.loads(result.stdout.strip())
except json.JSONDecodeError:
print(f"❌ 解析输出失败: {result.stdout}", file=sys.stderr)
sys.exit(1)
txt_path = data['txt_path']
title = data['title']
content_length = data['content_length']
print(f"✅ 转写完成: {title} ({content_length} 字符)")
print(f" TXT: {txt_path}")
if deep_mode:
result_data = deep_analysis(txt_path, title, 'podcast')
if result_data:
safe_title = re.sub(r'[::/\\?|<>*"\']', '_', title).strip('_')[:60]
output_file = f"/tmp/{safe_title}_analysis.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(result_data, f, ensure_ascii=False, indent=2)
print(f"\n✅ 分析完成!结果已保存到: {output_file}")
else:
upload_to_notebooklm(txt_path, title)
elif input_type == 'x_twitter':
print(f"🐦 处理 X/Twitter: {input_arg}")
print(" 通过代理级联获取推文内容...")
fetch_script = os.path.join(os.path.dirname(__file__), 'scripts', 'fetch_url.sh')
result = subprocess.run(
['bash', fetch_script, input_arg],
capture_output=True, text=True, timeout=60
)
if result.returncode != 0:
print(f"❌ 获取推文失败: {result.stderr}", file=sys.stderr)
sys.exit(1)
content = result.stdout.strip()
if not content:
print("❌ 获取到空内容", file=sys.stderr)
sys.exit(1)
# Extract title from content or URL
title = input_arg.split('/')[-1] or 'x_post'
# Try to extract first line as title
first_line = content.split('\n')[0].strip()
if first_line and len(first_line) < 100:
title = first_line.lstrip('#').strip()
safe_title = re.sub(r'[::/\\?|<>*"\']', '_', title).strip('_')[:60]
txt_path = tempfile.mktemp(suffix='.txt', prefix=f'x_{safe_title}_')
with open(txt_path, 'w', encoding='utf-8') as f:
f.write(f"# {title}\n\n")
f.write(f"来源: {input_arg}\n")
f.write(f"获取时间: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n---\n\n")
f.write(content)
print(f"✅ 推文内容已获取: {safe_title} ({len(content)} 字符)")
print(f" TXT: {txt_path}")
if deep_mode:
result_data = deep_analysis(txt_path, safe_title, 'x_twitter')
if result_data:
output_file = f"/tmp/{safe_title}_analysis.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(result_data, f, ensure_ascii=False, indent=2)
print(f"\n✅ 分析完成!结果已保存到: {output_file}")
else:
upload_to_notebooklm(txt_path, safe_title)
elif input_type == 'url':
print(f"🌐 处理 URL: {input_arg}")
# 添加 URL 作为 source
result = subprocess.run(
['notebooklm', 'source', 'add', input_arg],
capture_output=True, text=True
)
if result.returncode != 0:
print(f"❌ 添加失败: {result.stderr}", file=sys.stderr)
sys.exit(1)
print("✅ URL 已添加到 NotebookLM")
if deep_mode:
title = input_arg.split('/')[-1] or 'web_content'
print("⏳ 等待 NotebookLM 处理内容...")
time.sleep(3)
# 使用通用三轮递进提问
rounds = generate_questions_progressive(input_type)
total_questions = sum(len(qs) for _, qs in rounds)
print(f"\n📝 开始提问(共 {total_questions} 个问题,{len(rounds)} 轮)...")
all_questions = []
all_answers = []
for round_label, questions in rounds:
asked, answers = ask_round(round_label, questions, title)
all_questions.extend(asked)
all_answers.extend(answers)
result_data = {
"status": "success",
"title": title,
"url": input_arg,
"content_type": input_type,
"rounds": len(rounds),
"questions": all_questions,
"answers": all_answers,
"total_questions": len(all_questions),
"answered": len([a for a in all_answers if a]),
}
if to_feishu:
md = format_feishu_markdown(title, all_questions, all_answers)
create_feishu_doc(f"{title} - 深度解读", md)
output_file = f"/tmp/{title}_analysis.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(result_data, f, ensure_ascii=False, indent=2)
print(f"\n✅ 分析完成!结果已保存到: {output_file}")
else:
print(f"❌ 不支持的输入类型: {input_type}", file=sys.stderr)
print("提示: 请使用 EPUB、PDF、TXT、MD 文件或 URL", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
#!/bin/bash
# 打包 qiaomu-anything-to-notebooklm skill 用于分享
# 生成一个不包含大文件的精简版 tar.gz
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_NAME="qiaomu-anything-to-notebooklm"
OUTPUT_DIR="${1:-$HOME/Desktop}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_FILE="$OUTPUT_DIR/${SKILL_NAME}_${TIMESTAMP}.tar.gz"
# 颜色
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} 打包 ${SKILL_NAME} Skill${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
# 要打包的文件列表
FILES=(
"SKILL.md"
"README.md"
"install.sh"
"check_env.py"
"requirements.txt"
".gitignore"
)
# 创建临时目录
TEMP_DIR=$(mktemp -d)
TEMP_SKILL="$TEMP_DIR/$SKILL_NAME"
mkdir -p "$TEMP_SKILL"
echo "📦 正在打包文件..."
# 复制文件
for file in "${FILES[@]}"; do
if [ -f "$SKILL_DIR/$file" ]; then
cp "$SKILL_DIR/$file" "$TEMP_SKILL/"
echo " ✓ $file"
fi
done
# 创建 tar.gz
cd "$TEMP_DIR"
tar -czf "$OUTPUT_FILE" "$SKILL_NAME"
# 清理
rm -rf "$TEMP_DIR"
# 显示结果
FILE_SIZE=$(du -h "$OUTPUT_FILE" | cut -f1)
echo ""
echo -e "${GREEN}✅ 打包完成!${NC}"
echo ""
echo "📦 文件:$OUTPUT_FILE"
echo "📊 大小:$FILE_SIZE"
echo ""
echo "📤 分享说明:"
echo " 用户收到文件后,执行:"
echo " cd ~/.claude/skills/"
echo " tar -xzf ${SKILL_NAME}_${TIMESTAMP}.tar.gz"
echo " cd ${SKILL_NAME}"
echo " ./install.sh"
echo ""
echo "💡 注意:wexin-read-mcp 会在安装时自动克隆,无需打包"
echo ""
<div align="center">
🎯 Anything → NotebookLM
多源内容智能处理器:任何内容 → 播客 / PPT / 思维导图 / Quiz
      
快速开始 • 支持格式 • 使用示例 • 付费墙绕过 • 常见问题
</div>
---
✨ 这是什么?
一个 Claude Code Skill,用自然语言把任何内容变成任何格式。
你说:把这篇微信文章生成播客
AI :✅ 8 分钟播客已生成 → podcast.mp3
你说:这个付费文章做成思维导图
AI :✅ 自动绕过付费墙 → 思维导图已生成
你说:这期播客(小宇宙)做成 PPT
AI :✅ 自动转录音频 → 25 页 PPT 已生成核心能力:多源内容获取(含付费墙绕过)→ 上传 Google NotebookLM → AI 生成目标格式
---
🚀 支持的内容源(15+ 种)
<table> <tr> <td width="50%">
📱 社交与媒体
- 微信公众号(MCP 浏览器模拟)
- X/Twitter(推文 + 长线程)
- YouTube 视频(自动提取字幕)
- 播客(小宇宙 / 喜马拉雅 / B站)
🌐 网页(含付费墙绕过)
- 300+ 付费网站(NYT/WSJ/FT/Economist...)
- 任意公开网页(新闻、博客、文档)
- 搜索关键词(自动汇总结果)
</td> <td width="50%">
📚 电子书与文档
- PDF(支持扫描件 OCR)
- EPUB 电子书
- Markdown (.md)
- 纯文本 (.txt)
📄 Office 文档
- Word (.docx)
- PowerPoint (.pptx)
- Excel (.xlsx)
🖼️ 其他
- 图片(JPEG/PNG,自动 OCR)
- 音频(WAV/MP3,自动转录)
- ZIP 压缩包(批量处理)
</td> </tr> </table>
---
🛡️ 付费墙绕过
核心特性:自动检测并绕过 300+ 付费新闻网站的付费墙。
绕过策略(6 层级联)
Level 1: 代理服务(r.jina.ai / defuddle.md)
↓ 失败
Level 2: 站点专属 Bot UA(Googlebot ~50站 / Bingbot ~4站)
↓ 失败
Level 3: 通用绕过(UA伪装 + X-Forwarded-For + Referer伪装 + AMP + EU IP)
↓ 失败
Level 4: archive.today 存档(CAPTCHA 自动检测)
↓ 失败
Level 5: Google Cache
↓ 失败
Level 6: agent-fetch 本地工具支持的付费网站(部分)
| 类别 | 站点 |
|---|---|
| 🇺🇸 美国媒体 | NYT, WSJ, Bloomberg, Washington Post, The Information, Forbes, WIRED, The New Yorker, The Atlantic, USA Today, Boston Globe, LA Times, Chicago Tribune, Seattle Times, MIT Tech Review, Foreign Affairs |
| 🇬🇧 英国媒体 | FT, The Times, The Telegraph, The Economist |
| 🇩🇪 德国媒体 | Spiegel, Zeit, Sueddeutsche, FAZ, Handelsblatt |
| 🇫🇷 法国媒体 | Le Monde, Le Figaro, Le Parisien |
| 🇦🇺 澳洲媒体 | The Australian, SMH, The Age, Brisbane Times |
| 🇨🇳 中文媒体 | SCMP, Medium |
| 🌐 其他 | Haaretz, NZ Herald, Statista, Quora |
绕过技术(学自 Bypass Paywalls Clean)
| 技术 | 原理 | 覆盖率 |
|---|---|---|
| Googlebot UA + X-Forwarded-For | 搜索引擎爬虫白名单,直接获取全文 | ~50 站 |
| Bingbot UA | 同上,部分站点对 Bing 更友好 | ~4 站 |
| Cookie 清空 + Referer 伪装 | 清除计量 cookie,伪装来自 Google/Facebook/Twitter | 计量付费墙 |
| AMP 页面 | AMP 版付费墙实现较弱 | ~10 站 |
| JSON-LD 提取 | 从 HTML 内嵌的结构化数据提取 articleBody | 通用 |
| archive.today | 从网页存档获取已保存的内容 | 兜底方案 |
---
🎨 可以生成什么?
| 输出格式 | 用途 | 触发词示例 |
|---|---|---|
| 🎙️ 播客 | 通勤路上听 | "生成播客"、"做成音频" |
| 📊 PPT | 团队分享 | "做成PPT"、"生成幻灯片" |
| 🗺️ 思维导图 | 理清结构 | "画个思维导图"、"生成脑图" |
| 📝 Quiz | 自测掌握 | "生成Quiz"、"出题" |
| 🎬 视频 | 可视化 | "做个视频" |
| 📄 报告 | 深度分析 | "生成报告"、"写个总结" |
| 📈 信息图 | 数据可视化 | "做个信息图" |
| 📋 闪卡 | 记忆巩固 | "做成闪卡" |
---
⚡ 快速开始
前置需求
- ✅ Python 3.9+
- ✅ Git(macOS/Linux 自带)
就这两样! 其他依赖一键自动安装。
安装(3 步)
# 1. 克隆到 Claude skills 目录
cd ~/.claude/skills/
git clone https://github.com/joeseesun/qiaomu-anything-to-notebooklm
cd qiaomu-anything-to-notebooklm
# 2. 一键安装所有依赖
./install.sh
# 3. 按提示配置 MCP,然后重启 Claude Code首次使用
# NotebookLM 认证(只需一次)
notebooklm login
notebooklm list # 验证成功
# 环境检查(可选)
./check_env.py播客转写配置(可选)
如需使用小宇宙/喜马拉雅/B站转写功能,配置 Get笔记 API:
export GETNOTE_API_KEY="your_api_key"
export GETNOTE_CLIENT_ID="your_client_id"---
💡 使用示例
场景 1:付费文章 → 播客
你:把这篇 The Information 文章生成播客 https://www.theinformation.com/articles/...
AI 自动执行:
✓ 检测付费墙 → Googlebot UA 绕过
✓ 获取完整文章内容
✓ 上传到 NotebookLM
✓ 生成播客
✅ 结果:/tmp/article_podcast.mp3场景 2:播客(小宇宙)→ PPT
你:这期小宇宙播客做成 PPT https://xiaoyuzhoufm.com/episode/...
AI 自动执行:
✓ Get笔记 API 转写音频(2-5 分钟)
✓ 上传转写文本到 NotebookLM
✓ 生成 PPT
✅ 结果:/tmp/podcast_slides.pdf(25 页)场景 3:电子书 → 深度分析
你:深度分析这本书 /Users/joe/Books/sapiens.epub
AI 自动执行:
✓ 提取 EPUB 全文
✓ 上传到 NotebookLM
✓ 生成 12 个问题(3 轮递进:概览→深度挖掘→综合反刍)
✓ 逐轮提问,后轮受益于前轮对话上下文
✓ 输出结构化 JSON
✅ 结果:/tmp/sapiens_analysis.json(12 个问答,含核心观点、论证拆解、矛盾分析、认知改变)场景 4:X/Twitter 线程 → 思维导图
你:这个推文线程做成思维导图 https://x.com/user/status/123...
AI 自动执行:
✓ 代理级联获取推文内容(含完整线程)
✓ 上传到 NotebookLM
✓ 生成思维导图
✅ 结果:/tmp/tweet_mindmap.json场景 5:微信文章 → 飞书文档(深度分析)
你:深度分析这篇微信文章并写入飞书 https://mp.weixin.qq.com/s/abc123
AI 自动执行:
✓ MCP 浏览器模拟抓取微信文章
✓ 上传到 NotebookLM
✓ 生成 10 个问题并递归提问
✓ 格式化为飞书 Markdown
✓ 自动创建飞书文档
✅ 结果:飞书文档已创建(含完整问答)---
🎯 核心特性
🧠 智能识别
自动判断输入类型,无需手动指定
https://mp.weixin.qq.com/s/xxx → 微信公众号
https://xiaoyuzhoufm.com/episode/xxx → 小宇宙播客
https://x.com/user/status/xxx → X/Twitter
https://youtube.com/watch?v=xxx → YouTube 视频
/path/to/file.epub → EPUB 电子书
"搜索 'AI 趋势'" → 搜索查询🛡️ 付费墙自动绕过
无需手动处理,自动检测并绕过
检测付费墙 → 选择最佳策略 → 获取完整内容
︿________全自动________︿🚀 全自动处理
从获取到生成,一气呵成
输入 → 获取 → 转换 → 上传 → 生成 → 下载
︿___________全自动___________︿🌐 多源整合
支持混合多种内容源
付费文章 + YouTube 视频 + EPUB + 播客 → 综合报告---
📦 技术架构
┌──────────────────────────────────────────┐
│ 用户自然语言输入 │
│ "把这个付费文章生成播客 https://..." │
└──────────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ Claude Code Skill │
│ • 智能识别内容源类型 │
│ • 自动调用对应工具 │
└──────────┬───────────────────────────────┘
│
┌───────┴───────┐
│ │
▼ ▼
┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐
│ 微信 MCP │ │ 付费墙绕过 │ │ 播客转写 │ │ markitdown│
│ 浏览器模拟 │ │ 6层级联策略 │ │ Get笔记API│ │ 文件转换 │
└─────┬────┘ └──────┬───────┘ └─────┬────┘ └─────┬────┘
│ │ │ │
└──────────────┴─────────────────┴──────────────┘
│
▼
┌────────────────────────┐
│ NotebookLM API │
│ • 上传内容源 │
│ • AI 生成目标格式 │
└───────────┬────────────┘
│
▼
┌────────────────────────┐
│ 生成的文件 │
│ .mp3 / .pdf / .json │
└────────────────────────┘---
📂 项目结构
qiaomu-anything-to-notebooklm/
├── SKILL.md # Skill 定义文件
├── README.md # 本文件
├── main.py # 主入口:CLI 智能处理器
├── install.sh # 一键安装脚本
├── check_env.py # 13 项环境检查
├── package.sh # 打包分享脚本
├── requirements.txt # Python 依赖
├── LICENSE # MIT
├── scripts/
│ ├── fetch_url.sh # URL 抓取 + 付费墙绕过(6 层级联)
│ └── get_podcast_transcript.py # 播客/视频转写(Get笔记 API)
├── wexin-read-mcp/ # 微信公众号 MCP 服务器
│ └── src/
│ ├── server.py # MCP 入口
│ ├── scraper.py # Playwright 浏览器模拟
│ └── parser.py # HTML 解析
└── feishu-read-mcp/ # 飞书文档 MCP 服务器
└── src/
├── server.py # MCP 入口
├── scraper.py # 飞书文档抓取
├── parser.py # HTML → Markdown
└── image_handler.py # 图片处理---
🔧 高级用法
深度分析模式
python main.py https://example.com/article --deep-analysis
# 自动生成 12 个问题(3 轮递进:概览→深度挖掘→综合反刍),逐轮提问,输出结构化 JSON三轮递进策略:
| 轮次 | 问题数 | 目的 | 示例 |
|---|---|---|---|
| 第一轮·概览与框架 | 4 | 建立整体认知 | 概括主题、列出结构、提取核心论点、挖掘颠覆性内容 |
| 第二轮·深度挖掘 | 5 | 深入细节 | 拆解论证逻辑、分析矛盾、提炼核心洞察、提出尖锐批评 |
| 第三轮·综合与反刍 | 3 | 认知升级 | 最大认知改变、行动指南、推荐理由 |
NotebookLM 在同一会话中保持上下文,后轮问题自动受益于前轮回答,形成真正的"递进式"深度分析。
飞书文档输出
python main.py ./book.epub --deep-analysis --to-feishu
# 深度分析后自动创建飞书文档批量处理
把这些文章都生成播客:
1. https://mp.weixin.qq.com/s/abc123
2. https://www.wsj.com/articles/...
3. /Users/joe/notes.md---
🐛 故障排查
MCP 工具未找到
python ~/.claude/skills/qiaomu-anything-to-notebooklm/wexin-read-mcp/src/server.py
cd ~/.claude/skills/qiaomu-anything-to-notebooklm/wexin-read-mcp
pip install -r requirements.txt
playwright install chromiumNotebookLM 认证失败
notebooklm login # 重新登录
notebooklm list # 验证付费墙绕过失败
部分硬付费墙网站(如 The Information)服务器端不发送内容,需要 archive.today 存档。脚本会自动检测并提示:
⚠️ archive.ph needs human verification.
已自动打开浏览器,请完成验证后重试环境检查
./check_env.py # 13 项全面检查
./install.sh # 重新安装---
❓ 常见问题
<details> <summary><b>Q: 支持哪些语言?</b></summary>
A: NotebookLM 支持多语言,中文、英文效果最佳。 </details>
<details> <summary><b>Q: 播客是谁的声音?</b></summary>
A: Google AI 语音合成。英文是两个 AI 主持人对话,中文是单人叙述。 </details>
<details> <summary><b>Q: 付费墙绕过合法吗?</b></summary>
A: 本工具仅用于个人学习研究。技术原理基于搜索引擎白名单(Googlebot/Bingbot),不破解任何加密。建议支持优质新闻媒体,购买订阅。 </details>
<details> <summary><b>Q: 内容长度限制?</b></summary>
A:
- 最短:约 500 字
- 最长:约 50 万字
- 推荐:1000-10000 字效果最佳
</details>
<details> <summary><b>Q: 为什么需要 MCP?</b></summary>
A: 微信公众号有反爬虫,MCP 用 Playwright 浏览器模拟绕过。其他内容源(网页、YouTube、PDF)不需要 MCP。 </details>
<details> <summary><b>Q: 播客转写支持哪些平台?</b></summary>
A: 通过 Get笔记 API 支持小宇宙、喜马拉雅、B站视频。YouTube 由 NotebookLM 直接处理。 </details>
---
🙏 致谢
- Google NotebookLM - AI 内容生成
- Microsoft markitdown - 文件转换
- Bypass Paywalls Clean - 付费墙绕过策略参考
- wexin-read-mcp - 微信抓取
- notebooklm-py - NotebookLM CLI
📄 许可证
MIT License - 仅限个人学习研究使用
---
<div align="center">
如果觉得有用,请给个 ⭐ Star!
Made with ❤️ by Joe · Twitter @vista8 · 微信公众号「向阳乔木推荐看」
</div>
# Core MCP dependencies
fastmcp>=0.1.0
playwright>=1.40.0
beautifulsoup4>=4.12.0
lxml>=4.9.0
# File format conversion
markitdown[all]>=0.0.1
# NotebookLM (will be installed from PyPI if available, or from git)
# notebooklm-py