Asia/Shanghai
February 10, 2026

Feishu: Markdown → Wiki Pipeline

飞书:Markdown → Wiki 同步管线

Mingjian Shao
Feishu: Markdown → Wiki Pipeline
End-to-end workflow for uploading local markdown to Feishu Wiki with mermaid diagram support.
Feishu has no direct "upload to wiki" API. You must go through cloud docs first, following a 6-step pipeline:
StepActionAuthNotes
1Import Markdown → Cloud DocTAT or UATCreates doc owned by app
2Transfer ownership to userTATRequired for wiki move
3Move Cloud Doc → Wiki NodeUATSleep 3s after step 2!
4Fix Mermaid blocksUATReplace type 2 → type 40
5Insert ImagesUATUpload local images
6Insert File AttachmentsUATUpload xlsx, pdf, etc.
  • 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'])")
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"}'
Known limitations:
  • Mermaid code blocks are flattened to plain text paragraphs
  • Max file size: 20MB
  • Chinese text may need Unicode escaping (\uXXXX) in some contexts
Output: Returns a doc_token for use in subsequent steps.
Import creates a doc owned by the app. Wiki move requires user ownership.
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\"}"
Notes:
  • 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
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\"}"
Notes:
  • 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"
Mermaid code blocks are imported as plain text paragraphs (block_type=2). You need to replace them with native mermaid diagram blocks (block_type=40).⚠️ Process from bottom to top to avoid index drift after deletion.
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]}')
"
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}')
"
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)
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))}"
Upload local images into document blocks. Three-phase process:5a. Create empty image block (block_type 27)
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}'
Returns block_id of the created image block (initially a 100×100 placeholder).5b. Upload image file
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"
Returns file_token.5c. Patch the image block
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}\"}}"
Key field: replace_image (NOT update_image). After PATCH, the block updates with correct dimensions.
Upload files (xlsx, pdf, etc.) as downloadable attachment cards. Same three-phase pattern as images but different block type and parent_type.6a. Create empty file block (block_type 23)
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 wraps file block in a view block:
  • response.children[0] → View block (type 33)
  • response.children[0].children[0] → File block (type 23) ← USE THIS block_id
6b. Upload file
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"
6c. Patch the file block
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 vs File comparison table:
ImageFile
block_type2723
parent_typedocx_imagedocx_file
PATCH fieldreplace_imagereplace_file
ResponseDirect blockView(33) → File(23) wrapper
RenderingInline imageDownloadable file card
IssueSolution
Wiki move fails after ownership transferSleep 3 seconds between transfer and move
Mermaid flattened to plain textPost-process with block API (type 40)
Code block (type 14) doesn't render mermaidMust use type 40 add_ons
PATCH cannot change block_typeCreate new block + delete old block
Block indices shift after deleteProcess bottom to top
record field encodingDouble-encode: json.dumps(json.dumps(data))
Image block needs empty tokenCreate with {"token": ""}, real token set via PATCH after upload
File block wrapped in View blockExtract inner children[0].children[0] for the real file block_id
replace_file vs replace_imageFile attachments use replace_file, images use replace_image
docx_file vs docx_imageFile upload uses parent_type docx_file, images use docx_image
本地 Markdown 上传到飞书 Wiki 的完整工作流,含 Mermaid 图表支持。
飞书没有直接的 "上传到 Wiki" API。你必须先经过云文档,走一个 6 步流水线:
步骤操作认证备注
1导入 Markdown → 云文档TAT 或 UAT创建的文档归应用所有
2转移文档所有权给用户TATWiki 移动需要用户权限
3移动云文档 → Wiki 节点UAT步骤 2 后等 3 秒!
4修复 Mermaid 块UAT把 type 2 → type 40
5插入图片UAT上传本地图片
6插入文件附件UAT上传 xlsx, pdf 等
  • 飞书应用,具有 wikidocxdrive 权限
  • 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'])")
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
导入的文档归应用所有,移动到 Wiki 需要用户所有权。
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\"}"
⚠️ 关键:此步骤后等 3 秒再继续!
Bash
sleep 3  # 不要跳过
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\"}"
Mermaid 代码块被导入为纯文本段落(block_type=2),需要替换成原生 Mermaid 图表块(block_type=40)。⚠️ 从下往上处理,避免删除后索引偏移。Mermaid 块的关键结构:
Json
{
  "block_type": 40,
  "add_ons": {
    "component_type_id": "blk_631fefbbae02400430b8f9f4",
    "record": "{\"data\":\"graph TD\\n    A --> B\",\"theme\":\"default\",\"view\":\"chart\"}"
  }
}
将本地图片上传到文档块中。这是一个三阶段过程:5a. 创建空图片块 (block_type 27)
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}'
返回图片块的 block_id(初始为 100×100 占位图)。5b. 上传图片文件
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"
返回 file_token5c. 更新图片块
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}\"}}"
关键字段:replace_image(不是 update_image)。PATCH 后,块将更新为正确的尺寸。
上传文件(xlsx, pdf 等)作为可下载的附件卡片。模式与图片相同,但块类型和 parent_type 不同。6a. 创建空文件块 (block_type 23)
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}'
⚠️ 返回结构中文件块被包装在 View 块内:
  • response.children[0] → View 块 (type 33)
  • response.children[0].children[0] → 文件块 (type 23) ← 使用这个 block_id
6b. 上传文件
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"
6c. 更新文件块
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}\"}}"
图片 vs 文件对比表:
图片文件
block_type2723
parent_typedocx_imagedocx_file
PATCH 字段replace_imagereplace_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
Share this post:
Enjoy this post? Subscribe via RSS: English | 中文