用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/Divem/yuanwen-skills --skill feishu-doc-download命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| name | feishu-doc-download |
| description | 下载飞书文档并转为格式化的本地 Markdown 文件。使用 blocks API 保留标题、列表、加粗、代码块、引用、表格等格式。当用户想下载/保存飞书文档时使用。 |
| metadata | {"author":"wen.yuan","emoji":"📥","requires":{"bins":["python3"]}} |
将飞书文档通过 Open API 下载,解析 blocks 结构,生成带格式的本地 Markdown 文件。
凭证位置:~/.claude/skills/feishu-doc-reader/reference/feishu_config.json
import os, json
with open(os.path.expanduser("~/.claude/skills/feishu-doc-reader/reference/feishu_config.json")) as f:
cfg = json.load(f)
app_id = cfg["app_id"]
app_secret = cfg["app_secret"]
docx:document:readonly - 读取文档内容wiki:node:read - 如文档在知识库中raw_content 接口:它只返回纯文本,无任何格式feishu auth device-flow 经常超时或报 application:application:self_manage 权限错误以下是一个完整的 Python 脚本,用 execute_code 工具执行:
import json, urllib.request, ssl, os, re
from hermes_tools import write_file
# ====== 配置 ======
DOC_TOKEN = "从URL中提取的文档token"
OUTPUT_DIR = "/用户指定的输出目录"
# ====== 读取凭证 ======
with open(os.path.expanduser("~/.claude/skills/feishu-doc-reader/reference/feishu_config.json")) as f:
cfg = json.load(f)
app_id, app_secret = cfg["app_id"], cfg["app_secret"]
# ====== Step 1: 获取 Tenant Access Token ======
ctx = ssl.create_default_context()
auth_data = json.dumps({"app_id": app_id, "app_secret": app_secret}).encode()
req = urllib.request.Request(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
data=auth_data, headers={"Content-Type": "application/json"}
)
resp = urllib.request.urlopen(req, timeout=15, context=ctx)
token = json.loads(resp.read())["tenant_access_token"]
headers = {"Authorization": f"Bearer {token}"}
# ====== Step 2: 获取文档标题 ======
req_t = urllib.request.Request(
f"https://open.feishu.cn/open-apis/docx/v1/documents/{DOC_TOKEN}", headers=headers
)
title = json.loads(urllib.request.urlopen(req_t, timeout=15, context=ctx).read())["data"]["document"]["title"]
# ====== Step 3: 获取所有 Blocks(分页) ======
all_blocks = []
page_token = None
while True:
path = f"/open-apis/docx/v1/documents/{DOC_TOKEN}/blocks?page_size=500"
if page_token:
path += f"&page_token="
req_b = urllib.request.Request( + path, headers=headers)
bd = json.loads(urllib.request.urlopen(req_b, timeout=, context=ctx).read())
bd.get() != :
all_blocks.extend(bd[][])
page_token = bd.get(, {}).get()
page_token:
():
elements:
parts = []
el elements:
tr = el.get()
tr:
content = tr.get(, )
style = tr.get(, {})
style.get():
content =
style.get():
content =
style.get():
content =
style.get():
content =
link = style.get()
link:
parts.append()
:
parts.append(content)
.join(parts)
():
key [,,,,,,
,,,,,,
,,]:
d = block.get(key)
d:
extract_text(d.get(, []))
children_map = {}
b all_blocks:
pid = b.get()
pid:
children_map.setdefault(pid, []).append(b)
():
bt = block.get()
bid = block[]
md =
bt (, ):
level = bt -
text = get_text(block).strip()
text:
md +=
bt == :
text = get_text(block).strip()
text:
md +=
bt == :
text = get_text(block).strip()
text:
md +=
bt == :
text = get_text(block).strip()
text:
md +=
bt == :
text = get_text(block).strip()
text:
md +=
bt == :
code_data = block.get(, {})
lang = code_data.get(, )
code_text =
el code_data.get(, []):
tr = el.get()
tr:
code_text += tr.get(, )
md +=
bt == :
md +=
bt == :
img_data = block.get(, {})
img_token = img_data.get(, )
md +=
bt == :
table_data = block.get(, {})
rows = table_data.get(, )
cols = table_data.get(, )
kids = children_map.get(bid, [])
cells = {}
kid kids:
kid.get() == :
cell_data = kid.get(, {})
c = cell_data.get(, )
r = cell_data.get(, )
cell_kids = children_map.get(kid[], [])
cell_text = .join(get_text(ck).strip() ck cell_kids)
cells[(r, c)] = cell_text
rows > cols > :
r (rows):
row_texts = [cells.get((r, c), ) c (cols)]
md += + .join(row_texts) +
r == :
md += + .join([] * cols) +
md +=
bt == :
todo_data = block.get(, {})
checked = todo_data.get() ==
text = extract_text(todo_data.get(, [])).strip()
md +=
bt [, , , ]:
text = get_text(block).strip()
text:
md +=
kid children_map.get(bid, []):
md += render(kid)
md
root_children = children_map.get(DOC_TOKEN, [])
full_md =
child root_children:
full_md += render(child)
full_md = re.sub(, , full_md).strip() +
filename = re.sub(, , title) +
write_file(os.path.join(OUTPUT_DIR, filename), full_md)
| block_type | 类型 | Markdown 输出 |
|---|---|---|
| 1 | page | 跳过(根节点) |
| 2 | text | 普通段落 |
| 3-11 | heading1-9 | # 到 ######### |
| 12 | bullet | - item |
| 13 | ordered | 1. item |
| 14 | code | ```lang ... ``` |
| 15 | quote | > text |
| 16 | todo | - [ ] text / - [x] text |
| 21 | divider | --- |
| 26 | image |  |
| 30 | table | Markdown 表格 |
| 31 | table_cell | 表格单元格(table 子节点,不单独渲染) |
| 23 | grid | 跳过 |
| 24 | grid_column | 跳过 |
| 问题 | 原因 | 解决 |
|---|---|---|
| Auth error (code != 0) | app_id/app_secret 错误 | 检查凭证文件 |
| 文档访问被拒 | 应用无权限或文档未分享给应用 | 开通 docx:document:readonly 权限,文档分享给应用 |
| 内容为空 | 文档确实为空或权限不足 | 检查 blocks 返回是否为空 |
| 格式丢失 | 用了 raw_content 接口 | 改用 blocks API |
| feishu CLI 报 self_manage 权限错误 | CLI 需要 OAuth | 直接用 API,不依赖 CLI |
如果 URL 是 https://xxx.feishu.cn/wiki/TOKEN,需要先获取实际的 docx token:
GET https://open.feishu.cn/open-apis/wiki/v2/spaces/get_node?token=WIKI_TOKEN
Authorization: Bearer {token}
返回的 data.node.obj_token 就是 docx token,然后用上面的流程处理。
如果也想让 CLI 工作,创建 ~/.feishu-cli/config.json:
{
"channels": {
"feishu": {
"appId": "YOUR_APP_ID",
"appSecret": "YOUR_APP_SECRET",
"domain": "feishu"
}
}
}
但 CLI 的 OAuth device-flow 不稳定,推荐直接用 API。