# Yuquer

> 语雀文档查询、同步与下载工具 — 搜索/列出/读取语雀知识库和文档，创建/更新文档，下载文档到本地。触发词：语雀、yuque、查文档、搜索文档、同步文档、下载文档、导出文档、知识库。

- Skill: `jeandoom/yuquer` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jeandoom/yuquer`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jeandoom/yuquer/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Jeandoom (https://skillmd.com/u/jeandoom)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/jeandoom/yuquer

---


# 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` | 文档下载默认路径（可选，不配置则每次需用户指定） |

配置命令：

```bash
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 变量：

```bash
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）：

```bash
curl -s -H "X-Auth-Token: $YUQUE_TOKEN" "$YUQUE_BASE_URL/api/v2/..."
```

### 写操作通用模式

使用 Python urllib（避免 Windows curl 编码问题）。凭证从环境变量读取（已由 `eval $(python -m scripts.credentials env yuquer)` 加载）：

```python
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"、"语雀连接状态"

1. 用共享凭证 CLI 测试（推荐）：

```bash
python -m scripts.credentials test yuquer
```

2. 或手动加载凭证并调用用户信息接口验证：

```bash
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}')
"
```

3. 输出连接状态和用户信息

---

### 流程 2：查询/搜索文档

**触发词**："搜索 xxx"、"查找文档"、"列出知识库"、"看看有什么知识库"、"列出文档"

根据用户意图判断子场景：

#### 场景 2a：全局搜索

用户给出关键词搜索文档或知识库。

```bash
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）：

```bash
# 获取当前用户所属团队
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('未找到团队信息')
"
```

然后列出团队下的知识库：

```bash
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 知识库的文档"、"看看知识库有什么文档"。

```bash
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 文档"、"看看文档内容"、"查看文档详情"。

```bash
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 知识库创建文章"

1. 确定目标知识库（通过搜索或直接指定 repo_id）
2. 确认文档内容（title + body），默认 format=markdown
3. **展示创建计划**，等用户确认：
   > 📝 即将创建文档：
   > - 知识库：{repo_name} (id: {repo_id})
   > - 标题：{title}
   > - 格式：markdown
   > - 字数：约 {body_length} 字
   >
   > 确认创建吗？

4. 用户确认后执行创建：

```python
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")}')
```

5. 如果用户要求放到特定目录位置 → 执行流程 6 更新 TOC

---

### 流程 4：更新文档

**触发词**："更新文档"、"修改文档"、"追加内容到语雀"、"同步内容到语雀"

1. **先读取**当前文档内容（防止覆盖）：

```bash
curl -s -H "X-Auth-Token: $YUQUE_TOKEN" "$YUQUE_BASE_URL/api/v2/repos/<repo_id>/docs/<doc_id>"
```

2. 根据用户意图处理内容：
   - **追加**：在现有 body 末尾追加新内容
   - **替换**：使用用户提供的新内容完全替换
   - **修改标题/slug**：只更新指定字段，保留 body 不变

3. **展示更新计划**，等用户确认：
   > 📝 即将更新文档：
   > - 文档：{title} (id: {doc_id})
   > - 操作：{追加/替换/修改}
   > - 变更摘要：{简要描述变更内容}
   >
   > 确认更新吗？

4. 用户确认后执行更新：

```python
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：下载文档

**触发词**："下载文档"、"导出文档"、"保存文档到本地"、"批量下载"、"下载整个知识库"

#### 路径确定

按优先级确定保存路径：
1. 用户明确指定的路径 → 使用指定路径
2. `$YUQUE_DOWNLOAD_DIR`（共享凭证库配置）→ 使用配置路径
3. 都没有 → 提示用户：
   > 📂 请指定下载路径，或运行 `python -m scripts.credentials setup yuquer` 配置 `YUQUE_DOWNLOAD_DIR`。

#### 单文档下载

1. 读取文档详情获取 body
2. 构建文件内容（含 frontmatter 元数据）
3. 保存到本地

```python
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} 字节')
```

#### 批量下载知识库

用户说"下载整个知识库"、"导出知识库所有文档"。

```python
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：目录管理

**触发词**："查看目录结构"、"目录结构"、"把文档放到某目录下"、"添加到目录"

#### 读取目录

```bash
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。

1. 先读取当前 TOC 获取完整的节点列表和 UUID
2. 在目标位置添加新节点（appendNode）
3. 仅允许**添加**操作，不允许删除节点

```python
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-space`
- `book_slug` = `handbook`
- `doc_slug` = `api-guide`

## 文档版本

查看文档历史版本：

```bash
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`（共享凭证库配置）

