feishu-docs: Feishu Docx API skill for programmatic document creation and editing
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
---
|
||||
name: feishu-docs
|
||||
description: Create and edit Feishu/Lark documents programmatically via the Docx Open API — create docs, add content blocks, read raw text, and manage permissions. Use when the user asks to create a Feishu doc from scratch or add structured content.
|
||||
version: 1.0.0
|
||||
author: agent
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [feishu, lark, documents, docx, api, content]
|
||||
related_skills: [homelab-services]
|
||||
---
|
||||
|
||||
# Feishu Docs — Document Creation & Editing
|
||||
|
||||
## When to Use
|
||||
|
||||
- User asks you to create a Feishu document (docx) with structured content
|
||||
- User wants a report, summary, or reference saved as a Feishu doc
|
||||
- User says "帮我创建一份飞书文档" or similar
|
||||
- Supplement to lark-mcp MCP tools (which cover search, import, bitable, IM but not block-level editing)
|
||||
|
||||
## Trigger Conditions
|
||||
|
||||
- Keywords: 飞书文档, 创建文档, 写个文档, Feishu doc, Lark docx
|
||||
- NOT: 飞书表格 (use lark-mcp bitable tools), 发送消息 (use `send_message` or lark-mcp IM tools)
|
||||
|
||||
## Architecture
|
||||
|
||||
Two complementary systems:
|
||||
|
||||
| System | Strengths | Gaps |
|
||||
|--------|-----------|------|
|
||||
| **lark-mcp** (MCP server) | Bitable CRUD, IM/groups, document search, markdown→doc import | No block-level editing, no doc creation |
|
||||
| **Feishu Docx API** (this skill) | Create docs, add/edit/delete blocks, read raw content | Requires manual API calls via Python |
|
||||
|
||||
Use lark-mcp for everything it covers. Fall back to direct API for document creation and block editing.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The Feishu app must have these permissions in the developer console:
|
||||
- `docx:document` — create and manage documents
|
||||
- `drive:drive` — access cloud documents (for import API)
|
||||
|
||||
Without these, the API returns 404. User must add them at https://open.feishu.cn/app.
|
||||
|
||||
Credentials are in `~/.hermes/.env`:
|
||||
```
|
||||
FEISHU_APP_ID=cli_xxx
|
||||
FEISHU_APP_SECRET=xxx
|
||||
```
|
||||
|
||||
## Workflow: Create a Document from Scratch
|
||||
|
||||
### Step 1: Get tenant access token
|
||||
|
||||
```python
|
||||
import requests
|
||||
r = requests.post(
|
||||
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
|
||||
json={"app_id": aid, "app_secret": sec}, timeout=10
|
||||
)
|
||||
token = r.json()["tenant_access_token"]
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
```
|
||||
|
||||
### Step 2: Create empty document
|
||||
|
||||
```python
|
||||
r = requests.post(
|
||||
"https://open.feishu.cn/open-apis/docx/v1/documents",
|
||||
headers=headers,
|
||||
json={"title": "文档标题"},
|
||||
timeout=10
|
||||
)
|
||||
doc_id = r.json()["data"]["document"]["document_id"]
|
||||
# URL: https://xiteng.feishu.cn/docx/{doc_id}
|
||||
```
|
||||
|
||||
### Step 3: Add content blocks
|
||||
|
||||
The document root is also the root block. Add children to it:
|
||||
|
||||
```python
|
||||
blocks = [
|
||||
{"block_type": 3, "heading1": {"elements": [
|
||||
{"text_run": {"content": "大标题"}}
|
||||
]}},
|
||||
{"block_type": 2, "text": {"elements": [
|
||||
{"text_run": {"content": "正文段落"}}
|
||||
]}},
|
||||
{"block_type": 12, "bullet": {"elements": [
|
||||
{"text_run": {"content": "列表项"}}
|
||||
]}},
|
||||
]
|
||||
|
||||
# Batch up to 50 blocks per request
|
||||
r = requests.post(
|
||||
f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_id}/blocks/{doc_id}/children",
|
||||
headers=headers,
|
||||
json={"children": blocks},
|
||||
timeout=15
|
||||
)
|
||||
```
|
||||
|
||||
### Block type reference
|
||||
|
||||
| block_type | Name | Structure |
|
||||
|------------|------|-----------|
|
||||
| 2 | Text paragraph | `{"text": {"elements": [{"text_run": {"content": "..."}}]}}` |
|
||||
| 3 | Heading 1 | `{"heading1": {"elements": ...}}` |
|
||||
| 4 | Heading 2 | `{"heading2": {"elements": ...}}` |
|
||||
| 5 | Heading 3 | `{"heading3": {"elements": ...}}` |
|
||||
| 6 | Heading 4 | `{"heading4": {"elements": ...}}` |
|
||||
| 12 | Bullet list | `{"bullet": {"elements": ...}}` |
|
||||
| 13 | Ordered list | `{"ordered": {"elements": ...}}` |
|
||||
| 14 | Code block | `{"code": {"elements": ..., "style": {"language": 1}}}` |
|
||||
| 17 | Callout/hint | `{"callout": {"elements": ...}}` |
|
||||
| 22 | Divider | `{"divider": {}}` |
|
||||
| 27 | Table | Complex — see API docs |
|
||||
|
||||
### Step 4: Read back and verify
|
||||
|
||||
```python
|
||||
r = requests.get(
|
||||
f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_id}/raw_content",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10
|
||||
)
|
||||
content = r.json()["data"]["content"]
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### 1. 404 on import API — missing permissions
|
||||
The `drive/v1/files/import` API requires `drive:drive` permission. Without it, returns 404.
|
||||
**Fix**: User must add the permission in Feishu Developer Console → App → Permissions.
|
||||
|
||||
### 2. ENV var redaction in scripts
|
||||
When writing Python scripts that read `~/.hermes/.env`, do NOT write lines like:
|
||||
```python
|
||||
# ❌ Redaction will eat the string
|
||||
elif line.startswith("FEISHU_APP_SECRET=*** ...
|
||||
```
|
||||
Instead construct the key name dynamically:
|
||||
```python
|
||||
# ✅ Safe from redaction
|
||||
if k == "FEISHU_APP_SECRET": ...
|
||||
# or
|
||||
prefix = "FEISHU_APP"
|
||||
if k == prefix + "_SECRET": ...
|
||||
```
|
||||
|
||||
### 3. Block batch limit
|
||||
The children API accepts at most 50 blocks per request. Split large documents into batches.
|
||||
|
||||
### 4. Document ID = Root block ID
|
||||
The `document_id` returned from create is also the root block ID. Use it for both:
|
||||
- `GET /docx/v1/documents/{doc_id}` — get document metadata
|
||||
- `POST /docx/v1/documents/{doc_id}/blocks/{doc_id}/children` — add blocks
|
||||
|
||||
## Integration with lark-mcp
|
||||
|
||||
When lark-mcp is connected (see `references/lark-mcp-setup.md`), use it for:
|
||||
- `docx_builtin_search` — search existing documents
|
||||
- `docx_v1_document_rawContent` — read document plain text
|
||||
- `docx_builtin_import` — quick markdown→doc conversion (creates new doc)
|
||||
- `bitable_*` — multi-dimensional table operations
|
||||
- `im_*` — messages and groups
|
||||
|
||||
Use direct API (this skill) when you need:
|
||||
- Create a blank document with a specific title
|
||||
- Add/edit/delete individual blocks
|
||||
- Modify existing document structure
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
- [ ] `FEISHU_APP_ID` and `FEISHU_APP_SECRET` are set in `.env`
|
||||
- [ ] App has `docx:document` permission
|
||||
- [ ] Document created: `GET /docx/v1/documents/{id}` returns 200
|
||||
- [ ] Content added: `GET /docx/v1/documents/{id}/blocks` returns blocks
|
||||
- [ ] User can open the URL in Feishu
|
||||
|
||||
## Related Files
|
||||
|
||||
- `scripts/create_doc.py` — reusable script: create doc + add content from markdown
|
||||
- `references/lark-mcp-setup.md` — MCP server installation and credential management
|
||||
- `references/docx-block-types.md` — full block type reference with examples
|
||||
@@ -0,0 +1,82 @@
|
||||
# lark-mcp MCP Server Setup
|
||||
|
||||
## Overview
|
||||
|
||||
The [lark-mcp](https://github.com/whatevertogo/FeiShuSkill) package wraps the official Feishu/Lark MCP server
|
||||
(`@larksuiteoapi/lark-mcp`), exposing 19 tools for bitable, IM, document search, and wiki operations.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
hermes mcp add lark-mcp \
|
||||
--command npx \
|
||||
--args -y --args @larksuiteoapi/lark-mcp --args mcp \
|
||||
--args -a --args <FEISHU_APP_ID> \
|
||||
--args -s --args <FEISHU_APP_SECRET>
|
||||
```
|
||||
|
||||
## Common Pitfall: `hermes mcp add` produces malformed config
|
||||
|
||||
**Symptom**: After adding via CLI, `hermes mcp test lark-mcp` fails with "Connection closed".
|
||||
|
||||
**Root cause**: `hermes mcp add` may include the literal string `--args` as a positional argument
|
||||
in the config's `args` list, producing:
|
||||
```yaml
|
||||
args:
|
||||
- -y
|
||||
- --args # ← should NOT be here
|
||||
- '@larksuiteoapi/lark-mcp'
|
||||
- --args # ← should NOT be here
|
||||
- mcp
|
||||
```
|
||||
|
||||
**Fix**: Manually edit `~/.hermes/config.yaml` under `mcp_servers.lark-mcp`:
|
||||
```yaml
|
||||
lark-mcp:
|
||||
command: npx
|
||||
args:
|
||||
- -y
|
||||
- '@larksuiteoapi/lark-mcp'
|
||||
- mcp
|
||||
- -a
|
||||
- cli_xxxxxxxxxx
|
||||
- -s
|
||||
- your_secret_here
|
||||
enabled: true
|
||||
```
|
||||
|
||||
The `--args` tokens must NOT appear in the final config — only the actual argument values.
|
||||
|
||||
## Enabling after fix
|
||||
|
||||
After correcting the config, the MCP tools load on next session start. To load immediately:
|
||||
- Hermes CLI: `/reload-mcp` then `/reset`
|
||||
- Or restart Hermes
|
||||
|
||||
## Available Tools (19 total)
|
||||
|
||||
| Category | Tools |
|
||||
|----------|-------|
|
||||
| Bitable | `bitable_v1_app_create`, `bitable_v1_appTable_create/list`, `bitable_v1_appTableField_list`, `bitable_v1_appTableRecord_create/search/update` |
|
||||
| IM | `im_v1_message_create/list`, `im_v1_chat_create/list`, `im_v1_chatMembers_get` |
|
||||
| Document | `docx_builtin_search`, `docx_v1_document_rawContent`, `docx_builtin_import` |
|
||||
| Wiki | `wiki_v1_node_search`, `wiki_v2_space_getNode` |
|
||||
| Drive | `drive_v1_permissionMember_create` |
|
||||
| Contact | `contact_v3_user_batchGetId` |
|
||||
|
||||
## Tool Naming Convention
|
||||
|
||||
When calling from Hermes: `mcp__lark-mcp__tool_name` (double underscore between server and tool).
|
||||
|
||||
## Required Permissions
|
||||
|
||||
The Feishu app needs these permissions in the developer console:
|
||||
|
||||
| Permission | Required for |
|
||||
|------------|-------------|
|
||||
| `im:message` | Send/receive messages |
|
||||
| `im:chat` | Create groups, list members |
|
||||
| `bitable:app` | Multi-dimensional table operations |
|
||||
| `docx:document` | Read document content |
|
||||
| `drive:drive` | Search documents, import files |
|
||||
| `wiki:wiki` | Knowledge base operations |
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create a Feishu document and populate it with content blocks.
|
||||
|
||||
Usage: python3 scripts/create_doc.py <title> <markdown_file>
|
||||
|
||||
Reads Feishu credentials from ~/.hermes/.env (FEISHU_APP_ID, FEISHU_APP_SECRET).
|
||||
Creates a docx document with the given title, then converts basic markdown
|
||||
to Feishu doc blocks and adds them.
|
||||
|
||||
Currently supports: # H1, ## H2, ### H3, - bullet, blank line separator,
|
||||
plain text paragraphs. For richer content, extend build_blocks().
|
||||
"""
|
||||
|
||||
import json, os, re, requests, sys
|
||||
|
||||
|
||||
def get_feishu_token():
|
||||
"""Get tenant access token from Feishu Open API."""
|
||||
env_path = os.path.expanduser("~/.hermes/.env")
|
||||
creds = {}
|
||||
with open(env_path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
creds[k.strip()] = v.strip().strip('"').strip("'")
|
||||
|
||||
aid = creds.get("FEISHU_APP_ID")
|
||||
sec = creds.get("FEISHU_APP_SECRET")
|
||||
if not aid or not sec:
|
||||
sys.exit("FEISHU_APP_ID or FEISHU_APP_SECRET not found in .env")
|
||||
|
||||
r = requests.post(
|
||||
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
|
||||
json={"app_id": aid, "app_secret": sec},
|
||||
timeout=10,
|
||||
)
|
||||
return r.json()["tenant_access_token"], aid, sec
|
||||
|
||||
|
||||
def text_element(content):
|
||||
return [{"text_run": {"content": content}}]
|
||||
|
||||
|
||||
def build_blocks(markdown_text):
|
||||
"""Convert basic markdown to Feishu doc blocks."""
|
||||
blocks = []
|
||||
lines = markdown_text.strip().split("\n")
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# Blank line → divider or skip
|
||||
if not line.strip():
|
||||
# Two blank lines → divider
|
||||
if i + 1 < len(lines) and not lines[i + 1].strip():
|
||||
blocks.append({"block_type": 22, "divider": {}})
|
||||
i += 2
|
||||
continue
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Heading 3
|
||||
if line.startswith("### "):
|
||||
blocks.append({"block_type": 5, "heading3": {"elements": text_element(line[4:])}})
|
||||
# Heading 2
|
||||
elif line.startswith("## "):
|
||||
blocks.append({"block_type": 4, "heading2": {"elements": text_element(line[3:])}})
|
||||
# Heading 1
|
||||
elif line.startswith("# "):
|
||||
blocks.append({"block_type": 3, "heading1": {"elements": text_element(line[2:])}})
|
||||
# Bullet
|
||||
elif line.startswith("- ") or line.startswith("* "):
|
||||
blocks.append({"block_type": 12, "bullet": {"elements": text_element(line[2:])}})
|
||||
# Plain text
|
||||
else:
|
||||
blocks.append({"block_type": 2, "text": {"elements": text_element(line)}})
|
||||
i += 1
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 create_doc.py <title> [markdown_file]")
|
||||
print(" If no markdown_file, reads from stdin.")
|
||||
sys.exit(1)
|
||||
|
||||
title = sys.argv[1]
|
||||
if len(sys.argv) >= 3:
|
||||
with open(sys.argv[2], encoding="utf-8") as f:
|
||||
markdown = f.read()
|
||||
else:
|
||||
markdown = sys.stdin.read()
|
||||
|
||||
token, _, _ = get_feishu_token()
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
|
||||
# Step 1: Create document
|
||||
print(f"Creating doc: {title}")
|
||||
r = requests.post(
|
||||
"https://open.feishu.cn/open-apis/docx/v1/documents",
|
||||
headers=headers,
|
||||
json={"title": title},
|
||||
timeout=10,
|
||||
)
|
||||
result = r.json()
|
||||
if result.get("code") != 0:
|
||||
sys.exit(f"Create failed: {result.get('msg')}")
|
||||
|
||||
doc_id = result["data"]["document"]["document_id"]
|
||||
print(f" doc_id: {doc_id}")
|
||||
|
||||
# Step 2: Build and add blocks
|
||||
blocks = build_blocks(markdown)
|
||||
if not blocks:
|
||||
print(" No blocks to add (empty markdown)")
|
||||
else:
|
||||
batch_size = 50
|
||||
for i in range(0, len(blocks), batch_size):
|
||||
batch = blocks[i:i + batch_size]
|
||||
r = requests.post(
|
||||
f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_id}/blocks/{doc_id}/children",
|
||||
headers=headers,
|
||||
json={"children": batch},
|
||||
timeout=15,
|
||||
)
|
||||
result = r.json()
|
||||
if result.get("code") != 0:
|
||||
sys.exit(f"Block add failed (batch {i//batch_size}): {result.get('msg')}")
|
||||
print(f" Batch {i//batch_size + 1}: {len(batch)} blocks OK")
|
||||
|
||||
url = f"https://xiteng.feishu.cn/docx/{doc_id}"
|
||||
print(f"\n✅ {url}")
|
||||
print(doc_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user