{"skill":{"slug":"feishu-block-ops","displayName":"Feishu Block Ops","summary":"Low-level Feishu document block operations via REST API. Use when feishu_doc built-in actions are insufficient: batch update cells, precise position insert,...","description":"---\nname: feishu-block-ops\ndescription: |\n  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.\n---\n\n# Feishu Block Operations\n\nDirect REST API operations for Feishu cloud documents when the `feishu_doc` tool's built-in actions don't cover your needs.\n\n## When to Use This (vs feishu_doc)\n\n| Need | Use |\n|------|-----|\n| Read/write/append document | `feishu_doc` |\n| Create simple table | `feishu_doc` `create_table_with_values` |\n| Upload image/file | `feishu_doc` `upload_image`/`upload_file` |\n| **Batch update 200 cells at once** | **This skill** |\n| **Insert content at exact position** | **This skill** (or `feishu-md2blocks`) |\n| **Traverse block tree** | **This skill** |\n| **Table row/column insert/delete** | **This skill** |\n| **Merge/unmerge table cells** | **This skill** |\n| **Replace images in-place** | **This skill** |\n| **Delete blocks by index range** | **This skill** |\n\n## Authentication\n\nGet tenant access token from OpenClaw config:\n\n```python\nimport json, urllib.request\n\ndef get_feishu_token():\n    with open(os.path.expanduser(\"~/.openclaw/openclaw.json\")) as f:\n        c = json.load(f)[\"channels\"][\"feishu\"]\n    payload = json.dumps({\"app_id\": c[\"appId\"], \"app_secret\": c[\"appSecret\"]}).encode()\n    req = urllib.request.Request(\n        \"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal\",\n        data=payload, headers={\"Content-Type\": \"application/json\"}, method=\"POST\")\n    return json.loads(urllib.request.urlopen(req).read())[\"tenant_access_token\"]\n```\n\nAll API calls use header: `Authorization: Bearer {token}`\n\n## Rate Limits\n\n| Operation | Limit |\n|-----------|-------|\n| Read (GET) | 5 req/sec per app |\n| Write (POST/PATCH/DELETE) | 3 req/sec per app, 3 req/sec per document |\n\nUse `time.sleep(0.35)` between write calls. For reads, `time.sleep(0.25)`.\n\n## API Reference\n\nBase URL: `https://open.feishu.cn/open-apis/docx/v1/documents`\n\n### 1. Get Block\n\n```\nGET /docx/v1/documents/{doc}/blocks/{block_id}\n```\n\nReturns single block with full content (type, elements, children IDs, styles).\n\n### 2. Get Children (with optional full tree)\n\n```\nGET /docx/v1/documents/{doc}/blocks/{block_id}/children\n    ?with_descendants=true    # get ALL descendants, not just direct children\n    &page_size=500            # max 500\n    &document_revision_id=-1  # latest revision\n```\n\n**Tip:** Use `with_descendants=true` on table blocks to get all cells + cell content in one call.\n\n### 3. Create Blocks (simple, flat only)\n\n```\nPOST /docx/v1/documents/{doc}/blocks/{parent_id}/children\nBody: {\"children\": [...blocks], \"index\": 0}\n```\n\n- Max 50 blocks per call\n- **Cannot** create nested structures (e.g. table with cell content)\n- `index` in body: 0=beginning, -1=end (default)\n\n### 4. Create Nested Blocks (tables, grids, etc.)\n\n```\nPOST /docx/v1/documents/{doc}/blocks/{parent_id}/descendant\nBody: {\n    \"children_id\": [\"temp_id_1\", \"temp_id_2\"],\n    \"descendants\": [...all_blocks_with_parent_child_relations],\n    \"index\": 0\n}\n```\n\n- Max 1000 blocks per call\n- `children_id`: only first-level child IDs (NOT grandchildren — causes error 1770006)\n- `descendants`: flat array of ALL blocks including nested ones, each with `block_id`, `block_type`, `children` (list of child temp IDs)\n- ⚠️ **`index` MUST be in request body, NOT as URL query parameter** — `?index=N` is silently ignored\n\n### 5. Batch Update Blocks\n\n```\nPATCH /docx/v1/documents/{doc}/blocks/batch_update\nBody: {\"requests\": [...update_requests]}\n```\n\nMax 200 blocks per call. Each request object contains `block_id` + one operation:\n\n| Operation | Purpose |\n|-----------|---------|\n| `update_text_elements` | Replace text content + inline elements |\n| `update_text_style` | Change alignment, folded, language, wrap, background_color |\n| `update_table_property` | Modify column widths, header rows/columns |\n| `insert_table_row` | Insert rows at index |\n| `insert_table_column` | Insert columns at index |\n| `delete_table_rows` | Delete rows by index + count |\n| `delete_table_columns` | Delete columns by index + count |\n| `merge_table_cells` | Merge cells (row_start, row_end, column_start, column_end) |\n| `unmerge_table_cells` | Unmerge previously merged cells |\n| `replace_image` | Replace image block's content with new file_token |\n\n#### Example: batch update text in multiple cells\n\n```python\nrequests = []\nfor block_id, new_text in updates.items():\n    requests.append({\n        \"block_id\": block_id,\n        \"update_text_elements\": {\n            \"elements\": [{\"text_run\": {\"content\": new_text}}]\n        }\n    })\n\napi_call(token, \"PATCH\",\n    f\"https://open.feishu.cn/open-apis/docx/v1/documents/{doc}/blocks/batch_update\",\n    {\"requests\": requests})\n```\n\n### 6. Update Single Block\n\n```\nPATCH /docx/v1/documents/{doc}/blocks/{block_id}\nBody: {same operations as batch_update, without block_id wrapper}\n```\n\n### 7. Delete Blocks\n\n```\nDELETE /docx/v1/documents/{doc}/blocks/{parent_id}/children/batch_delete\nBody: {\"start_index\": 0, \"end_index\": 5}\n```\n\n- ⚠️ Uses `start_index`/`end_index` (half-open interval `[start, end)`), **NOT** `block_ids`\n- Indices are relative to the parent block's children list\n\n## Block Types\n\n| Type | ID | Notes |\n|------|---:|-------|\n| Page | 1 | Document root, always one |\n| Text | 2 | Plain paragraph |\n| Heading1–9 | 3–11 | |\n| Bullet | 12 | Unordered list item |\n| Ordered | 13 | Ordered list item |\n| Code | 14 | Code block |\n| Quote | 15 | Block quote |\n| Todo | 17 | Checkbox item |\n| Callout | 19 | Highlighted block |\n| Divider | 22 | Horizontal rule (body: `{}`) |\n| Grid | 24 | Multi-column layout |\n| GridColumn | 25 | Column in grid |\n| Image | 27 | Image block |\n| Table | 31 | Table container |\n| TableCell | 32 | Cell in table |\n| QuoteContainer | 34 | Quote wrapper (body: `{}`) |\n\n## Text Elements\n\nText blocks contain an `elements` array. Each element is one of:\n\n```python\n# Plain text\n{\"text_run\": {\"content\": \"hello\", \"text_element_style\": {\"bold\": True, \"link\": {\"url\": \"...\"}}}}\n\n# Mention user\n{\"mention_user\": {\"user_id\": \"ou_xxx\", \"text_element_style\": {}}}\n\n# Mention document\n{\"mention_doc\": {\"token\": \"xxx\", \"obj_type\": 22, \"text_element_style\": {}}}\n\n# Equation (LaTeX)\n{\"equation\": {\"content\": \"E=mc^2\"}}\n\n# Reminder\n{\"reminder\": {\"expire_time\": 1234567890, \"is_whole_day\": True}}\n```\n\n## Common Patterns\n\n### Pattern 1: Read table content\n\n```python\n# 1. Get table's descendants in one call\nurl = f\".../blocks/{table_block_id}/children?with_descendants=true&page_size=500&document_revision_id=-1\"\nitems = api_call(token, \"GET\", url)[\"data\"][\"items\"]\n\n# 2. Extract text from cells\nfor item in items:\n    if item[\"block_type\"] == 2 and \"text\" in item:\n        text = \"\".join(e.get(\"text_run\", {}).get(\"content\", \"\") for e in item[\"text\"][\"elements\"])\n```\n\n### Pattern 2: Insert Markdown at position\n\nUse `feishu-md2blocks` skill's `md2blocks.py` script with `--after <block_id>`.\n\nOr manually:\n```python\n# 1. Convert markdown\nconvert_resp = api_call(token, \"POST\", \".../blocks/convert\",\n    {\"content_type\": \"markdown\", \"content\": md_text})\n\n# 2. Clean table blocks (remove merge_info)\nfor block in convert_resp[\"data\"][\"blocks\"]:\n    if block.get(\"block_type\") == 31 and \"table\" in block:\n        block[\"table\"][\"property\"].pop(\"merge_info\", None)\n\n# 3. Insert at position (index IN BODY)\napi_call(token, \"POST\", f\".../blocks/{parent_id}/descendant\", {\n    \"children_id\": convert_resp[\"data\"][\"first_level_block_ids\"],\n    \"descendants\": convert_resp[\"data\"][\"blocks\"],\n    \"index\": target_index\n})\n```\n\n### Pattern 3: Batch edit table cells\n\n```python\n# 1. Get all descendants of table\nitems = get_descendants(table_id)\n\n# 2. Build update map\nupdates = []\nfor item in items:\n    if needs_update(item):\n        updates.append({\n            \"block_id\": item[\"block_id\"],\n            \"update_text_elements\": {\n                \"elements\": [{\"text_run\": {\"content\": new_value}}]\n            }\n        })\n\n# 3. Batch update (max 200 per call)\nfor i in range(0, len(updates), 200):\n    api_call(token, \"PATCH\", f\".../blocks/batch_update\",\n        {\"requests\": updates[i:i+200]})\n    time.sleep(0.35)\n```\n\n### Pattern 4: Delete then re-insert (position workaround)\n\nWhen you need to replace content at a specific position:\n\n```python\n# 1. Find the index range to replace\nchildren = get_doc_children(doc)\nstart_idx = children.index(first_block_to_replace)\nend_idx = children.index(last_block_to_replace) + 1\n\n# 2. Delete old blocks\napi_call(token, \"DELETE\", f\".../children/batch_delete\",\n    {\"start_index\": start_idx, \"end_index\": end_idx})\n\n# 3. Insert new content at same position\napi_call(token, \"POST\", f\".../blocks/{doc}/descendant\", {\n    \"children_id\": new_ids,\n    \"descendants\": new_blocks,\n    \"index\": start_idx\n})\n```\n\n## Gotchas & Lessons Learned\n\n1. **`/descendant` index in body, not URL** — The most common pitfall. `?index=N` compiles but is silently ignored.\n2. **`batch_delete` uses index range** — `{\"start_index\": 0, \"end_index\": 5}` deletes children[0..4]. Do NOT pass `block_ids`.\n3. **Table `merge_info` is read-only** — Must strip from blocks before insertion or API returns error.\n4. **`children_id` is first-level only** — Including grandchild IDs in `children_id` causes error 1770006.\n5. **Rate limit is per-document** — Multiple concurrent edits to the same doc share the 3/sec limit.\n6. **`with_descendants=true` saves calls** — One GET instead of N+1 for reading table content.\n7. **Convert API returns temp IDs** — After insertion, actual block IDs differ from the temp IDs used during convert.\n","tags":{"latest":"1.0.0"},"stats":{"comments":0,"downloads":671,"installsAllTime":0,"installsCurrent":0,"stars":0,"versions":1},"createdAt":1772605566752,"updatedAt":1779077521784},"latestVersion":{"version":"1.0.0","createdAt":1772605566752,"changelog":"feishu-block-ops 1.0.0\n\n- Initial release providing direct Feishu document block operations via REST API.\n- Supports batch cell updates, precise positioned inserts, block tree traversal, table row/column manipulation, and in-place image replacement.\n- Enables advanced use cases not covered by the standard feishu_doc actions.\n- Detailed usage instructions, rate limits, API reference, and common patterns included in documentation.","license":null},"metadata":null,"owner":{"handle":"deadblue22","userId":"s17dp4k7kf43jx2arar9shhd218859yy","displayName":"deadblue","image":"https://avatars.githubusercontent.com/u/448649?v=4"},"moderation":{"isSuspicious":false,"isMalwareBlocked":false,"verdict":"clean","reasonCodes":["review.llm_review"],"summary":"Review: review.llm_review","engineVersion":"v2.4.24","updatedAt":1780089753377}}