飞书 Block 操作
通过 REST API 进行低级飞书文档块操作。当 feishu_doc 内置操作不足时使用:批量更新单元格、精确定位插入...
技能说明
name: feishu-block-ops description: | Low-level Feishu document block operations via REST API. Use when feishu_doc built-in actions are insufficient: batch update cells, precise position insert, traverse block tree, table row/column manipulation, image replacement, or any operation requiring direct block-level control. Complements feishu-doc, not a replacement.
Feishu Block Operations
Direct REST API operations for Feishu cloud documents when the feishu_doc tool's built-in actions don't cover your needs.
When to Use This (vs feishu_doc)
| Need | Use |
|---|---|
| Read/write/append document | feishu_doc |
| Create simple table | feishu_doc create_table_with_values |
| Upload image/file | feishu_doc upload_image/upload_file |
| Batch update 200 cells at once | This skill |
| Insert content at exact position | This skill (or feishu-md2blocks) |
| Traverse block tree | This skill |
| Table row/column insert/delete | This skill |
| Merge/unmerge table cells | This skill |
| Replace images in-place | This skill |
| Delete blocks by index range | This skill |
Authentication
Get tenant access token from OpenClaw config:
import json, urllib.request
def get_feishu_token():
with open(os.path.expanduser("~/.openclaw/openclaw.json")) as f:
c = json.load(f)["channels"]["feishu"]
payload = json.dumps({"app_id": c["appId"], "app_secret": c["appSecret"]}).encode()
req = urllib.request.Request(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
data=payload, headers={"Content-Type": "application/json"}, method="POST")
return json.loads(urllib.request.urlopen(req).read())["tenant_access_token"]
All API calls use header: Authorization: Bearer {token}
Rate Limits
| Operation | Limit |
|---|---|
| Read (GET) | 5 req/sec per app |
| Write (POST/PATCH/DELETE) | 3 req/sec per app, 3 req/sec per document |
Use time.sleep(0.35) between write calls. For reads, time.sleep(0.25).
API Reference
Base URL: https://open.feishu.cn/open-apis/docx/v1/documents
1. Get Block
GET /docx/v1/documents/{doc}/blocks/{block_id}
Returns single block with full content (type, elements, children IDs, styles).
2. Get Children (with optional full tree)
GET /docx/v1/documents/{doc}/blocks/{block_id}/children
?with_descendants=true # get ALL descendants, not just direct children
&page_size=500 # max 500
&document_revision_id=-1 # latest revision
Tip: Use with_descendants=true on table blocks to get all cells + cell content in one call.
3. Create Blocks (simple, flat only)
POST /docx/v1/documents/{doc}/blocks/{parent_id}/children
Body: {"children": [...blocks], "index": 0}
- Max 50 blocks per call
- Cannot create nested structures (e.g. table with cell content)
indexin body: 0=beginning, -1=end (default)
4. Create Nested Blocks (tables, grids, etc.)
POST /docx/v1/documents/{doc}/blocks/{parent_id}/descendant
Body: {
"children_id": ["temp_id_1", "temp_id_2"],
"descendants": [...all_blocks_with_parent_child_relations],
"index": 0
}
- Max 1000 blocks per call
children_id: only first-level child IDs (NOT grandchildren — causes error 1770006)descendants: flat array of ALL blocks including nested ones, each withblock_id,block_type,children(list of child temp IDs)- ⚠️
indexMUST be in request body, NOT as URL query parameter —?index=Nis silently ignored
5. Batch Update Blocks
PATCH /docx/v1/documents/{doc}/blocks/batch_update
Body: {"requests": [...update_requests]}
Max 200 blocks per call. Each request object contains block_id + one operation:
| Operation | Purpose |
|---|---|
update_text_elements | Replace text content + inline elements |
update_text_style | Change alignment, folded, language, wrap, background_color |
update_table_property | Modify column widths, header rows/columns |
insert_table_row | Insert rows at index |
insert_table_column | Insert columns at index |
delete_table_rows | Delete rows by index + count |
delete_table_columns | Delete columns by index + count |
merge_table_cells | Merge cells (row_start, row_end, column_start, column_end) |
unmerge_table_cells | Unmerge previously merged cells |
replace_image | Replace image block's content with new file_token |
Example: batch update text in multiple cells
requests = []
for block_id, new_text in updates.items():
requests.append({
"block_id": block_id,
"update_text_elements": {
"elements": [{"text_run": {"content": new_text}}]
}
})
api_call(token, "PATCH",
f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc}/blocks/batch_update",
{"requests": requests})
6. Update Single Block
PATCH /docx/v1/documents/{doc}/blocks/{block_id}
Body: {same operations as batch_update, without block_id wrapper}
7. Delete Blocks
DELETE /docx/v1/documents/{doc}/blocks/{parent_id}/children/batch_delete
Body: {"start_index": 0, "end_index": 5}
- ⚠️ Uses
start_index/end_index(half-open interval[start, end)), NOTblock_ids - Indices are relative to the parent block's children list
Block Types
| Type | ID | Notes |
|---|---|---|
| Page | 1 | Document root, always one |
| Text | 2 | Plain paragraph |
| Heading1–9 | 3–11 | |
| Bullet | 12 | Unordered list item |
| Ordered | 13 | Ordered list item |
| Code | 14 | Code block |
| Quote | 15 | Block quote |
| Todo | 17 | Checkbox item |
| Callout | 19 | Highlighted block |
| Divider | 22 | Horizontal rule (body: {}) |
| Grid | 24 | Multi-column layout |
| GridColumn | 25 | Column in grid |
| Image | 27 | Image block |
| Table | 31 | Table container |
| TableCell | 32 | Cell in table |
| QuoteContainer | 34 | Quote wrapper (body: {}) |
Text Elements
Text blocks contain an elements array. Each element is one of:
# Plain text
{"text_run": {"content": "hello", "text_element_style": {"bold": True, "link": {"url": "..."}}}}
# Mention user
{"mention_user": {"user_id": "ou_xxx", "text_element_style": {}}}
# Mention document
{"mention_doc": {"token": "xxx", "obj_type": 22, "text_element_style": {}}}
# Equation (LaTeX)
{"equation": {"content": "E=mc^2"}}
# Reminder
{"reminder": {"expire_time": 1234567890, "is_whole_day": True}}
Common Patterns
Pattern 1: Read table content
# 1. Get table's descendants in one call
url = f".../blocks/{table_block_id}/children?with_descendants=true&page_size=500&document_revision_id=-1"
items = api_call(token, "GET", url)["data"]["items"]
# 2. Extract text from cells
for item in items:
if item["block_type"] == 2 and "text" in item:
text = "".join(e.get("text_run", {}).get("content", "") for e in item["text"]["elements"])
Pattern 2: Insert Markdown at position
Use feishu-md2blocks skill's md2blocks.py script with --after <block_id>.
Or manually:
# 1. Convert markdown
convert_resp = api_call(token, "POST", ".../blocks/convert",
{"content_type": "markdown", "content": md_text})
# 2. Clean table blocks (remove merge_info)
for block in convert_resp["data"]["blocks"]:
if block.get("block_type") == 31 and "table" in block:
block["table"]["property"].pop("merge_info", None)
# 3. Insert at position (index IN BODY)
api_call(token, "POST", f".../blocks/{parent_id}/descendant", {
"children_id": convert_resp["data"]["first_level_block_ids"],
"descendants": convert_resp["data"]["blocks"],
"index": target_index
})
Pattern 3: Batch edit table cells
# 1. Get all descendants of table
items = get_descendants(table_id)
# 2. Build update map
updates = []
for item in items:
if needs_update(item):
updates.append({
"block_id": item["block_id"],
"update_text_elements": {
"elements": [{"text_run": {"content": new_value}}]
}
})
# 3. Batch update (max 200 per call)
for i in range(0, len(updates), 200):
api_call(token, "PATCH", f".../blocks/batch_update",
{"requests": updates[i:i+200]})
time.sleep(0.35)
Pattern 4: Delete then re-insert (position workaround)
When you need to replace content at a specific position:
# 1. Find the index range to replace
children = get_doc_children(doc)
start_idx = children.index(first_block_to_replace)
end_idx = children.index(last_block_to_replace) + 1
# 2. Delete old blocks
api_call(token, "DELETE", f".../children/batch_delete",
{"start_index": start_idx, "end_index": end_idx})
# 3. Insert new content at same position
api_call(token, "POST", f".../blocks/{doc}/descendant", {
"children_id": new_ids,
"descendants": new_blocks,
"index": start_idx
})
Gotchas & Lessons Learned
/descendantindex in body, not URL — The most common pitfall.?index=Ncompiles but is silently ignored.batch_deleteuses index range —{"start_index": 0, "end_index": 5}deletes children[0..4]. Do NOT passblock_ids.- Table
merge_infois read-only — Must strip from blocks before insertion or API returns error. children_idis first-level only — Including grandchild IDs inchildren_idcauses error 1770006.- Rate limit is per-document — Multiple concurrent edits to the same doc share the 3/sec limit.
with_descendants=truesaves calls — One GET instead of N+1 for reading table content.- Convert API returns temp IDs — After insertion, actual block IDs differ from the temp IDs used during convert.
如何使用「飞书 Block 操作」?
- 打开小龙虾AI(Web 或 iOS App)
- 点击上方「立即使用」按钮,或在对话框中输入任务描述
- 小龙虾AI 会自动匹配并调用「飞书 Block 操作」技能完成任务
- 结果即时呈现,支持继续对话优化