
Markdown Proxy
- 732 installs
- 495 repo stars
- Updated April 7, 2026
- joeseesun/markdown-proxy
markdown-proxy is an agent skill that fetches clean Markdown from any web page, including login-gated sites, for developers who need coding agents to read Twitter, WeChat, Lark docs, and other authenticated content as st
About
markdown-proxy is an MIT-licensed agent skill from joeseesun/markdown-proxy that enables coding agents to fetch clean Markdown representations of web pages, including those behind authentication such as Twitter, WeChat, and Lark documentation. Developers reach for markdown-proxy when agents must ingest gated documentation, social threads, or internal wiki pages without manual copy-paste. The skill acts as a proxy layer converting HTML-heavy or JS-rendered pages into agent-readable Markdown. Copyright 2026 joeseesun confirms active maintenance as a standalone repository focused on agent web-fetch workflows.
- Converts any URL into clean, LLM-ready Markdown automatically
- Built-in support for login-required pages including X/Twitter, WeChat public accounts, and Feishu/Lark documents
- Five dedicated content-type extraction handlers
- Seamless integration with Claude and other coding agents
- Runs as a local proxy so agents can request web content without leaving their context
Markdown Proxy by the numbers
- 732 all-time installs (skills.sh)
- Ranked #1,386 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/joeseesun/markdown-proxy --skill markdown-proxyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 732 |
|---|---|
| repo stars | ★ 495 |
| Security audit | 2 / 3 scanners passed |
| Last updated | April 7, 2026 |
| Repository | joeseesun/markdown-proxy ↗ |
How do agents fetch Markdown from login-gated pages?
Let their coding agent fetch clean Markdown from any web page, including those behind logins like Twitter, WeChat, or Lark docs.
Who is it for?
Developers building agents that must read authenticated docs, social posts, or internal wiki pages as clean Markdown instead of raw HTML.
Skip if: Scraping at scale without authorization, binary file downloads, or pages where Markdown conversion loses critical interactive UI state.
When should I use this skill?
Agent needs to read a web page behind login, convert a URL to Markdown, or fetch Twitter, WeChat, or Lark doc content.
What you get
Clean Markdown text extracted from authenticated or complex web pages ready for agent prompts and documentation pipelines.
- Clean Markdown page content
- Agent-ready text from authenticated URLs
Files
Markdown Proxy - URL to Markdown
将任意 URL 转为干净的 Markdown。支持需要登录的页面、PDF、专有平台。
URL Routing (先判断再执行)
收到 URL 后,先判断类型,不同类型走不同通道:
| URL Pattern | Route To | Reason |
|---|---|---|
mp.weixin.qq.com | scripts/fetch_weixin.py | 公众号需 Playwright 抓取 |
feishu.cn/docx/ feishu.cn/wiki/ larksuite.com/docx/ | scripts/fetch_feishu.py | 需飞书 API 认证 |
youtube.com youtu.be | yt-search-download skill | YouTube 有专用工具链 |
.pdf (URL or local path) | scripts/extract_pdf.sh | PDF 专用提取 |
| All other URLs | scripts/fetch.sh | 代理级联自动 fallback |
Workflow
Step 1: Route by URL Type
if URL contains "mp.weixin.qq.com":
→ python3 ~/.claude/skills/qiaomu-markdown-proxy/scripts/fetch_weixin.py "URL"
→ Done
if URL contains "feishu.cn/docx/" or "feishu.cn/wiki/" or "larksuite.com/docx/":
→ python3 ~/.claude/skills/qiaomu-markdown-proxy/scripts/fetch_feishu.py "URL"
→ Done
if URL contains "youtube.com" or "youtu.be":
→ Call yt-search-download skill
→ Done
if URL ends with ".pdf" or is local PDF path:
if remote URL:
→ Try: curl -sL "https://r.jina.ai/{url}"
→ If fails: download + extract_pdf.sh
if local path:
→ bash ~/.claude/skills/qiaomu-markdown-proxy/scripts/extract_pdf.sh "PATH"
→ Done
else:
→ bash ~/.claude/skills/qiaomu-markdown-proxy/scripts/fetch.sh "URL"
→ DoneStep 2: Display Content
After fetching, show to user:
Title: {title}
Author: {author} (if available)
Source: {platform} (公众号 / 飞书文档 / 网页 / PDF)
URL: {original_url}
Summary
{3-5 sentence summary}
Content
{full Markdown, truncated at 200 lines if long}Step 3: Save File (Default)
Save to ~/Downloads/{title}.md with YAML frontmatter by default.
- Filename: use article title, remove special characters
- Format: YAML frontmatter (title, author, date, url, source) + Markdown body
- Tell user the saved path
- Skip only if user says "just preview" or "don't save"
After saving and reporting the path, stop. Do not analyze, comment on, or discuss the content unless asked.
Examples
General URL
bash ~/.claude/skills/qiaomu-markdown-proxy/scripts/fetch.sh "https://example.com/article"X/Twitter Post
bash ~/.claude/skills/qiaomu-markdown-proxy/scripts/fetch.sh "https://x.com/username/status/1234567890"WeChat Article
python3 ~/.claude/skills/qiaomu-markdown-proxy/scripts/fetch_weixin.py "https://mp.weixin.qq.com/s/abc123"Feishu Document
python3 ~/.claude/skills/qiaomu-markdown-proxy/scripts/fetch_feishu.py "https://xxx.feishu.cn/docx/xxxxxxxx"PDF (Remote)
curl -sL "https://r.jina.ai/https://example.com/paper.pdf"PDF (Local)
bash ~/.claude/skills/qiaomu-markdown-proxy/scripts/extract_pdf.sh "/path/to/paper.pdf"With Custom Proxy
bash ~/.claude/skills/qiaomu-markdown-proxy/scripts/fetch.sh "https://example.com" "http://127.0.0.1:7890"Notes
- r.jina.ai and defuddle.md require no API key
fetch.shhandles proxy cascade with automatic fallback- Content validation: filters error pages, requires >5 lines
- WeChat script requires:
pip install playwright beautifulsoup4 lxml && playwright install chromium - Feishu script requires:
FEISHU_APP_ID+FEISHU_APP_SECRETenv vars - PDF extraction tries: marker-pdf → pdftotext → pypdf
- For detailed method documentation, see
references/methods.md
MIT License
Copyright (c) 2026 joeseesun
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.
qiaomu-markdown-proxy
Convert any URL to clean Markdown, with built-in support for login-required pages (X/Twitter, WeChat, Feishu/Lark docs, etc.)
将任意 URL 转为干净的 Markdown,支持需要登录的页面(X/Twitter、微信公众号、飞书文档等)。
[English](#english) | [中文](#中文)
---
<a name="english"></a>
English
Features
Send any URL to Claude, and it automatically fetches the full content as Markdown. Five content types have dedicated extraction:
| URL Type | Method | Why |
|---|---|---|
WeChat Articles (mp.weixin.qq.com) | Built-in Playwright script | Anti-scraping protection requires headless browser |
Feishu/Lark Docs (feishu.cn, larksuite.com) | Built-in Feishu API script | Requires API authentication, auto-converts to Markdown |
| YouTube | Dedicated YouTube skill | Video content has its own toolchain |
| PDF (remote or local) | Built-in PDF extraction (extract_pdf.sh) | Three-method cascade: marker-pdf → pdftotext → pypdf |
| All other URLs | Proxy cascade via fetch.sh: r.jina.ai → defuddle.md → agent-fetch | Free, no API key, content validation built-in |
Prerequisites
- [ ] Claude Code installed
- [ ] curl (built-in on macOS/Linux)
- [ ] (Optional - WeChat scraping) Python 3.8+ with playwright
pip install playwright beautifulsoup4 lxml
playwright install chromium- [ ] (Optional - PDF extraction) One of:
- marker-pdf (best quality):
pip install marker-pdf - pdftotext (fast):
brew install poppler - pypdf (fallback):
pip install pypdf - [ ] (Optional - Proxy fallback) agent-fetch
npx agent-fetch --help # No pre-install needed, npx auto-downloads- [ ] (Optional - Feishu docs) Environment variables
FEISHU_APP_IDandFEISHU_APP_SECRET
echo $FEISHU_APP_ID # Verify configuredInstallation
npx skills add joeseesun/qiaomu-markdown-proxyVerify:
ls ~/.claude/skills/qiaomu-markdown-proxy/SKILL.mdUsage
Just send Claude a URL:
- "Read this article: https://example.com/post"
- "Fetch this tweet: https://x.com/user/status/123456"
- "Read this WeChat article: https://mp.weixin.qq.com/s/abc123"
- "Convert this Feishu doc to Markdown: https://xxx.feishu.cn/docx/xxxxxxxx"
Proxy Priority
1. r.jina.ai — Most complete content, preserves image links 2. defuddle.md — Cleaner output with YAML frontmatter 3. [agent-fetch](https://github.com/teng-lin/agent-fetch) — Local tool, no network proxy needed 4. defuddle CLI — Local CLI, good for standard web pages
Feishu/Lark Document Support
Built-in fetch_feishu.py script fetches documents via Feishu Open API and auto-converts to Markdown:
- Supports new docs (docx), legacy docs (doc), and wiki pages
- Auto-parses document blocks into Markdown format
- Supports headings, lists, code blocks, quotes, todos, equations, images, etc.
- Requires
FEISHU_APP_IDandFEISHU_APP_SECRETenvironment variables - App needs
docx:document:readonlypermission
Troubleshooting
| Issue | Solution |
|---|---|
| WeChat scraping fails | Run playwright install chromium to install browser |
| Feishu returns permission error | Check FEISHU_APP_ID and FEISHU_APP_SECRET env vars, confirm app has document read permission |
| Feishu wiki page fails | Confirm app has wiki:wiki:readonly permission |
| r.jina.ai returns empty | Auto-falls back to defuddle.md (no action needed) |
| All proxies fail | URL may have strict auth restrictions, try npx agent-fetch |
Credits
- r.jina.ai — Free URL-to-Markdown proxy by Jina AI
- defuddle.md — Clean article extraction service
- agent-fetch — Local URL content extraction tool
- Playwright — Browser automation for WeChat scraping
- Feishu Open Platform — Feishu Document API
---
<a name="中文"></a>
中文
功能
给 Claude 发一个 URL,自动抓取完整内容并转为 Markdown。支持五种内容类型的专用抓取:
| URL 类型 | 抓取方式 | 原因 |
|---|---|---|
微信公众号 (mp.weixin.qq.com) | 内置 Playwright 脚本 | 公众号有反爬,需无头浏览器 |
飞书文档 (feishu.cn/docx/, /wiki/, /docs/) | 内置飞书 API 脚本 | 需要 API 认证,自动转 Markdown |
| YouTube | 专用 YouTube skill | 视频内容有专用工具链 |
| PDF(远程 URL 或本地文件) | 内置 PDF 提取(extract_pdf.sh) | 三级 fallback:marker-pdf → pdftotext → pypdf |
| 其他所有 URL | 代理级联 fetch.sh:r.jina.ai → defuddle.md → agent-fetch | 免费、无需 API key、内置内容验证 |
前置条件
- [ ] 已安装 Claude Code
- [ ] curl(macOS/Linux 自带)
- [ ] (可选 - 公众号抓取)Python 3.8+ 及 playwright
pip install playwright beautifulsoup4 lxml
playwright install chromium- [ ] (可选 - PDF 提取)以下任一:
- marker-pdf(最佳质量):
pip install marker-pdf - pdftotext(速度快):
brew install poppler - pypdf(兜底):
pip install pypdf - [ ] (可选 - 代理降级)agent-fetch
npx agent-fetch --help # 无需预装,npx 自动下载- [ ] (可选 - 飞书抓取)环境变量
FEISHU_APP_ID和FEISHU_APP_SECRET
echo $FEISHU_APP_ID # 验证已配置安装
npx skills add joeseesun/qiaomu-markdown-proxy验证:
ls ~/.claude/skills/qiaomu-markdown-proxy/SKILL.md使用示例
直接给 Claude 发 URL:
- "帮我读一下这篇文章:https://example.com/post"
- "抓取这条推文:https://x.com/user/status/123456"
- "读一下这篇公众号:https://mp.weixin.qq.com/s/abc123"
- "把这个飞书文档转成 Markdown:https://xxx.feishu.cn/docx/xxxxxxxx"
- "读一下这个飞书知识库页面:https://xxx.feishu.cn/wiki/xxxxxxxx"
- "提取这个 PDF:https://example.com/paper.pdf"
- "转换本地 PDF:/path/to/document.pdf"
代理优先级
1. r.jina.ai — 内容最完整,保留图片链接 2. defuddle.md — 输出更干净,带 YAML frontmatter 3. [agent-fetch](https://github.com/teng-lin/agent-fetch) — 本地工具,无需网络代理 4. defuddle CLI — 本地 CLI,适合普通网页
飞书文档支持
内置 fetch_feishu.py 脚本,通过飞书开放 API 抓取文档内容并自动转为 Markdown:
- 支持新版文档(docx)、旧版文档(doc)、知识库页面(wiki)
- 自动解析文档 blocks 并转换为 Markdown 格式
- 支持标题、列表、代码块、引用、待办、公式、图片等
- 需要飞书应用的
FEISHU_APP_ID和FEISHU_APP_SECRET环境变量 - 应用需要
docx:document:readonly权限
常见问题
| 问题 | 解决方法 |
|---|---|
| 公众号抓取失败 | 运行 playwright install chromium 安装浏览器 |
| 飞书文档返回权限错误 | 检查 FEISHU_APP_ID 和 FEISHU_APP_SECRET 环境变量,确认应用有文档读取权限 |
| 飞书知识库页面抓取失败 | 确认应用有 wiki:wiki:readonly 权限 |
| PDF 提取失败 | 安装任一工具:pip install marker-pdf、brew install poppler、pip install pypdf |
| r.jina.ai 返回空内容 | 自动降级到 defuddle.md(无需手动操作) |
| 所有代理都失败 | URL 可能有严格认证限制,尝试 npx agent-fetch |
致谢
- r.jina.ai — Jina AI 提供的免费 URL 转 Markdown 代理
- defuddle.md — 干净的文章提取服务
- agent-fetch — 本地 URL 内容提取工具
- Playwright — 微信公众号抓取的浏览器自动化
- 飞书开放平台 — 飞书文档 API
---
关注作者
- X (Twitter): @vista8
- 微信公众号「向阳乔木推荐看」
<p align="center"> <img src="https://github.com/joeseesun/terminal-boost/raw/main/assets/wechat-qr.jpg?raw=true" alt="向阳乔木推荐看公众号二维码" width="300"> </p>
Fetch Methods Reference
Proxy Cascade (General URLs)
Use scripts/fetch.sh for automatic proxy cascade with fallback. Try in order until success:
1. r.jina.ai
bash ~/.claude/skills/qiaomu-markdown-proxy/scripts/fetch.sh "https://example.com"Wide coverage, preserves image links. Try this first.
2. defuddle.md
Automatically tried by fetch.sh if r.jina.ai fails. Cleaner output with YAML frontmatter.
3. agent-fetch
Last resort local tool, automatically tried if both proxies fail.
With Custom Proxy
bash ~/.claude/skills/qiaomu-markdown-proxy/scripts/fetch.sh "https://example.com" "http://127.0.0.1:7890"PDF to Markdown
Remote PDF URL
r.jina.ai handles PDF URLs directly:
curl -sL "https://r.jina.ai/https://example.com/paper.pdf"If that fails, download and extract locally:
curl -sL "https://example.com/paper.pdf" -o /tmp/input.pdf
bash ~/.claude/skills/qiaomu-markdown-proxy/scripts/extract_pdf.sh /tmp/input.pdfLocal PDF File
bash ~/.claude/skills/qiaomu-markdown-proxy/scripts/extract_pdf.sh /path/to/file.pdfThe script tries three methods in order:
1. marker-pdf (best quality, requires: pip install marker-pdf)
- Best for papers, tables, complex layouts
- Preserves formatting and structure
2. pdftotext (fast, requires: brew install poppler)
- Good for text-heavy PDFs
- Fast extraction with layout preservation
3. pypdf (no-dependency fallback, requires: pip install pypdf)
- Works everywhere Python is available
- Basic text extraction
WeChat Public Account (公众号)
Use the proxy cascade first (r.jina.ai / defuddle.md). Works for most articles without extra tools.
If proxies are blocked, use the built-in Playwright script as last resort:
python3 ~/.claude/skills/qiaomu-markdown-proxy/scripts/fetch_weixin.py "https://mp.weixin.qq.com/s/abc123"Requirements (one-time setup, ~300 MB):
pip install playwright beautifulsoup4 lxml
playwright install chromiumOutput: YAML frontmatter (title, author, date, url) + Markdown body
JSON output:
python3 ~/.claude/skills/qiaomu-markdown-proxy/scripts/fetch_weixin.py "URL" --jsonFeishu / Lark Document
Built-in API script for Feishu documents. Requires app credentials:
export FEISHU_APP_ID=your_app_id
export FEISHU_APP_SECRET=your_app_secret
python3 ~/.claude/skills/qiaomu-markdown-proxy/scripts/fetch_feishu.py "https://xxx.feishu.cn/docx/xxxxxxxx"Supported types:
docx- New-style documentsdoc- Legacy documentswiki- Wiki pages (auto-resolves to actual document)
Required permissions: docx:document:readonly, wiki:wiki:readonly
Output: YAML frontmatter (title, document_id, url) + Markdown body
JSON output:
python3 ~/.claude/skills/qiaomu-markdown-proxy/scripts/fetch_feishu.py "URL" --jsonYouTube Videos
Use the dedicated yt-search-download skill for YouTube content. It handles:
- Video download
- Subtitle extraction
- Transcript generation
Do not use qiaomu-markdown-proxy for YouTube URLs.
Content Validation
The fetch.sh script validates content before returning:
- Must have more than 5 lines
- Filters out common error pages:
- "Don't miss what's happening" (Twitter login wall)
- "Access Denied"
- "404 Not Found"
If validation fails, automatically tries the next method.
#!/usr/bin/env bash
# Extract PDF to Markdown with multiple fallback methods.
# Usage: extract_pdf.sh <pdf_path>
set -euo pipefail
PDF_PATH="${1:?Usage: extract_pdf.sh <pdf_path>}"
if [ ! -f "$PDF_PATH" ]; then
echo "ERROR: File not found: $PDF_PATH" >&2
exit 1
fi
# Method 1: marker-pdf (best quality for papers, tables, complex layouts)
if command -v marker_single &>/dev/null; then
OUTPUT_DIR="${2:-$HOME/Downloads}"
marker_single "$PDF_PATH" --output_dir "$OUTPUT_DIR"
exit 0
fi
# Method 2: pdftotext (fast, good for text-heavy PDFs)
if command -v pdftotext &>/dev/null; then
pdftotext -layout "$PDF_PATH" - | sed 's/\f/\n---\n/g'
exit 0
fi
# Method 3: pypdf (no-dependency fallback)
python3 -c "
import sys
try:
import pypdf
except ImportError:
print('ERROR: pypdf not installed. Run: pip install pypdf', file=sys.stderr)
sys.exit(1)
reader = pypdf.PdfReader(sys.argv[1])
print('\n\n'.join(page.extract_text() for page in reader.pages))
" "$PDF_PATH"
#!/usr/bin/env python3
"""Fetch Feishu/Lark document as Markdown. Standalone script using Feishu Open API."""
import sys
import json
import os
import re
import requests
FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
def get_tenant_access_token():
"""获取 tenant_access_token"""
app_id = os.environ.get("FEISHU_APP_ID")
app_secret = os.environ.get("FEISHU_APP_SECRET")
if not app_id or not app_secret:
return None, "环境变量 FEISHU_APP_ID 或 FEISHU_APP_SECRET 未设置"
url = f"{FEISHU_API_BASE}/auth/v3/tenant_access_token/internal"
resp = requests.post(url, json={"app_id": app_id, "app_secret": app_secret})
data = resp.json()
if data.get("code") != 0:
return None, f"获取 token 失败: {data.get('msg', resp.text)}"
return data["tenant_access_token"], None
def parse_feishu_url(url):
"""从飞书 URL 解析 document_id 和文档类型"""
patterns = [
(r"feishu\.cn/docx/([A-Za-z0-9]+)", "docx"),
(r"feishu\.cn/docs/([A-Za-z0-9]+)", "doc"),
(r"feishu\.cn/wiki/([A-Za-z0-9]+)", "wiki"),
(r"larksuite\.com/docx/([A-Za-z0-9]+)", "docx"),
(r"larksuite\.com/docs/([A-Za-z0-9]+)", "doc"),
(r"larksuite\.com/wiki/([A-Za-z0-9]+)", "wiki"),
]
for pattern, doc_type in patterns:
m = re.search(pattern, url)
if m:
return m.group(1), doc_type
return None, None
def get_document_info(token, doc_id):
"""获取文档元信息(标题等)"""
url = f"{FEISHU_API_BASE}/docx/v1/documents/{doc_id}"
headers = {"Authorization": f"Bearer {token}"}
resp = requests.get(url, headers=headers)
data = resp.json()
if data.get("code") == 0:
return data.get("data", {}).get("document", {})
return {}
def get_document_blocks(token, doc_id):
"""获取文档所有 blocks"""
url = f"{FEISHU_API_BASE}/docx/v1/documents/{doc_id}/blocks"
headers = {"Authorization": f"Bearer {token}"}
all_blocks = []
page_token = None
while True:
params = {"page_size": 500}
if page_token:
params["page_token"] = page_token
resp = requests.get(url, headers=headers, params=params)
data = resp.json()
if data.get("code") != 0:
return None, f"获取 blocks 失败: {data.get('msg', resp.text)}"
items = data.get("data", {}).get("items", [])
all_blocks.extend(items)
if not data.get("data", {}).get("has_more", False):
break
page_token = data["data"].get("page_token")
return all_blocks, None
def get_wiki_node(token, wiki_token):
"""获取知识库节点信息,返回实际的 obj_token 和 obj_type"""
url = f"{FEISHU_API_BASE}/wiki/v2/spaces/get_node"
headers = {"Authorization": f"Bearer {token}"}
resp = requests.get(url, headers=headers, params={"token": wiki_token})
data = resp.json()
if data.get("code") == 0:
node = data.get("data", {}).get("node", {})
return node.get("obj_token"), node.get("obj_type")
return None, None
def extract_text_from_elements(elements):
"""从 text_run / mention_user 等元素中提取文本"""
if not elements:
return ""
parts = []
for el in elements:
if "text_run" in el:
tr = el["text_run"]
text = tr.get("content", "")
style = tr.get("text_element_style", {})
if style.get("bold"):
text = f"**{text}**"
if style.get("italic"):
text = f"*{text}*"
if style.get("strikethrough"):
text = f"~~{text}~~"
if style.get("inline_code"):
text = f"`{text}`"
if style.get("link", {}).get("url"):
import urllib.parse
link_url = urllib.parse.unquote(style["link"]["url"])
text = f"[{text}]({link_url})"
parts.append(text)
elif "mention_user" in el:
parts.append(f"@{el['mention_user'].get('user_id', 'user')}")
elif "equation" in el:
parts.append(f"${el['equation'].get('content', '')}$")
return "".join(parts)
def blocks_to_markdown(blocks):
"""将飞书 blocks 转为 Markdown"""
lines = []
ordered_list_counter = {} # parent_id -> counter
for block in blocks:
block_type = block.get("block_type")
parent_id = block.get("parent_id", "")
# 1 = Page, 2 = Text, 3 = Heading1, 4 = Heading2, ..., 9 = Heading7+
# 10 = BulletList, 11 = OrderedList, 12 = Code, 13 = Quote
# 14 = Equation, 15 = Todo, 16 = Divider
# 17 = Image, 18 = TableCell, 19 = Table
# 22 = Callout, 23 = ChatCard, 27 = Grid, 28 = GridColumn
if block_type == 2: # Text
text_data = block.get("text", {})
text = extract_text_from_elements(text_data.get("elements", []))
if text.strip():
lines.append(text)
else:
lines.append("")
elif block_type in (3, 4, 5, 6, 7, 8, 9): # Heading 1-7
level = block_type - 2
heading_data = block.get("heading" + str(level), {}) or block.get("heading", {})
# Try multiple key formats
for key in [f"heading{level}", "heading"]:
if key in block:
heading_data = block[key]
break
text = extract_text_from_elements(heading_data.get("elements", []))
lines.append(f"{'#' * level} {text}")
elif block_type == 10: # Bullet list
text_data = block.get("bullet", {})
text = extract_text_from_elements(text_data.get("elements", []))
lines.append(f"- {text}")
elif block_type == 11: # Ordered list
text_data = block.get("ordered", {})
text = extract_text_from_elements(text_data.get("elements", []))
counter = ordered_list_counter.get(parent_id, 0) + 1
ordered_list_counter[parent_id] = counter
lines.append(f"{counter}. {text}")
elif block_type == 12: # Code block
code_data = block.get("code", {})
text = extract_text_from_elements(code_data.get("elements", []))
lang = code_data.get("style", {}).get("language", "")
# Map language codes
lang_map = {1: "plaintext", 2: "abap", 3: "ada", 4: "apache", 5: "apex",
6: "assembly", 7: "bash", 8: "c", 9: "csharp", 10: "cpp",
11: "clojure", 12: "cmake", 13: "coffeescript", 14: "css",
15: "d", 16: "dart", 17: "delphi", 18: "django", 19: "dockerfile",
20: "elixir", 21: "elm", 22: "erlang", 23: "fortran",
24: "fsharp", 25: "go", 26: "graphql", 27: "groovy", 28: "haskell",
29: "html", 30: "http", 31: "java", 32: "javascript",
33: "json", 34: "julia", 35: "kotlin", 36: "latex", 37: "lisp",
38: "lua", 39: "makefile", 40: "markdown", 41: "matlab",
42: "nginx", 43: "objectivec", 44: "ocaml", 45: "perl",
46: "php", 47: "powershell", 48: "properties", 49: "protobuf",
50: "python", 51: "r", 52: "ruby", 53: "rust", 54: "scala",
55: "scheme", 56: "scss", 57: "shell", 58: "sql", 59: "swift",
60: "thrift", 61: "toml", 62: "typescript", 63: "vbnet",
64: "verilog", 65: "vhdl", 66: "visual_basic", 67: "vue",
68: "xml", 69: "yaml"}
lang_str = lang_map.get(lang, "") if isinstance(lang, int) else str(lang)
lines.append(f"```{lang_str}")
lines.append(text)
lines.append("```")
elif block_type == 13: # Quote
text_data = block.get("quote", {})
text = extract_text_from_elements(text_data.get("elements", []))
lines.append(f"> {text}")
elif block_type == 14: # Equation block
eq_data = block.get("equation", {})
text = extract_text_from_elements(eq_data.get("elements", []))
lines.append(f"$$\n{text}\n$$")
elif block_type == 15: # Todo
todo_data = block.get("todo", {})
text = extract_text_from_elements(todo_data.get("elements", []))
done = todo_data.get("style", {}).get("done", False)
checkbox = "[x]" if done else "[ ]"
lines.append(f"- {checkbox} {text}")
elif block_type == 16: # Divider
lines.append("---")
elif block_type == 17: # Image
image_data = block.get("image", {})
token_val = image_data.get("token", "")
lines.append(f"")
elif block_type == 22: # Callout
callout_data = block.get("callout", {})
# Callout is a container, children will be processed separately
emoji = callout_data.get("emoji_id", "")
if emoji:
lines.append(f"> {emoji}")
elif block_type == 1: # Page (root), skip
pass
else:
# Unknown block type, try to extract any text
for key in block:
if isinstance(block[key], dict) and "elements" in block[key]:
text = extract_text_from_elements(block[key]["elements"])
if text.strip():
lines.append(text)
break
return "\n\n".join(lines)
def fetch_feishu_doc(url_or_id):
"""主函数:获取飞书文档并转为 Markdown"""
# 解析 URL
doc_id, doc_type = parse_feishu_url(url_or_id)
if not doc_id:
# 可能直接传了 doc_token
doc_id = url_or_id
doc_type = "docx"
# 获取 token
token, err = get_tenant_access_token()
if err:
return {"error": err}
# Wiki 需要先获取实际文档 ID
if doc_type == "wiki":
real_id, real_type = get_wiki_node(token, doc_id)
if real_id:
doc_id = real_id
doc_type = real_type or "docx"
else:
return {"error": f"无法获取知识库节点信息: {doc_id}"}
# 获取文档信息
doc_info = get_document_info(token, doc_id)
title = doc_info.get("title", "")
# 获取 blocks
blocks, err = get_document_blocks(token, doc_id)
if err:
return {"error": err}
# 转换为 Markdown
content = blocks_to_markdown(blocks)
return {
"title": title,
"document_id": doc_id,
"url": url_or_id,
"content": content,
}
def format_as_markdown(result):
"""格式化为 Markdown 文档"""
if "error" in result:
return f"Error: {result['error']}"
parts = ["---"]
if result.get("title"):
parts.append(f'title: "{result["title"]}"')
parts.append(f'document_id: "{result["document_id"]}"')
if result.get("url"):
parts.append(f'url: "{result["url"]}"')
parts.append("---")
parts.append("")
if result.get("title"):
parts.append(f"# {result['title']}")
parts.append("")
parts.append(result.get("content", ""))
return "\n".join(parts)
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: fetch_feishu.py <feishu_url_or_doc_token> [--json]", file=sys.stderr)
print(" 需要环境变量: FEISHU_APP_ID, FEISHU_APP_SECRET", file=sys.stderr)
sys.exit(1)
url = sys.argv[1]
use_json = "--json" in sys.argv
result = fetch_feishu_doc(url)
if use_json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(format_as_markdown(result))
#!/usr/bin/env python3
"""Fetch WeChat (公众号) article as Markdown. Standalone script using Playwright + BeautifulSoup."""
import sys
import json
import asyncio
import re
async def fetch_weixin_article(url: str) -> dict:
"""Fetch and parse a WeChat article, return dict with title, author, publish_time, content."""
try:
from playwright.async_api import async_playwright
except ImportError:
return {"error": "playwright not installed. Run: pip install playwright && playwright install chromium"}
try:
from bs4 import BeautifulSoup
except ImportError:
return {"error": "beautifulsoup4 not installed. Run: pip install beautifulsoup4 lxml"}
html = None
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page(
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"
)
try:
await page.goto(url, wait_until="domcontentloaded", timeout=30000)
await page.wait_for_selector("#js_content", timeout=15000)
html = await page.content()
except Exception as e:
await browser.close()
return {"error": f"Failed to load page: {e}"}
await browser.close()
if not html:
return {"error": "No HTML content retrieved"}
soup = BeautifulSoup(html, "lxml")
# Extract title
title_el = soup.select_one("#activity-name")
title = title_el.get_text(strip=True) if title_el else ""
# Extract author
author_el = soup.select_one("#js_author_name") or soup.select_one(".rich_media_meta_text")
author = author_el.get_text(strip=True) if author_el else ""
# Extract publish time
time_el = soup.select_one("#publish_time")
publish_time = time_el.get_text(strip=True) if time_el else ""
# Extract content
content_el = soup.select_one("#js_content")
if not content_el:
return {"error": "Could not find article content (#js_content)"}
# Convert to markdown-like text
# Remove scripts and styles
for tag in content_el.find_all(["script", "style"]):
tag.decompose()
# Process images - extract src or data-src
for img in content_el.find_all("img"):
src = img.get("data-src") or img.get("src") or ""
if src:
img.replace_with(f"\n\n")
else:
img.decompose()
# Get text with basic formatting
lines = []
for element in content_el.find_all(["p", "h1", "h2", "h3", "h4", "section", "blockquote"]):
text = element.get_text(strip=True)
if not text:
continue
tag = element.name
if tag in ("h1", "h2", "h3", "h4"):
prefix = "#" * int(tag[1])
lines.append(f"{prefix} {text}")
elif tag == "blockquote":
lines.append(f"> {text}")
else:
lines.append(text)
content = "\n\n".join(lines)
# If structured extraction got nothing, fall back to plain text
if not content.strip():
content = content_el.get_text("\n", strip=True)
return {
"title": title,
"author": author,
"publish_time": publish_time,
"content": content,
"url": url,
}
def format_as_markdown(result: dict) -> str:
"""Format result dict as a Markdown document."""
if "error" in result:
return f"Error: {result['error']}"
parts = ["---"]
if result.get("title"):
parts.append(f"title: \"{result['title']}\"")
if result.get("author"):
parts.append(f"author: \"{result['author']}\"")
if result.get("publish_time"):
parts.append(f"date: \"{result['publish_time']}\"")
parts.append(f"url: \"{result['url']}\"")
parts.append("---")
parts.append("")
if result.get("title"):
parts.append(f"# {result['title']}")
parts.append("")
parts.append(result.get("content", ""))
return "\n".join(parts)
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: fetch_weixin.py <weixin_url> [--json]", file=sys.stderr)
sys.exit(1)
url = sys.argv[1]
use_json = "--json" in sys.argv
result = asyncio.run(fetch_weixin_article(url))
if use_json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(format_as_markdown(result))
#!/usr/bin/env bash
# Fetch a URL as Markdown via proxy cascade with auto-fallback.
# Usage: fetch.sh <url> [proxy_url]
# Example: fetch.sh https://example.com http://127.0.0.1:7890
set -euo pipefail
URL="${1:?Usage: fetch.sh <url> [proxy_url]}"
PROXY="${2:-}"
_curl() {
if [ -n "$PROXY" ]; then
https_proxy="$PROXY" http_proxy="$PROXY" curl -sL "$@"
else
curl -sL "$@"
fi
}
_has_content() {
local content="$1"
local line_count=$(echo "$content" | wc -l | tr -d ' ')
# Must have more than 5 lines
[ "$line_count" -gt 5 ] || return 1
# Filter out common error pages
echo "$content" | grep -qv "Don't miss what's happening" || return 1
echo "$content" | grep -qv "Access Denied" || return 1
echo "$content" | grep -qv "404 Not Found" || return 1
return 0
}
# 1. r.jina.ai - wide coverage, preserves image links
OUT=$(_curl "https://r.jina.ai/$URL" 2>/dev/null || true)
if _has_content "$OUT"; then
echo "$OUT"
exit 0
fi
# 2. defuddle.md - cleaner output with YAML frontmatter
OUT=$(_curl "https://defuddle.md/$URL" 2>/dev/null || true)
if _has_content "$OUT"; then
echo "$OUT"
exit 0
fi
# 3. agent-fetch - last resort local tool
if command -v npx &>/dev/null; then
OUT=$(npx --yes agent-fetch "$URL" --json 2>/dev/null || true)
if [ -n "$OUT" ]; then
echo "$OUT"
exit 0
fi
fi
echo "ERROR: All fetch methods failed for: $URL" >&2
exit 1
Related skills
FAQ
Which gated sites does markdown-proxy support?
markdown-proxy fetches clean Markdown from login-gated pages including Twitter, WeChat, and Lark documentation, converting complex HTML into structured text agents can consume in prompts.
What license is markdown-proxy released under?
markdown-proxy is released under the MIT License by joeseesun in 2026 as a standalone repository dedicated to agent-friendly Markdown extraction from web pages.
Is Markdown Proxy safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.