feishu-docs: Feishu Docx API skill for programmatic document creation and editing
This commit is contained in:
@@ -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