yuquer — 语雀文档查询、同步与下载
通过语雀 OpenAPI v2 查询、同步和下载语雀知识库文档。
触发条件
- 用户提到"语雀"、"yuque"、"查文档"、"搜索文档"
- 用户提到"同步文档"、"新建文档"、"更新文档"、"发到语雀"
- 用户提到"下载文档"、"导出文档"、"保存文档"
- 用户提到"查看目录结构"、"目录管理"、"知识库"
前置条件
- Python 3.8+ 已安装(用于写操作和复杂读操作)
- curl 可用(用于简单读操作)
- 已通过共享凭证管理配置语雀 Token(
python -m scripts.credentials setup yuquer)
配置
语雀凭证统一由共享凭证库管理(~/.claude/credentials.env,字段 YUQUE_*):
| 字段 | 说明 |
|---|---|
YUQUE_TOKEN |
团队 API Token(语雀团队管理后台 → Token 设置) |
YUQUE_BASE_URL |
团队 Base URL,如 https://your-team.yuque.com |
YUQUE_DOWNLOAD_DIR |
文档下载默认路径(可选,不配置则每次需用户指定) |
配置命令:
python -m scripts.credentials setup yuquer
获取方式:
- YUQUE_TOKEN:语雀团队管理后台 → Token 设置
- YUQUE_BASE_URL:团队 URL 中的子域名部分,如
https://your-team.yuque.com - YUQUE_DOWNLOAD_DIR:文档下载的默认保存路径(可选,不配置则每次需用户指定)
⚠️ 凭证存于共享凭证库(已 .gitignore),不要提交到版本控制。
安全规则
- ✅ 允许:查询、搜索、创建文档、更新文档、下载文档、目录读取和添加
- 🚫 禁止:删除文档、删除知识库、删除目录节点
- 🛡️ 更新前必读:更新文档前必须先读取当前内容,防止意外覆盖
- 🔒 写操作确认:创建/更新前展示目标信息,等用户确认后执行
当用户要求删除操作时,回复:
⚠️ 删除操作不在本 skill 支持范围内,请前往语雀平台手动操作。
API 调用模板
配置读取
所有 API 调用前先加载共享凭证库中的语雀凭证到 shell 变量:
eval "$(python -m scripts.credentials env yuquer)"
# 加载后即可用 $YUQUE_TOKEN $YUQUE_BASE_URL $YUQUE_DOWNLOAD_DIR
如果加载失败(提示「未配置任何凭证」),提示用户:
⚠️ 请先配置语雀凭证。运行:
python -m scripts.credentials setup yuquer
读操作通用模式
使用 curl 调用,认证头 X-Auth-Token(凭证已通过 env 加载到 shell):
curl -s -H "X-Auth-Token: $YUQUE_TOKEN" "$YUQUE_BASE_URL/api/v2/..."
写操作通用模式
使用 Python urllib(避免 Windows curl 编码问题)。凭证从环境变量读取(已由 eval $(python -m scripts.credentials env yuquer) 加载):
import json, os, urllib.request
base_url = os.environ['YUQUE_BASE_URL']
token = os.environ['YUQUE_TOKEN']
payload = json.dumps({
# 按具体接口填写
}).encode('utf-8')
req = urllib.request.Request(
f'{base_url}/api/v2/...',
data=payload,
headers={
'X-Auth-Token': token,
'Content-Type': 'application/json'
},
method='POST' # or 'PUT'
)
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read().decode('utf-8'))
print(json.dumps(result, ensure_ascii=False, indent=2))
错误处理
| HTTP 状态码 | 处理方式 |
|---|---|
| 400 | 检查路径参数、查询参数、枚举值 |
| 401 | Token 无效/过期 → 提示检查 YUQUE_TOKEN(python -m scripts.credentials setup yuquer) |
| 403 | 无权限 → 提示资源不在 Token 权限范围 |
| 404 | 资源不存在 → 检查 repo_id/doc_id/slug |
| 422 | 参数校验失败 → 检查必填字段(title、body 等) |
| 429 | 速率限制 → 等待后重试(每小时 5000 次,每秒 100 次) |
| 500 | 语雀服务异常 → 稍后重试 |
执行流程
收到请求后,根据触发词判断场景类型,执行对应流程。
流程 1:验证连通性
触发词:"测试语雀连接"、"检查 token"、"语雀连接状态"
- 用共享凭证 CLI 测试(推荐):
python -m scripts.credentials test yuquer
- 或手动加载凭证并调用用户信息接口验证:
eval "$(python -m scripts.credentials env yuquer)"
curl -s -H "X-Auth-Token: $YUQUE_TOKEN" "$YUQUE_BASE_URL/api/v2/user" | python -c "
import sys, json
data = json.load(sys.stdin)
if 'data' in data:
u = data['data']
print(f'✅ 连接成功')
print(f' 用户: {u.get(\"name\", \"N/A\")}')
print(f' 登录名: {u.get(\"login\", \"N/A\")}')
print(f' ID: {u.get(\"id\", \"N/A\")}')
else:
print(f'❌ 连接失败: {data}')
"
- 输出连接状态和用户信息
流程 2:查询/搜索文档
触发词:"搜索 xxx"、"查找文档"、"列出知识库"、"看看有什么知识库"、"列出文档"
根据用户意图判断子场景:
场景 2a:全局搜索
用户给出关键词搜索文档或知识库。
curl -s -H "X-Auth-Token: $YUQUE_TOKEN" "$YUQUE_BASE_URL/api/v2/search?q=<关键词>&type=doc&limit=20" | python -c "
import sys, json
data = json.load(sys.stdin)
if 'data' in data:
for item in data['data']:
print(f'- [{item.get(\"type\",\"?\")}] {item.get(\"title\",\"N/A\")}')
print(f' slug: {item.get(\"slug\",\"N/A\")} | id: {item.get(\"id\",\"N/A\")}')
else:
print('未找到结果')
"
场景 2b:列出知识库
用户说"列出知识库"、"看看有什么知识库"。
需要先获取 group_id(团队 ID):
# 获取当前用户所属团队
curl -s -H "X-Auth-Token: $YUQUE_TOKEN" "$YUQUE_BASE_URL/api/v2/user" | python -c "
import sys, json
data = json.load(sys.stdin)
groups = data.get('data', {}).get('groups', [])
if groups:
for g in groups:
print(f'- {g.get(\"name\",\"N/A\")} (id: {g.get(\"id\",\"N/A\")}, login: {g.get(\"login\",\"N/A\")})')
else:
print('未找到团队信息')
"
然后列出团队下的知识库:
curl -s -H "X-Auth-Token: $YUQUE_TOKEN" "$YUQUE_BASE_URL/api/v2/groups/<group_id>/repos" | python -c "
import sys, json
data = json.load(sys.stdin)
if 'data' in data:
for repo in data['data']:
print(f'- {repo.get(\"name\",\"N/A\")} (slug: {repo.get(\"slug\",\"N/A\")}, id: {repo.get(\"id\",\"N/A\")})')
if repo.get('description'):
print(f' {repo[\"description\"][:80]}')
else:
print('未找到知识库')
"
场景 2c:列出知识库下的文档
用户说"列出 xxx 知识库的文档"、"看看知识库有什么文档"。
curl -s -H "X-Auth-Token: $YUQUE_TOKEN" "$YUQUE_BASE_URL/api/v2/repos/<repo_id>/docs?limit=50" | python -c "
import sys, json
data = json.load(sys.stdin)
if 'data' in data:
for doc in data['data']:
print(f'- {doc.get(\"title\",\"N/A\")} (slug: {doc.get(\"slug\",\"N/A\")}, id: {doc.get(\"id\",\"N/A\")})')
else:
print('未找到文档')
"
资源定位:支持以下方式指定 repo_id:
- 直接给 repo_id 数字
- 给
group_login/book_slug路径 → 先调GET /api/v2/repos/{group_login}/{book_slug}获取 repo_id
场景 2d:读取文档详情
用户说"读取 xxx 文档"、"看看文档内容"、"查看文档详情"。
curl -s -H "X-Auth-Token: $YUQUE_TOKEN" "$YUQUE_BASE_URL/api/v2/repos/<repo_id>/docs/<doc_id>" | python -c "
import sys, json
data = json.load(sys.stdin)
if 'data' in data:
doc = data['data']
print(f'# {doc.get(\"title\",\"N/A\")}')
print(f'slug: {doc.get(\"slug\",\"N/A\")} | id: {doc.get(\"id\",\"N/A\")}')
print(f'格式: {doc.get(\"format\",\"N/A\")} | 字数: {doc.get(\"word_count\",0)}')
print(f'更新时间: {doc.get(\"updated_at\",\"N/A\")}')
print()
print(doc.get('body', '(无内容)'))
else:
print('文档不存在或无权限访问')
"
资源定位:支持以下方式:
repo_id/doc_id→GET /api/v2/repos/{repo_id}/docs/{doc_id}group_login/book_slug/doc_slug→GET /api/v2/repos/{group_login}/{book_slug}/docs/{doc_slug}
流程 3:创建文档
触发词:"新建文档"、"创建文档"、"把内容发到语雀"、"在 xxx 知识库创建文章"
确定目标知识库(通过搜索或直接指定 repo_id)
确认文档内容(title + body),默认 format=markdown
展示创建计划,等用户确认:
📝 即将创建文档:
- 知识库:{repo_name} (id: {repo_id})
- 标题:{title}
- 格式:markdown
- 字数:约 {body_length} 字
确认创建吗?
用户确认后执行创建:
import json, os, urllib.request
base_url = os.environ['YUQUE_BASE_URL']
token = os.environ['YUQUE_TOKEN']
payload = json.dumps({
'title': '<文档标题>',
'slug': '<可选自定义slug>',
'body': '<Markdown内容>',
'format': 'markdown',
'public': 0
}).encode('utf-8')
req = urllib.request.Request(
f'{base_url}/api/v2/repos/<repo_id>/docs',
data=payload,
headers={
'X-Auth-Token': token,
'Content-Type': 'application/json'
},
method='POST'
)
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read().decode('utf-8'))
doc = result.get('data', {})
print(f'✅ 文档创建成功')
print(f' 标题: {doc.get("title","N/A")}')
print(f' slug: {doc.get("slug","N/A")}')
print(f' id: {doc.get("id","N/A")}')
print(f' URL: {base_url}/{doc.get("slug","N/A")}')
- 如果用户要求放到特定目录位置 → 执行流程 6 更新 TOC
流程 4:更新文档
触发词:"更新文档"、"修改文档"、"追加内容到语雀"、"同步内容到语雀"
- 先读取当前文档内容(防止覆盖):
curl -s -H "X-Auth-Token: $YUQUE_TOKEN" "$YUQUE_BASE_URL/api/v2/repos/<repo_id>/docs/<doc_id>"
根据用户意图处理内容:
- 追加:在现有 body 末尾追加新内容
- 替换:使用用户提供的新内容完全替换
- 修改标题/slug:只更新指定字段,保留 body 不变
展示更新计划,等用户确认:
📝 即将更新文档:
- 文档:{title} (id: {doc_id})
- 操作:{追加/替换/修改}
- 变更摘要:{简要描述变更内容}
确认更新吗?
用户确认后执行更新:
import json, os, urllib.request
base_url = os.environ['YUQUE_BASE_URL']
token = os.environ['YUQUE_TOKEN']
payload = json.dumps({
'title': '<更新后标题>', # 可选,不修改则不传
'slug': '<更新后slug>', # 可选
'body': '<更新后完整body>' # 必须传完整内容,不能只传变更部分
}).encode('utf-8')
req = urllib.request.Request(
f'{base_url}/api/v2/repos/<repo_id>/docs/<doc_id>',
data=payload,
headers={
'X-Auth-Token': token,
'Content-Type': 'application/json'
},
method='PUT'
)
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read().decode('utf-8'))
doc = result.get('data', {})
print(f'✅ 文档更新成功')
print(f' 标题: {doc.get("title","N/A")}')
print(f' 更新时间: {doc.get("updated_at","N/A")}')
流程 5:下载文档
触发词:"下载文档"、"导出文档"、"保存文档到本地"、"批量下载"、"下载整个知识库"
路径确定
按优先级确定保存路径:
- 用户明确指定的路径 → 使用指定路径
$YUQUE_DOWNLOAD_DIR(共享凭证库配置)→ 使用配置路径- 都没有 → 提示用户:
📂 请指定下载路径,或运行
python -m scripts.credentials setup yuquer配置YUQUE_DOWNLOAD_DIR。
单文档下载
- 读取文档详情获取 body
- 构建文件内容(含 frontmatter 元数据)
- 保存到本地
import json, os, urllib.request
base_url = os.environ['YUQUE_BASE_URL']
token = os.environ['YUQUE_TOKEN']
# 1. 读取文档详情
req = urllib.request.Request(
f'{base_url}/api/v2/repos/<repo_id>/docs/<doc_id>',
headers={'X-Auth-Token': token}
)
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read().decode('utf-8'))
doc = result['data']
# 2. 确定保存路径
target_dir = '<用户指定路径 或 $YUQUE_DOWNLOAD_DIR>'
os.makedirs(target_dir, exist_ok=True)
# 3. 构建 Markdown(含 frontmatter)
frontmatter = f"""---
title: "{doc.get('title', '')}"
slug: "{doc.get('slug', '')}"
doc_id: "{doc.get('id', '')}"
repo_id: "{doc.get('repo_id', '')}"
updated_at: "{doc.get('updated_at', '')}"
format: "{doc.get('format', 'markdown')}"
---
"""
content = frontmatter + doc.get('body', '')
# 4. 写入文件
filename = doc.get('slug', str(doc.get('id', 'doc'))) + '.md'
filepath = os.path.join(target_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
file_size = os.path.getsize(filepath)
print(f'✅ 文档已下载')
print(f' 标题: {doc.get("title", "")}')
print(f' 保存到: {filepath}')
print(f' 文件大小: {file_size} 字节')
批量下载知识库
用户说"下载整个知识库"、"导出知识库所有文档"。
import json, os, urllib.request, time
BASE = os.environ['YUQUE_BASE_URL']
TOKEN = os.environ['YUQUE_TOKEN']
target_dir = '<用户指定路径 或 $YUQUE_DOWNLOAD_DIR>/<book_slug>'
os.makedirs(target_dir, exist_ok=True)
headers = {'X-Auth-Token': TOKEN}
# 1. 获取文档列表
req = urllib.request.Request(f'{BASE}/api/v2/repos/<repo_id>/docs?limit=100', headers=headers)
with urllib.request.urlopen(req) as resp:
docs = json.loads(resp.read().decode('utf-8'))['data']
print(f'📚 共 {len(docs)} 篇文档,开始下载...')
# 2. 逐个下载
for i, doc_summary in enumerate(docs):
doc_id = doc_summary['id']
try:
req = urllib.request.Request(
f'{BASE}/api/v2/repos/<repo_id>/docs/{doc_id}',
headers=headers
)
with urllib.request.urlopen(req) as resp:
doc = json.loads(resp.read().decode('utf-8'))['data']
frontmatter = f"""---
title: "{doc.get('title', '')}"
slug: "{doc.get('slug', '')}"
doc_id: "{doc.get('id', '')}"
updated_at: "{doc.get('updated_at', '')}"
---
"""
content = frontmatter + doc.get('body', '')
filename = doc.get('slug', str(doc_id)) + '.md'
filepath = os.path.join(target_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f' [{i+1}/{len(docs)}] {doc.get("title", "")}')
time.sleep(0.1) # 避免触发速率限制
except Exception as e:
print(f' [{i+1}/{len(docs)}] ❌ 失败: {doc_summary.get("title","")} - {e}')
print(f'\n✅ 下载完成,保存到: {target_dir}')
流程 6:目录管理
触发词:"查看目录结构"、"目录结构"、"把文档放到某目录下"、"添加到目录"
读取目录
curl -s -H "X-Auth-Token: $YUQUE_TOKEN" "$YUQUE_BASE_URL/api/v2/repos/<repo_id>/toc" | python -c "
import sys, json
data = json.load(sys.stdin)
toc = data.get('data', [])
def print_tree(nodes, indent=0):
for node in nodes:
prefix = ' ' * indent
icon = '📁' if node.get('type') == 'section' else '📄'
title = node.get('title', 'N/A')
nid = node.get('id', '')
print(f'{prefix}{icon} {title} (uuid: {nid})')
children = node.get('children', [])
if children:
print_tree(children, indent + 1)
print_tree(toc)
"
添加文档到目录
⚠️ 创建文档后不会自动出现在目录中,需要手动添加到 TOC。
- 先读取当前 TOC 获取完整的节点列表和 UUID
- 在目标位置添加新节点(appendNode)
- 仅允许添加操作,不允许删除节点
import json, os, urllib.request
base_url = os.environ['YUQUE_BASE_URL']
token = os.environ['YUQUE_TOKEN']
# 构建 TOC 更新请求
# 注意:toc 参数是完整的目录 JSON 数组,需要包含现有所有节点 + 新增节点
# 新增节点格式:
# {"type": "DOC", "id": "<doc_id>", "title": "<文档标题>", "uuid": "<自动生成>"}
# 建议:先读取当前 TOC,在合适位置插入新节点,再整体提交
payload = json.dumps({
'toc': '<完整的目录 JSON 数组>'
}).encode('utf-8')
req = urllib.request.Request(
f'{base_url}/api/v2/repos/<repo_id>/toc',
data=payload,
headers={
'X-Auth-Token': token,
'Content-Type': 'application/json'
},
method='PUT'
)
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read().decode('utf-8'))
print(f'✅ 目录更新成功')
⚠️ 安全限制:
- 更新 TOC 时,必须保留所有现有节点,只做添加/移动操作
- 不要删除任何现有节点
- 操作前先读取当前 TOC 并展示给用户确认
资源定位
语雀 API 支持多种方式定位资源,按可读性优先选择:
知识库定位
| 方式 | API 路径 | 适用场景 |
|---|---|---|
| slug 路径 | /api/v2/repos/{group_login}/{book_slug} |
用户给了语雀 URL 路径 |
| ID | /api/v2/repos/{repo_id} |
已知知识库 ID |
文档定位
| 方式 | API 路径 | 适用场景 |
|---|---|---|
| slug 路径 | /api/v2/repos/{group_login}/{book_slug}/docs/{doc_slug} |
用户给了完整语雀 URL |
| repo_id + doc_slug | /api/v2/repos/{repo_id}/docs/{doc_slug} |
已知知识库 ID + 文档 slug |
| repo_id + doc_id | /api/v2/repos/{repo_id}/docs/{doc_id} |
已知两个 ID,最快捷 |
从语雀 URL 提取参数
当用户给了一个语雀文档 URL,如 https://team.yuque.com/team-space/handbook/api-guide:
group_login=team-spacebook_slug=handbookdoc_slug=api-guide
文档版本
查看文档历史版本:
curl -s -H "X-Auth-Token: $YUQUE_TOKEN" "$YUQUE_BASE_URL/api/v2/repos/<repo_id>/docs/<doc_id>/versions" | python -c "
import sys, json
data = json.load(sys.stdin)
if 'data' in data:
for v in data['data']:
print(f'- v{v.get(\"version\",0)} | {v.get(\"title\",\"N/A\")} | {v.get(\"created_at\",\"N/A\")}')
else:
print('无版本记录')
"
注意事项
- 语雀 API 速率限制:每小时 5000 次,每秒 100 次,团队下所有 Token 共享
- 响应头
X-RateLimit-Remaining可查看剩余次数 - 创建文档后不会自动添加到目录,需额外调用 TOC 更新接口
- 文档 body 字段返回 Markdown 格式(对 markdown 类型的文档)
lake类型文档(表格、画板等)body 可能为空,无法通过 API 获取完整内容- 默认使用 markdown 格式创建/更新文档,除非用户明确要求其他格式
- 所有写操作(创建/更新)前必须展示操作计划并等待用户确认
- 下载路径优先使用用户指定路径,其次使用
$YUQUE_DOWNLOAD_DIR(共享凭证库配置)