End-to-end workflow for uploading local markdown to Feishu Wiki with mermaid diagram support.
Overview
| Step | Action | Auth | Notes |
|---|---|---|---|
| 1 | Import Markdown → Cloud Doc | TAT or UAT | Creates doc owned by app |
| 2 | Transfer ownership to user | TAT | Required for wiki move |
| 3 | Move Cloud Doc → Wiki Node | UAT | Sleep 3s after step 2! |
| 4 | Fix Mermaid blocks | UAT | Replace type 2 → type 40 |
| 5 | Insert Images | UAT | Upload local images |
| 6 | Insert File Attachments | UAT | Upload xlsx, pdf, etc. |
Prerequisites
- Feishu App with wiki, docx, drive scopes
- UAT (User Access Token) — refreshed via OAuth flow
- TAT (Tenant Access Token) — obtained via app credentials
Bash
# Config
APP_ID="cli_xxxxxxxxxxxxx"
APP_SECRET="your_app_secret"
WIKI_SPACE_ID="your_wiki_space_id"
PARENT_NODE="your_parent_node_token"
USER_OPEN_ID="your_user_open_id"
# Get TAT
TAT=$(curl -s -X POST 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal' \
-H 'Content-Type: application/json' \
-d "{\"app_id\":\"$APP_ID\",\"app_secret\":\"$APP_SECRET\"}" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['tenant_access_token'])")
# Get UAT from cached token
UAT=$(python3 -c "import json; print(json.load(open('$HOME/.feishu_token_cache.json'))['access_token'])")
Step 1: Import Markdown → Cloud Doc
Bash
curl -X POST "https://open.feishu.cn/open-apis/docx/v1/documents/import" \
-H "Authorization: Bearer $TAT" \
-H "Content-Type: application/json" \
-d '{"markdown": "# Your Document\n\nContent here...", "file_name": "My Document"}'
- Mermaid code blocks are flattened to plain text paragraphs
- Max file size: 20MB
- Chinese text may need Unicode escaping (\uXXXX) in some contexts
Step 2: Transfer Ownership to User
Bash
curl -X POST "https://open.feishu.cn/open-apis/drive/v1/permissions/${DOC_TOKEN}/members/transfer_owner?type=docx" \
-H "Authorization: Bearer $TAT" \
-H "Content-Type: application/json" \
-d "{\"member_type\":\"openid\",\"member_id\":\"$USER_OPEN_ID\"}"
- Uses TAT (app transfers to user)
- If user already owns it, returns error code 1063002 — safe to ignore
- ⚠️ CRITICAL: Wait 3 seconds after this before the next step!
Bash
sleep 3 # DO NOT SKIP — wiki move fails without this delay
Step 3: Move Cloud Doc → Wiki Node
Bash
curl -X POST "https://open.feishu.cn/open-apis/wiki/v2/spaces/${WIKI_SPACE_ID}/nodes/move_docs_to_wiki" \
-H "Authorization: Bearer $UAT" \
-H "Content-Type: application/json" \
-d "{\"parent_wiki_token\":\"$PARENT_NODE\",\"obj_type\":\"docx\",\"obj_token\":\"$DOC_TOKEN\"}"
- Uses UAT — user must own the doc
- Returns wiki_token (different from doc_token)
- If you lose the wiki_token, retrieve it later:
Bash
curl -X GET "https://open.feishu.cn/open-apis/wiki/v2/spaces/get_node?token=${DOC_TOKEN}&obj_type=docx" \
-H "Authorization: Bearer $UAT"
Step 4: Fix Mermaid Blocks
4a. Find flattened mermaid blocks
Bash
curl -s -X GET "https://open.feishu.cn/open-apis/docx/v1/documents/${DOC_TOKEN}/blocks?page_size=200" \
-H "Authorization: Bearer $UAT" | python3 -c "
import json, sys
data = json.load(sys.stdin)
for item in data['data']['items']:
if item.get('block_type') != 2: continue
for el in item.get('text', {}).get('elements', []):
content = el.get('text_run', {}).get('content', '')
if any(kw in content for kw in ['graph TD', 'graph LR', 'flowchart', 'sequenceDiagram']):
print(f'FOUND: {item[\"block_id\"]} | {content[:100]}')
"
4b. Get block index
Bash
curl -s -X GET "https://open.feishu.cn/open-apis/docx/v1/documents/${DOC_TOKEN}/blocks/${DOC_TOKEN}" \
-H "Authorization: Bearer $UAT" | python3 -c "
import json, sys
data = json.load(sys.stdin)
children = data['data']['block']['children']
target = '${BLOCK_ID}'
for i, c in enumerate(children):
if c == target:
print(f'Index: {i}')
"
4c. Create mermaid diagram block
Json
{
"block_type": 40,
"add_ons": {
"component_type_id": "blk_631fefbbae02400430b8f9f4",
"record": "{\"data\":\"graph TD\\n A --> B\",\"theme\":\"default\",\"view\":\"chart\"}"
}
}
- component_type_id is fixed: blk_631fefbbae02400430b8f9f4
- record is a JSON string (double-encoded in request body)
4d. Delete old flattened text block
Bash
curl -s -X DELETE "https://open.feishu.cn/open-apis/docx/v1/documents/${DOC_TOKEN}/blocks/${DOC_TOKEN}/children/batch_delete" \
-H "Authorization: Bearer $UAT" \
-H "Content-Type: application/json" \
-d "{\"start_index\": ${BLOCK_INDEX}, \"end_index\": $((BLOCK_INDEX + 1))}"
Step 5: Insert Images
Bash
curl -s -X POST "https://open.feishu.cn/open-apis/docx/v1/documents/${DOC_TOKEN}/blocks/${PARENT_BLOCK_ID}/children" \
-H "Authorization: Bearer $UAT" \
-H "Content-Type: application/json" \
-d '{"children": [{"block_type": 27, "image": {"token": ""}}], "index": 1}'
Bash
curl -s -X POST "https://open.feishu.cn/open-apis/drive/v1/medias/upload_all" \
-H "Authorization: Bearer $UAT" \
-F "file_name=screenshot.png" \
-F "parent_type=docx_image" \
-F "parent_node=${IMAGE_BLOCK_ID}" \
-F "size=${FILE_SIZE}" \
-F "file=@screenshot.png"
Bash
curl -s -X PATCH "https://open.feishu.cn/open-apis/docx/v1/documents/${DOC_TOKEN}/blocks/${IMAGE_BLOCK_ID}?document_revision_id=-1" \
-H "Authorization: Bearer $UAT" \
-H "Content-Type: application/json" \
-d "{\"replace_image\": {\"token\": \"${FILE_TOKEN}\"}}"
Step 6: Insert File Attachments
Bash
curl -s -X POST "https://open.feishu.cn/open-apis/docx/v1/documents/${DOC_TOKEN}/blocks/${PARENT_BLOCK_ID}/children?document_revision_id=-1" \
-H "Authorization: Bearer $UAT" \
-H "Content-Type: application/json" \
-d '{"children": [{"block_type": 23, "file": {"token": ""}}], "index": 1}'
- response.children[0] → View block (type 33)
- response.children[0].children[0] → File block (type 23) ← USE THIS block_id
Bash
curl -s -X POST "https://open.feishu.cn/open-apis/drive/v1/medias/upload_all" \
-H "Authorization: Bearer $UAT" \
-F "file_name=template.xlsx" \
-F "parent_type=docx_file" \
-F "parent_node=${FILE_BLOCK_ID}" \
-F "size=${FILE_SIZE}" \
-F "file=@template.xlsx"
Bash
curl -s -X PATCH "https://open.feishu.cn/open-apis/docx/v1/documents/${DOC_TOKEN}/blocks/${FILE_BLOCK_ID}?document_revision_id=-1" \
-H "Authorization: Bearer $UAT" \
-H "Content-Type: application/json" \
-d "{\"replace_file\": {\"token\": \"${FILE_TOKEN}\"}}"
| Image | File | |
|---|---|---|
| block_type | 27 | 23 |
| parent_type | docx_image | docx_file |
| PATCH field | replace_image | replace_file |
| Response | Direct block | View(33) → File(23) wrapper |
| Rendering | Inline image | Downloadable file card |
Gotchas & Lessons Learned
| Issue | Solution |
|---|---|
| Wiki move fails after ownership transfer | Sleep 3 seconds between transfer and move |
| Mermaid flattened to plain text | Post-process with block API (type 40) |
| Code block (type 14) doesn't render mermaid | Must use type 40 add_ons |
| PATCH cannot change block_type | Create new block + delete old block |
| Block indices shift after delete | Process bottom to top |
| record field encoding | Double-encode: json.dumps(json.dumps(data)) |
| Image block needs empty token | Create with {"token": ""}, real token set via PATCH after upload |
| File block wrapped in View block | Extract inner children[0].children[0] for the real file block_id |
| replace_file vs replace_image | File attachments use replace_file, images use replace_image |
| docx_file vs docx_image | File upload uses parent_type docx_file, images use docx_image |
本地 Markdown 上传到飞书 Wiki 的完整工作流,含 Mermaid 图表支持。
概述
| 步骤 | 操作 | 认证 | 备注 |
|---|---|---|---|
| 1 | 导入 Markdown → 云文档 | TAT 或 UAT | 创建的文档归应用所有 |
| 2 | 转移文档所有权给用户 | TAT | Wiki 移动需要用户权限 |
| 3 | 移动云文档 → Wiki 节点 | UAT | 步骤 2 后等 3 秒! |
| 4 | 修复 Mermaid 块 | UAT | 把 type 2 → type 40 |
| 5 | 插入图片 | UAT | 上传本地图片 |
| 6 | 插入文件附件 | UAT | 上传 xlsx, pdf 等 |
前置条件
- 飞书应用,具有 wiki、docx、drive 权限
- UAT(用户访问令牌)— 通过 OAuth 流程刷新
- TAT(租户访问令牌)— 通过应用凭证获取
Bash
# 配置
APP_ID="cli_xxxxxxxxxxxxx"
APP_SECRET="your_app_secret"
WIKI_SPACE_ID="your_wiki_space_id"
PARENT_NODE="your_parent_node_token"
USER_OPEN_ID="your_user_open_id"
# 获取 TAT
TAT=$(curl -s -X POST 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal' \
-H 'Content-Type: application/json' \
-d "{\"app_id\":\"$APP_ID\",\"app_secret\":\"$APP_SECRET\"}" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['tenant_access_token'])")
# 从缓存获取 UAT
UAT=$(python3 -c "import json; print(json.load(open('$HOME/.feishu_token_cache.json'))['access_token'])")
步骤 1:导入 Markdown → 云文档
Bash
curl -X POST "https://open.feishu.cn/open-apis/docx/v1/documents/import" \
-H "Authorization: Bearer $TAT" \
-H "Content-Type: application/json" \
-d '{"markdown": "# 你的文档\n\n内容...", "file_name": "我的文档"}'
- Mermaid 代码块会被扁平化为纯文本段落
- 最大文件:20MB
- 中文可能需要 Unicode 转义(\uXXXX)
步骤 2:转移所有权给用户
Bash
curl -X POST "https://open.feishu.cn/open-apis/drive/v1/permissions/${DOC_TOKEN}/members/transfer_owner?type=docx" \
-H "Authorization: Bearer $TAT" \
-H "Content-Type: application/json" \
-d "{\"member_type\":\"openid\",\"member_id\":\"$USER_OPEN_ID\"}"
Bash
sleep 3 # 不要跳过
步骤 3:移动云文档 → Wiki 节点
Bash
curl -X POST "https://open.feishu.cn/open-apis/wiki/v2/spaces/${WIKI_SPACE_ID}/nodes/move_docs_to_wiki" \
-H "Authorization: Bearer $UAT" \
-H "Content-Type: application/json" \
-d "{\"parent_wiki_token\":\"$PARENT_NODE\",\"obj_type\":\"docx\",\"obj_token\":\"$DOC_TOKEN\"}"
步骤 4:修复 Mermaid 块
Json
{
"block_type": 40,
"add_ons": {
"component_type_id": "blk_631fefbbae02400430b8f9f4",
"record": "{\"data\":\"graph TD\\n A --> B\",\"theme\":\"default\",\"view\":\"chart\"}"
}
}
步骤 5:插入图片
Bash
curl -s -X POST "https://open.feishu.cn/open-apis/docx/v1/documents/${DOC_TOKEN}/blocks/${PARENT_BLOCK_ID}/children" \
-H "Authorization: Bearer $UAT" \
-H "Content-Type: application/json" \
-d '{"children": [{"block_type": 27, "image": {"token": ""}}], "index": 1}'
Bash
curl -s -X POST "https://open.feishu.cn/open-apis/drive/v1/medias/upload_all" \
-H "Authorization: Bearer $UAT" \
-F "file_name=screenshot.png" \
-F "parent_type=docx_image" \
-F "parent_node=${IMAGE_BLOCK_ID}" \
-F "size=${FILE_SIZE}" \
-F "file=@screenshot.png"
Bash
curl -s -X PATCH "https://open.feishu.cn/open-apis/docx/v1/documents/${DOC_TOKEN}/blocks/${IMAGE_BLOCK_ID}?document_revision_id=-1" \
-H "Authorization: Bearer $UAT" \
-H "Content-Type: application/json" \
-d "{\"replace_image\": {\"token\": \"${FILE_TOKEN}\"}}"
步骤 6:插入文件附件
Bash
curl -s -X POST "https://open.feishu.cn/open-apis/docx/v1/documents/${DOC_TOKEN}/blocks/${PARENT_BLOCK_ID}/children?document_revision_id=-1" \
-H "Authorization: Bearer $UAT" \
-H "Content-Type: application/json" \
-d '{"children": [{"block_type": 23, "file": {"token": ""}}], "index": 1}'
- response.children[0] → View 块 (type 33)
- response.children[0].children[0] → 文件块 (type 23) ← 使用这个 block_id
Bash
curl -s -X POST "https://open.feishu.cn/open-apis/drive/v1/medias/upload_all" \
-H "Authorization: Bearer $UAT" \
-F "file_name=template.xlsx" \
-F "parent_type=docx_file" \
-F "parent_node=${FILE_BLOCK_ID}" \
-F "size=${FILE_SIZE}" \
-F "file=@template.xlsx"
Bash
curl -s -X PATCH "https://open.feishu.cn/open-apis/docx/v1/documents/${DOC_TOKEN}/blocks/${FILE_BLOCK_ID}?document_revision_id=-1" \
-H "Authorization: Bearer $UAT" \
-H "Content-Type: application/json" \
-d "{\"replace_file\": {\"token\": \"${FILE_TOKEN}\"}}"
| 图片 | 文件 | |
|---|---|---|
| block_type | 27 | 23 |
| parent_type | docx_image | docx_file |
| PATCH 字段 | replace_image | replace_file |
| 响应结构 | 直接返回块 | View(33) → File(23) 包装 |
| 渲染效果 | 内联图片 | 可下载文件卡片 |
踩坑与经验
| 问题 | 解决方案 |
|---|---|
| 转移所有权后 Wiki 移动失败 | 转移和移动之间等 3 秒 |
| Mermaid 被扁平化为纯文本 | 用块 API 后处理(type 40) |
| 代码块(type 14)不渲染 Mermaid | 必须用 type 40 插件块 |
| PATCH 无法改变 block_type | 新建块 + 删旧块 |
| 删除后块索引偏移 | 从下往上处理 |
| record 字段编码 | 双重编码:json.dumps(json.dumps(data)) |
| 图片块需要空 token | 创建时用 {"token": ""},上传后通过 PATCH 设置真实 token |
| 文件块被 View 块包装 | 提取内部 children[0].children[0] 获取真实文件 block_id |
| replace_file vs replace_image | 文件附件用 replace_file,图片用 replace_image |
| docx_file vs docx_image | 文件上传用 parent_type docx_file,图片用 docx_image |