@clawhub-hellostar999-6ebde8e541
飞书(Feishu/Lark)文档与消息操作技能。When to use: 用户要求创建、删除、修改飞书文档;查询或更新文档中指定行/列的数据;向飞书联系人或群聊发送消息。Triggers: "创建飞书文档"、"删除文档"、"修改文档内容"、"更新第X行第Y列"、"查询文档"、"发送飞书消息"、"发消息给群"。
---
name: feishu-ops
description: |
飞书(Feishu/Lark)文档与消息操作技能。When to use: 用户要求创建、删除、修改飞书文档;查询或更新文档中指定行/列的数据;向飞书联系人或群聊发送消息。Triggers: "创建飞书文档"、"删除文档"、"修改文档内容"、"更新第X行第Y列"、"查询文档"、"发送飞书消息"、"发消息给群"。
---
# 飞书操作技能 (feishu-ops)
支持飞书文档的完整 CRUD 以及单元格精细操作,同时支持发送消息给个人和群聊。
## 凭证配置
所有操作需要 `app_id` 和 `app_secret`,位于技能目录的 `scripts/config.json`:
```json
{
"app_id": "cli_xxxxx",
"app_secret": "xxxxx"
}
```
若无配置文件,脚本会报错并提示创建。
## 文档操作
### 创建文档
```python
python scripts/feishu_doc.py create "文档标题"
```
成功返回:`{"doc_token": "xxx", "doc_url": "https://feishu.cn/doc/xxx"}`
### 删除文档
```python
python scripts/feishu_doc.py delete <doc_token>
```
### 写入/追加文档内容
```python
# 全量写入(覆盖)
python scripts/feishu_doc.py write <doc_token> "## 标题\n这是内容"
# 追加段落
python scripts/feishu_doc.py append <doc_token> "新追加的段落"
```
### 读取文档
```python
python scripts/feishu_doc.py read <doc_token>
```
### 精细操作:单元格读写
飞书文档的 blocks API 支持按 block_id 精确操作。使用前需先 `read` 文档获取 block 树结构。
```python
# 查询指定 block 内容
python scripts/feishu_doc.py query-block <doc_token> <block_id>
# 更新指定 block 的文本内容
python scripts/feishu_doc.py update-block <doc_token> <block_id> "新的文本内容"
# 在文档末尾追加一个段落 block
python scripts/feishu_doc.py append-block <doc_token> "段落文本"
```
> **表格操作**:表格为复合 block。先用 `read` 获取表格 block_id,再遍历表格内部 cell blocks 进行读写。
## 消息操作
### 发送文本消息给用户
需要目标用户的 `open_id`(可用 `search_user.py` 查询)。
```python
python scripts/feishu_msg.py send-user <open_id> "消息内容"
```
### 发送文本消息到群
需要目标群的 `chat_id`(可用 `search_chat.py` 查询)。
```python
python scripts/feishu_msg.py send-chat <chat_id> "消息内容"
```
### 查询群聊消息
```python
python scripts/feishu_msg.py get-messages <chat_id> [page_size]
```
返回群内消息列表,自动正确显示中文内容。
### 发送文件到群
需要先安装 SDK:
```bash
pip install lark-oapi
```
发送本地文件到群:
```python
python scripts/feishu_msg.py send-file <chat_id> <本地文件路径>
```
例如发送到龙虾测试群:
```python
python scripts/feishu_msg.py send-file oc_2c6df8f6e06e88d34729baacc124b89e "C:\\Users\\10430\\Desktop\\采购数据.xlsx"
```
### 查询用户 open_id
```python
python scripts/feishu_msg.py search-user <姓名关键词>
```
### 查询群聊 chat_id
```python
python scripts/feishu_msg.py search-chat <群名关键词>
```
## 常用工作流
**创建文档并写入内容:**
1. `create` 获取 doc_token
2. `write` 或 `append-block` 写入内容
3. `read` 确认内容
**更新表格中第R行第C列:**
1. `read` 获取文档 block 树
2. 找到目标表格的 block_id
3. 遍历表格 rows/cells,用 `update-block` 更新目标单元格
**发消息给同事:**
1. `search-user` 查找 open_id
2. `send-user` 发送消息
## 脚本索引
| 脚本 | 功能 |
|------|------|
| `scripts/feishu_doc.py` | 文档 CRUD + block 精细操作 |
| `scripts/feishu_msg.py` | 消息发送 + 用户/群查询 + 获取消息 + 文件发送(需 lark-oapi SDK) |
| `scripts/config.json` | 凭证配置 |
| `references/api_ref.md` | 完整 API 参数说明 |
FILE:scripts/config.json
{
"app_id": "",
"app_secret": ""
}
FILE:scripts/feishu_doc.py
"""
飞书文档操作脚本
用法:
python feishu_doc.py create "<标题>"
python feishu_doc.py delete <doc_token>
python feishu_doc.py read <doc_token>
python feishu_doc.py write <doc_token> "<markdown内容>"
python feishu_doc.py append <doc_token> "<文本>"
python feishu_doc.py append-block <doc_token> "<段落文本>"
python feishu_doc.py query-block <doc_token> <block_id>
python feishu_doc.py update-block <doc_token> <block_id> "<新文本>"
"""
import json
import sys
import urllib.request
import urllib.error
SCRIPT_DIR = __file__.rsplit("/", 1)[0] if "/" in __file__ else __file__.rsplit("\\", 1)[0]
CONFIG_PATH = SCRIPT_DIR + "/config.json"
def load_config():
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
return json.load(f)
def get_tenant_access_token(app_id, app_secret):
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
data = json.dumps({"app_id": app_id, "app_secret": app_secret}).encode("utf-8")
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
return result.get("tenant_access_token", "")
def create_document(title):
config = load_config()
token = get_tenant_access_token(config["app_id"], config["app_secret"])
url = "https://open.feishu.cn/open-apis/docx/v1/documents"
data = json.dumps({"title": title}).encode("utf-8")
req = urllib.request.Request(url, data=data, headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
})
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
if result.get("code") != 0:
print(json.dumps(result, ensure_ascii=False))
return
doc = result["data"]["document"]
print(json.dumps({"doc_token": doc["document_id"], "doc_url": f"https://feishu.cn/docx/{doc['document_id']}"}, ensure_ascii=False))
def delete_document(doc_token):
config = load_config()
token = get_tenant_access_token(config["app_id"], config["app_secret"])
url = f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_token}"
req = urllib.request.Request(url, method="DELETE", headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
print(json.dumps(result, ensure_ascii=False))
def read_document(doc_token):
config = load_config()
token = get_tenant_access_token(config["app_id"], config["app_secret"])
url = f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_token}/blocks?page_size=500"
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
print(json.dumps(result, ensure_ascii=False, indent=2))
def _text_to_blocks(text):
"""将文本转为 Text Block(block_type=2)"""
blocks = []
for line in text.split("\n"):
blocks.append({
"block_type": 2, # Text Block
"text": {
"elements": [{"type": "text_run", "text_run": {"content": line}}]
}
})
return blocks
def write_document(doc_token, content):
config = load_config()
token = get_tenant_access_token(config["app_id"], config["app_secret"])
# 获取现有 blocks
url = f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_token}/blocks?page_size=500"
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
if result.get("code") != 0:
print(json.dumps(result, ensure_ascii=False))
return
items = result["data"]["items"]
if not items:
print(json.dumps({"error": "文档为空或不存在"}))
return
root_block_id = items[0]["block_id"]
# 写入:新 blocks
url2 = f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_token}/blocks/{root_block_id}/children"
blocks = _text_to_blocks(content)
data = json.dumps({"children": blocks, "index": 0}).encode("utf-8")
req2 = urllib.request.Request(url2, data=data, method="POST", headers={
"Content-Type": "application/json; charset=utf-8",
"Authorization": f"Bearer {token}"
})
with urllib.request.urlopen(req2) as resp:
result2 = json.loads(resp.read())
print(json.dumps(result2, ensure_ascii=False))
def append_paragraph(doc_token, text):
config = load_config()
token = get_tenant_access_token(config["app_id"], config["app_secret"])
url = f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_token}/blocks?page_size=500"
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
if result.get("code") != 0:
print(json.dumps(result, ensure_ascii=False))
return
items = result["data"]["items"]
if not items:
print(json.dumps({"error": "文档为空"}))
return
root_block_id = items[0]["block_id"]
url2 = f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_token}/blocks/{root_block_id}/children"
blocks = _text_to_blocks(text)
data = json.dumps({"children": blocks}).encode("utf-8")
req2 = urllib.request.Request(url2, data=data, method="POST", headers={
"Content-Type": "application/json; charset=utf-8",
"Authorization": f"Bearer {token}"
})
with urllib.request.urlopen(req2) as resp:
result2 = json.loads(resp.read())
print(json.dumps(result2, ensure_ascii=False))
def query_block(doc_token, block_id):
config = load_config()
token = get_tenant_access_token(config["app_id"], config["app_secret"])
url = f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_token}/blocks/{block_id}"
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
print(json.dumps(result, ensure_ascii=False, indent=2))
def update_block_text(doc_token, block_id, new_text):
"""更新指定 block 的文本内容(仅支持 Text/Heading/Bullet 等文本类 block)"""
config = load_config()
token = get_tenant_access_token(config["app_id"], config["app_secret"])
url = f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_token}/blocks/{block_id}"
data = json.dumps({
"update_text_elements": {
"elements": [{"type": "text_run", "text_run": {"content": new_text}}]
}
}).encode("utf-8")
req = urllib.request.Request(url, data=data, method="PATCH", headers={
"Content-Type": "application/json; charset=utf-8",
"Authorization": f"Bearer {token}"
})
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
print(json.dumps(result, ensure_ascii=False))
def append_block(doc_token, text):
"""追加段落 block(便捷别名)"""
append_paragraph(doc_token, text)
if __name__ == "__main__":
args = sys.argv[1:]
if len(args) < 1:
print("用法: python feishu_doc.py <command> [args...]")
sys.exit(1)
cmd = args[0]
if cmd == "create" and len(args) >= 2:
create_document(args[1])
elif cmd == "delete" and len(args) >= 2:
delete_document(args[1])
elif cmd == "read" and len(args) >= 2:
read_document(args[1])
elif cmd == "write" and len(args) >= 3:
write_document(args[1], args[2])
elif cmd == "append" and len(args) >= 3:
append_paragraph(args[1], args[2])
elif cmd == "append-block" and len(args) >= 3:
append_block(args[1], args[2])
elif cmd == "query-block" and len(args) >= 3:
query_block(args[1], args[2])
elif cmd == "update-block" and len(args) >= 4:
update_block_text(args[1], args[2], args[3])
else:
print("未知命令或参数不足")
sys.exit(1)
FILE:scripts/feishu_msg.py
"""
飞书消息操作脚本(支持文件发送)
用法:
python feishu_msg.py send-user <open_id> <消息内容>
python feishu_msg.py send-chat <chat_id> <消息内容>
python feishu_msg.py search-user <姓名关键词>
python feishu_msg.py search-chat <群名关键词>
python feishu_msg.py get-messages <chat_id> [page_size]
python feishu_msg.py send-file <chat_id> <本地文件路径>
"""
import json
import sys
import urllib.parse
import uuid
import datetime
import os
# 全局 UTF-8 输出修复(Windows GBK 终端适配)
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
SCRIPT_DIR = __file__.rsplit("/", 1)[0] if "/" in __file__ else __file__.rsplit("\\", 1)[0]
CONFIG_PATH = SCRIPT_DIR + "/config.json"
# 尝试导入 lark-oapi SDK,upload_file 函数会用到
try:
import lark_oapi as lark
from lark_oapi.api.im.v1 import *
SDK_AVAILABLE = True
except ImportError:
SDK_AVAILABLE = False
def load_config():
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
return json.load(f)
def get_tenant_access_token(app_id, app_secret):
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
data = json.dumps({"app_id": app_id, "app_secret": app_secret}).encode("utf-8")
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
return result.get("tenant_access_token", "")
def send_message(receive_id, receive_id_type, content, msg_type="text"):
if not SDK_AVAILABLE:
# fallback to urllib
import urllib.request
config = load_config()
token = get_tenant_access_token(config["app_id"], config["app_secret"])
url = "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=" + receive_id_type
payload = {
"receive_id": receive_id,
"msg_type": msg_type,
"content": json.dumps({"text": content}) if msg_type == "text" else content,
"uuid": str(uuid.uuid4())
}
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, method="POST", headers={
"Content-Type": "application/json; charset=utf-8",
"Authorization": f"Bearer {token}"
})
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
print(json.dumps(result, ensure_ascii=False))
return result
# Use SDK
client = lark.Client.builder() \
.app_id(load_config()["app_id"]) \
.app_secret(load_config()["app_secret"]) \
.log_level(lark.LogLevel.ERROR) \
.build()
request = CreateMessageRequest.builder() \
.receive_id_type(receive_id_type) \
.request_body(CreateMessageRequestBody.builder()
.receive_id(receive_id)
.msg_type(msg_type)
.content(content if msg_type != "text" else json.dumps({"text": content}))
.build()) \
.build()
response = client.im.v1.message.create(request)
print(json.dumps({"code": response.code, "msg": response.msg}, ensure_ascii=False))
return {"code": response.code, "msg": response.msg}
def send_user(open_id, content):
return send_message(open_id, "open_id", content)
def send_chat(chat_id, content):
return send_message(chat_id, "chat_id", content)
def _upload_file_http(filename, file_data):
"""通过 HTTP multipart/form-data 直接上传文件(适合所有文件大小)"""
import urllib.request, urllib.error
file_size = len(file_data)
boundary = uuid.uuid4().hex
# Build multipart form data
body_parts = []
body_parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="file_type"\r\n\r\nstream'.encode())
body_parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="file_name"\r\n\r\n{filename}'.encode())
body_parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="size"\r\n\r\n{file_size}'.encode())
header_part = f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="{filename}"\r\nContent-Type: application/octet-stream\r\n\r\n'.encode()
body_parts.append(header_part + file_data + f'\r\n--{boundary}--'.encode())
body = b'\r\n'.join(body_parts)
# Get token
config = load_config()
token = get_tenant_access_token(config["app_id"], config["app_secret"])
url = 'https://open.feishu.cn/open-apis/im/v1/files?receive_id_type=chat_id'
req = urllib.request.Request(url, data=body, headers={
'Authorization': 'Bearer ' + token,
'Content-Type': f'multipart/form-data; boundary={boundary}'
})
try:
with urllib.request.urlopen(req, timeout=max(60, file_size // 1024 // 100)) as r:
resp = json.loads(r.read())
if resp.get('code') == 0:
return resp['data']['file_key']
else:
print(json.dumps({"code": resp.get('code'), "msg": resp.get('msg')}, ensure_ascii=False))
return None
except urllib.error.HTTPError as e:
body_err = e.read().decode('utf-8', errors='replace')
try:
err_obj = json.loads(body_err)
print(json.dumps({"code": err_obj.get('code'), "msg": err_obj.get('msg')}, ensure_ascii=False))
except Exception:
print(json.dumps({"code": e.code, "msg": body_err[:500]}, ensure_ascii=False))
return None
def send_file(chat_id, filepath):
"""上传本地文件并发送到群聊"""
import io
if not SDK_AVAILABLE:
print("错误: 需要安装 lark-oapi SDK,请运行: pip install lark-oapi")
return
if not os.path.exists(filepath):
print(f"错误: 文件不存在: {filepath}")
return
filename = os.path.basename(filepath)
config = load_config()
client = lark.Client.builder() \
.app_id(config["app_id"]) \
.app_secret(config["app_secret"]) \
.log_level(lark.LogLevel.ERROR) \
.build()
# 1. 根据扩展名判断文件类型
ext = os.path.splitext(filename)[1].lower().lstrip('.')
image_exts = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
with open(filepath, 'rb') as f:
file_data = f.read()
file_size = len(file_data)
print(f"文件大小: {file_size/1024/1024:.2f} MB")
# 图片类型使用 image.create API
if ext in image_exts:
from lark_oapi.api.im.v1 import CreateImageRequest, CreateImageRequestBody
upload_req = CreateImageRequest.builder().request_body(
CreateImageRequestBody.builder().image_type('message').image(io.BytesIO(file_data)).build()
).build()
upload_resp = client.im.v1.image.create(upload_req)
if upload_resp.code != 0:
print(json.dumps({"code": upload_resp.code, "msg": upload_resp.msg}, ensure_ascii=False))
return
image_key = upload_resp.data.image_key
print(f"图片上传成功: {filename} -> image_key: {image_key}")
content = json.dumps({"image_key": image_key})
msg_type = "image"
else:
# 其他文件使用 HTTP 直传(所有文件均走 im/v1/files 接口)
file_key = _upload_file_http(filename, file_data)
if file_key is None:
return
print(f"文件上传成功: {filename} -> file_key: {file_key}")
content = json.dumps({"file_key": file_key})
msg_type = "file"
# 2. 发送消息
from lark_oapi.api.im.v1 import CreateMessageRequest, CreateMessageRequestBody
msg_request = CreateMessageRequest.builder() \
.receive_id_type("chat_id") \
.request_body(CreateMessageRequestBody.builder()
.receive_id(chat_id)
.msg_type(msg_type)
.content(content)
.build()) \
.build()
msg_response = client.im.v1.message.create(msg_request)
print(json.dumps({"code": msg_response.code, "msg": msg_response.msg}, ensure_ascii=False))
def search_user(keyword):
import urllib.request
config = load_config()
token = get_tenant_access_token(config["app_id"], config["app_secret"])
encoded_keyword = urllib.parse.quote(keyword)
url = f"https://open.feishu.cn/open-apis/contact/v3/users/search?query={encoded_keyword}&page_size=20&user_id_type=open_id"
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
print(json.dumps(result, ensure_ascii=False, indent=2))
return result
def search_chat(keyword):
import urllib.request
config = load_config()
token = get_tenant_access_token(config["app_id"], config["app_secret"])
encoded_keyword = urllib.parse.quote(keyword)
url = f"https://open.feishu.cn/open-apis/im/v1/chats?search_key={encoded_keyword}&page_size=20"
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
print(json.dumps(result, ensure_ascii=False, indent=2))
return result
def get_messages(chat_id, page_size=50):
"""获取群聊消息列表,正确解析并展示中文内容"""
import urllib.request
config = load_config()
token = get_tenant_access_token(config["app_id"], config["app_secret"])
url = f"https://open.feishu.cn/open-apis/im/v1/messages?container_id_type=chat&container_id={chat_id}&page_size={page_size}"
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
if result.get("code") != 0:
print(json.dumps(result, ensure_ascii=False, indent=2))
return result
items = result.get("data", {}).get("items", [])
print(f"共获取到 {len(items)} 条消息:")
print()
for it in items:
msg_type = it.get("msg_type")
sender = it.get("sender", {})
sender_id = sender.get("id", "")
sender_type = sender.get("sender_type", "")
ct = int(it.get("create_time", 0)) // 1000
dt = datetime.datetime.fromtimestamp(ct).strftime("%Y-%m-%d %H:%M:%S") if ct else "N/A"
body_str = it.get("body", {}).get("content", "{}")
body = json.loads(body_str)
print(f"[{dt}] {sender_type} {sender_id[:16]} | {msg_type}")
if msg_type == "text":
text = body.get("text", "")
print(f" {text}")
elif msg_type == "post":
title = body.get("title", "")
parts = []
for section in body.get("content", []):
for item in section:
tag = item.get("tag", "")
if tag == "text":
parts.append(item.get("text", ""))
elif tag == "at":
parts.append("@" + item.get("user_id", ""))
elif tag == "a":
parts.append(item.get("text", "") + "(" + item.get("href", "") + ")")
content = "".join(parts)
if title:
print(f" Title: {title}")
print(f" {content}")
elif msg_type == "file":
file_key = body.get("file_key", "")
print(f" [文件] key: {file_key}")
elif msg_type == "system":
template = body.get("template", "")
from_user = ",".join(body.get("from_user", []))
to_chatters = ",".join(body.get("to_chatters", []))
try:
content_str = template.format(from_user=from_user, to_chatters=to_chatters)
except Exception:
content_str = json.dumps(body, ensure_ascii=False)
print(f" {content_str}")
else:
print(f" {json.dumps(body, ensure_ascii=False)[:100]}")
print()
return result
if __name__ == "__main__":
args = sys.argv[1:]
if len(args) < 1:
print("用法: python feishu_msg.py <command> [args...]")
print("可用命令: send-user, send-chat, search-user, search-chat, get-messages, send-file")
sys.exit(1)
cmd = args[0]
if cmd == "send-user" and len(args) >= 3:
send_user(args[1], args[2])
elif cmd == "send-chat" and len(args) >= 3:
send_chat(args[1], args[2])
elif cmd == "search-user" and len(args) >= 2:
search_user(args[1])
elif cmd == "search-chat" and len(args) >= 2:
search_chat(args[1])
elif cmd == "get-messages" and len(args) >= 2:
page_size = int(args[2]) if len(args) >= 3 else 50
get_messages(args[1], page_size)
elif cmd == "send-file" and len(args) >= 3:
send_file(args[1], args[2])
else:
print("未知命令或参数不足")
print("可用命令: send-user, send-chat, search-user, search-chat, get-messages, send-file")
sys.exit(1)
FILE:scripts/fetch_chat_messages.py
import json, urllib.request, urllib.error, datetime
config = json.load(open('filepath'))
url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal'
data = json.dumps({'app_id': config['app_id'], 'app_secret': config['app_secret']}).encode()
req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'})
with urllib.request.urlopen(req) as r:
token = json.loads(r.read())['tenant_access_token']
chat_id = 'oc_3b77de8ba3bc9aa5512abfd8881604bb'
url2 = 'https://open.feishu.cn/open-apis/im/v1/messages?container_id_type=chat&container_id=' + chat_id + '&page_size=50'
req2 = urllib.request.Request(url2, headers={'Authorization': 'Bearer ' + token})
with urllib.request.urlopen(req2) as r:
result = json.loads(r.read())
items = result.get('data', {}).get('items', [])
print('Total messages:', len(items))
print()
msgs = []
for it in items:
msg_type = it.get('msg_type')
sender = it.get('sender', {}).get('id', '')
ct = int(it.get('create_time', 0)) // 1000
dt = datetime.datetime.fromtimestamp(ct).strftime('%Y-%m-%d %H:%M:%S') if ct else 'N/A'
body = json.loads(it.get('body', {}).get('content', '{}'))
if msg_type == 'text':
content = body.get('text', '')
msgs.append({'time': dt, 'type': 'text', 'sender': sender, 'content': content})
print(f"[text] {dt} | {sender} | {content}")
elif msg_type == 'post':
# rich text post
title = body.get('title', '')
parts = []
for section in body.get('content', []):
for item in section:
tag = item.get('tag', '')
if tag == 'text':
parts.append(item.get('text', ''))
elif tag == 'at':
parts.append('@' + item.get('user_id', ''))
content = title + ' ' + ''.join(parts)
msgs.append({'time': dt, 'type': 'post', 'sender': sender, 'content': content})
print(f"[post] {dt} | {sender} | {title} | {''.join(parts)}")
elif msg_type == 'system':
template = body.get('template', '')
from_user = ','.join(body.get('from_user', []))
to_chatters = ','.join(body.get('to_chatters', []))
content = template.format(from_user=from_user, to_chatters=to_chatters)
msgs.append({'time': dt, 'type': 'system', 'sender': 'system', 'content': content})
print(f"[system] {dt} | {content}")
# create document
url3 = 'https://open.feishu.cn/open-apis/docx/v1/documents'
data3 = json.dumps({'title': 'test群聊消息记录'}).encode('utf-8')
req3 = urllib.request.Request(url3, data=data3, method='POST', headers={'Content-Type': 'application/json; charset=utf-8', 'Authorization': 'Bearer ' + token})
with urllib.request.urlopen(req3) as r:
doc_result = json.loads(r.read())
print()
print('doc create code:', doc_result.get('code'))
if doc_result.get('code') != 0:
print('failed:', doc_result.get('msg'))
import sys
sys.exit(1)
doc_token = doc_result['data']['document']['document_id']
print('doc_token:', doc_token)
# get root block
url4 = 'https://open.feishu.cn/open-apis/docx/v1/documents/' + doc_token + '/blocks?page_size=50'
req4 = urllib.request.Request(url4, headers={'Authorization': 'Bearer ' + token})
with urllib.request.urlopen(req4) as r:
blocks_result = json.loads(r.read())
root_id = blocks_result['data']['items'][0]['block_id']
def h2(content):
return {'block_type': 4, 'heading2': {'elements': [{'type': 'text_run', 'text_run': {'content': content}}]}}
def bullet(content):
return {'block_type': 12, 'bullet': {'elements': [{'type': 'text_run', 'text_run': {'content': content}}]}}
def text(content):
return {'block_type': 2, 'text': {'elements': [{'type': 'text_run', 'text_run': {'content': content}}]}}
doc_blocks = [h2('群聊消息记录')]
for m in msgs:
type_icon = {'text': '', 'post': '', 'system': '[系统] '}.get(m['type'], '')
line = '%(time)s | %(type_icon)s%(content)s' % {'time': m['time'], 'type_icon': type_icon, 'content': m['content']}
doc_blocks.append(bullet(line))
url5 = 'https://open.feishu.cn/open-apis/docx/v1/documents/' + doc_token + '/blocks/' + root_id + '/children'
data5 = json.dumps({'children': doc_blocks}).encode('utf-8')
req5 = urllib.request.Request(url5, data=data5, method='POST', headers={'Content-Type': 'application/json; charset=utf-8', 'Authorization': 'Bearer ' + token})
with urllib.request.urlopen(req5) as r:
write_result = json.loads(r.read())
print('write code:', write_result.get('code'))
if write_result.get('code') == 0:
print('doc_url:', 'https://feishu.cn/docx/' + doc_token)
else:
print('write failed:', write_result.get('msg'))
FILE:scripts/fix_xlsx_encoding.py
import zipfile, shutil, os, re, tempfile
desktop = os.path.join(os.environ['USERPROFILE'], 'Desktop')
target_size = 15914
matching = [(f, os.path.getsize(os.path.join(desktop, f))) for f in os.listdir(desktop)
if f.endswith('.xlsx') and os.path.getsize(os.path.join(desktop, f)) == target_size]
filename, _ = matching[0]
filepath = os.path.join(desktop, filename)
print('Fixing:', filepath)
# Read all files from the xlsx
with zipfile.ZipFile(filepath, 'r') as z:
names = z.namelist()
files = {}
for name in names:
with z.open(name) as f:
files[name] = f.read()
# Fix sharedStrings.xml: the bytes are GBK but XML declares UTF-8
# Re-decode as GBK and re-encode as UTF-8
shared = files['xl/sharedStrings.xml']
# Decode the raw bytes as GBK, then encode as UTF-8
content_str = shared.decode('gbk', errors='replace')
# Remove the encoding declaration and save as UTF-8
content_str = content_str.replace('encoding="UTF-8"', 'encoding="UTF-8"')
# Re-encode only the content (t tags) as proper UTF-8
def fix_t(content):
# For each <t> element, get the text, decode from GBK bytes (the bytes we have are GBK),
# then the string is already decoded - but we need to re-encode properly
def replacer(m):
return m.group(0)
return content
# Actually, let's take a different approach: rebuild the strings correctly
# The sharedStrings.xml stores raw UTF-8 bytes but labeled as such - the issue is the underlying storage
# Let's just re-save the strings by reading them correctly (as GBK) and writing as UTF-8
new_shared = shared.decode('gbk', errors='replace').encode('utf-8', errors='replace')
files['xl/sharedStrings.xml'] = new_shared
# Also fix worksheet and other XMLs that may have inline strings
for name in ['xl/worksheets/sheet1.xml', 'xl/workbook.xml']:
if name in files:
content = files[name]
try:
s = content.decode('gbk', errors='replace')
files[name] = s.encode('utf-8', errors='replace')
except:
pass
# Save fixed xlsx
fixed_path = os.path.join(desktop, 'fixed_' + filename)
with zipfile.ZipFile(fixed_path, 'w', zipfile.ZIP_DEFLATED) as zout:
for name in names:
zout.writestr(name, files[name])
print('Fixed file saved to:', fixed_path)
print('Original size:', os.path.getsize(filepath))
print('Fixed size:', os.path.getsize(fixed_path))
FILE:scripts/rebuild_xlsx.py
import openpyxl
from openpyxl.styles import Font, Alignment, PatternFill
import datetime, os, json, lark_oapi as lark
from lark_oapi.api.im.v1 import *
# Read original file
filepath = 'C:/Users/10430/Desktop/采购数据.xlsx'
wb_src = openpyxl.load_workbook(filepath)
ws_src = wb_src.active
rows = list(ws_src.iter_rows(values_only=True))
print(f'Read {len(rows)} rows from original file')
# Create new clean xlsx
wb_new = openpyxl.Workbook()
ws_new = wb_new.active
ws_new.title = '采购数据'
# Styles
header_font = Font(bold=True)
header_fill = PatternFill(start_color='D9E1F2', end_color='D9E1F2', fill_type='solid')
center = Alignment(horizontal='center', vertical='center')
# Write header
headers = ['采购时间', '部门', '项目', '采购人', '花费']
for col, h in enumerate(headers, 1):
cell = ws_new.cell(row=1, column=col, value=h)
cell.font = header_font
cell.fill = header_fill
cell.alignment = center
# Write data rows
for r_idx, row in enumerate(rows[1:], 2): # skip header row
for c_idx, val in enumerate(row, 1):
if isinstance(val, datetime.datetime):
ws_new.cell(row=r_idx, column=c_idx, value=val).number_format = 'YYYY-MM-DD'
else:
ws_new.cell(row=r_idx, column=c_idx, value=val)
# Auto-adjust column widths
for col in ws_new.columns:
max_len = 0
col_letter = col[0].column_letter
for cell in col:
if cell.value:
max_len = max(max_len, len(str(cell.value)))
ws_new.column_dimensions[col_letter].width = min(max_len + 4, 30)
# Save locally
out_path = 'C:/Users/10430/Desktop/采购数据_干净版.xlsx'
wb_new.save(out_path)
print(f'Saved to: {out_path}')
# Upload via lark-oapi SDK
config = json.load(open('C:/Users/10430/.openclaw/workspace/skills/feishu-ops/scripts/config.json', encoding='utf-8'))
client = lark.Client.builder() \
.app_id(config['app_id']) \
.app_secret(config['app_secret']) \
.log_level(lark.LogLevel.ERROR) \
.build()
with open(out_path, 'rb') as f:
file_data = f.read()
upload_resp = client.im.v1.file.create(
CreateFileRequest.builder()
.request_body(CreateFileRequestBody.builder()
.file_type('xlsx')
.file_name('采购数据_干净版.xlsx')
.file(file_data)
.build())
.build()
)
print('Upload code:', upload_resp.code)
if upload_resp.code != 0:
print('Upload failed:', upload_resp.msg)
import sys
sys.exit(1)
file_key = upload_resp.data.file_key
print('file_key:', file_key)
# Send to chat
msg_resp = client.im.v1.message.create(
CreateMessageRequest.builder()
.receive_id_type('chat_id')
.request_body(CreateMessageRequestBody.builder()
.receive_id('oc_2c6df8f6e06e88d34729baacc124b89e')
.msg_type('file')
.content(json.dumps({'file_key': file_key}))
.build())
.build()
)
print('Send code:', msg_resp.code, 'msg:', msg_resp.msg)
print('Done!')
FILE:scripts/send_news.py
import json, urllib.request, uuid
config = json.load(open('C:/Users/10430/.openclaw/workspace/skills/feishu-ops/scripts/config.json', encoding='utf-8'))
url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal'
data = json.dumps({'app_id': config['app_id'], 'app_secret': config['app_secret']}).encode('utf-8')
req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'})
with urllib.request.urlopen(req) as r:
token = json.loads(r.read())['tenant_access_token']
news = """以下是今日国际热点新闻 Top 10(2026年3月19日):
1. 中美巴黎经贸磋商释放积极信号,美方希望双边关系稳定发展
2. 以军称伊朗使用集束弹头导弹再次发动袭击,中东局势持续升级
3. 美情报总监称伊朗政权"完整但遭严重削弱"
4. 美军多处基地遭袭击,"被炸了个遍"
5. 世卫组织正为中东地区可能发生的"核灾难"做准备
6. 日本央行维持政策利率在0.75%左右
7. 卡塔尔多处能源设施再遭袭起火,阿联酋谴责
8. 一艘船在阿联酋海域遭袭起火,波斯湾局势紧张
9. 美联储按兵不动,美元大涨,人民币对美元汇率跌破6.9
10. 高市启"跪美"外交,引发国际关注
—— 由AI龙虾自动整理推送"""
url2 = 'https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id'
payload = {
'receive_id': 'oc_2c6df8f6e06e88d34729baacc124b89e',
'msg_type': 'text',
'content': json.dumps({'text': news}),
'uuid': str(uuid.uuid4())
}
data2 = json.dumps(payload).encode('utf-8')
req2 = urllib.request.Request(url2, data=data2, method='POST', headers={'Content-Type': 'application/json; charset=utf-8', 'Authorization': 'Bearer ' + token})
with urllib.request.urlopen(req2) as r:
result = json.loads(r.read())
print('code:', result.get('code'), '| msg:', result.get('msg'))
FILE:scripts/test_blocks.py
import json, urllib.request, urllib.error
config = json.load(open('C:/Users/10430/.openclaw/workspace/skills/feishu-ops/scripts/config.json'))
token = 't-g1043jexJNXU6IOGYC26VWSWESLRGIMMOWRXJWYS'
doc_token = 'KH6qdDlNuoACh4xSaPTcFFr3npc'
tests = [
(3, 'heading1'),
(4, 'heading2'),
(5, 'heading3'),
(6, 'bullet'),
(7, 'ordered'),
]
for bt, key in tests:
blocks = [{'block_type': bt, key: {'elements': [{'type': 'text_run', 'text_run': {'content': 'Test content'}}], 'style': {}}}]
url = 'https://open.feishu.cn/open-apis/docx/v1/documents/' + doc_token + '/blocks/' + doc_token + '/children'
data = json.dumps({'children': blocks}).encode('utf-8')
req = urllib.request.Request(url, data=data, method='POST', headers={'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token})
try:
with urllib.request.urlopen(req) as r:
result = json.loads(r.read())
code = result.get('code')
print('block_type', bt, '(' + key + '): OK code=' + str(code))
except urllib.error.HTTPError as e:
body = e.read().decode()
print('block_type', bt, '(' + key + '): Error ' + str(e.code) + ' - ' + body[:100])
FILE:scripts/test_blocks2.py
import json, urllib.request, urllib.error
config = json.load(open('C:/Users/10430/.openclaw/workspace/skills/feishu-ops/scripts/config.json'))
token = 't-g1043jexJNXU6IOGYC26VWSWESLRGIMMOWRXJWYS'
doc_token = 'KH6qdDlNuoACh4xSaPTcFFr3npc'
url = 'https://open.feishu.cn/open-apis/docx/v1/documents/' + doc_token + '/blocks/' + doc_token + '/children'
tests = [
({'block_type': 6, 'bullet': {'elements': [{'type': 'text', 'text': {'content': 'Bullet item'}}], 'style': {}}}, 'bullet with text type'),
({'block_type': 9, 'code': {'elements': [{'type': 'text_run', 'text_run': {'content': 'Code line'}}], 'style': {'language': 1}}}, 'code block'),
({'block_type': 10, 'quote': {'elements': [{'type': 'text_run', 'text_run': {'content': 'Quote text'}}], 'style': {}}}, 'quote block'),
({'block_type': 2, 'paragraph': {'elements': [{'type': 'text_run', 'text_run': {'content': 'Para text'}}], 'style': {}}}, 'paragraph'),
]
for block, desc in tests:
data = json.dumps({'children': [block]}).encode('utf-8')
req = urllib.request.Request(url, data=data, method='POST', headers={'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token})
try:
with urllib.request.urlopen(req) as r:
result = json.loads(r.read())
print(desc + ': OK code=' + str(result.get('code')))
except urllib.error.HTTPError as e:
body = e.read().decode()
print(desc + ': Error ' + str(e.code) + ' - ' + body[:120])
FILE:scripts/upload_sdk.py
import lark_oapi as lark
from lark_oapi.api.im.v1 import *
import json, os
config = json.load(open('C:/Users/10430/.openclaw/workspace/skills/feishu-ops/scripts/config.json', encoding='utf-8'))
client = lark.Client.builder() \
.app_id(config['app_id']) \
.app_secret(config['app_secret']) \
.log_level(lark.LogLevel.ERROR) \
.build()
desktop = os.path.join(os.environ['USERPROFILE'], 'Desktop')
target_size = 15914
matching = [(f, os.path.getsize(os.path.join(desktop, f))) for f in os.listdir(desktop)
if f.endswith('.xlsx') and os.path.getsize(os.path.join(desktop, f)) == target_size]
filename, _ = matching[0]
filepath = os.path.join(desktop, filename)
print('File:', filepath)
# Upload file using SDK
with open(filepath, 'rb') as f:
file_data = f.read()
request = CreateFileRequest.builder() \
.request_body(CreateFileRequestBody.builder()
.file_type("xlsx")
.file_name(filename)
.file(file_data)
.build()) \
.build()
response = client.im.v1.file.create(request)
print('code:', response.code, 'msg:', response.msg)
if response.success():
print('file_key:', response.data.file_key)
else:
print('raw:', response.raw.content[:200] if hasattr(response, 'raw') else 'no raw')
FILE:scripts/upload_test.py
import json, os, requests, urllib.parse
config = json.load(open('C:/Users/10430/.openclaw/workspace/skills/feishu-ops/scripts/config.json', encoding='utf-8'))
r = requests.post('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal',
json={'app_id': config['app_id'], 'app_secret': config['app_secret']})
token = r.json()['tenant_access_token']
desktop = os.path.join(os.environ['USERPROFILE'], 'Desktop')
target_size = 15914
matching = [(f, os.path.getsize(os.path.join(desktop, f))) for f in os.listdir(desktop)
if f.endswith('.xlsx') and os.path.getsize(os.path.join(desktop, f)) == target_size]
filename, _ = matching[0]
filepath = os.path.join(desktop, filename)
encoded_name = urllib.parse.quote(filename)
# Use data= instead of files= for file upload (plain binary)
url = f'https://open.feishu.cn/open-apis/im/v1/files?file_name={encoded_name}&file_type=xlsx'
with open(filepath, 'rb') as f:
file_data = f.read()
headers = {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/octet-stream'
}
r2 = requests.post(url, data=file_data, headers=headers)
print('data=bytes:', r2.status_code, r2.json().get('code'), r2.json().get('msg', '')[:80])
# Try multipart with correct format
url3 = f'https://open.feishu.cn/open-apis/im/v1/files?file_name={encoded_name}&file_type=xlsx'
with open(filepath, 'rb') as f:
files = {'file': (filename, f)}
r3 = requests.post(url3, files=files, headers={'Authorization': 'Bearer ' + token})
print('multipart:', r3.status_code, r3.json().get('code'), r3.json().get('msg', '')[:80])
# Try with explicit filename in multipart
url4 = f'https://open.feishu.cn/open-apis/im/v1/files?file_name={encoded_name}&file_type=xlsx'
with open(filepath, 'rb') as f:
files = {'file': (filename, f, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')}
r4 = requests.post(url4, files=files, headers={'Authorization': 'Bearer ' + token})
print('multipart+ct:', r4.status_code, r4.json().get('code'), r4.json().get('msg', '')[:80])
FILE:scripts/write_news.py
import json, urllib.request, urllib.error
config = json.load(open('C:/Users/10430/.openclaw/workspace/skills/feishu-ops/scripts/config.json'))
token = 't-g1043jexJNXU6IOGYC26VWSWESLRGIMMOWRXJWYS'
doc_token = 'KH6qdDlNuoACh4xSaPTcFFr3npc'
# get root block id
url = 'https://open.feishu.cn/open-apis/docx/v1/documents/' + doc_token + '/blocks?page_size=50'
req = urllib.request.Request(url, headers={'Authorization': 'Bearer ' + token})
with urllib.request.urlopen(req) as r:
result = json.loads(r.read())
root_id = result['data']['items'][0]['block_id']
print('root block:', root_id)
def h1(content):
return {'block_type': 3, 'heading1': {'elements': [{'type': 'text_run', 'text_run': {'content': content}}]}}
def h2(content):
return {'block_type': 4, 'heading2': {'elements': [{'type': 'text_run', 'text_run': {'content': content}}]}}
def bullet(content):
return {'block_type': 12, 'bullet': {'elements': [{'type': 'text_run', 'text_run': {'content': content}}]}}
def ordered(content):
return {'block_type': 13, 'ordered': {'elements': [{'type': 'text_run', 'text_run': {'content': content}}]}}
def text(content):
return {'block_type': 2, 'text': {'elements': [{'type': 'text_run', 'text_run': {'content': content}}]}}
news = [
h1('今日热点新闻汇总'),
h2('国内要闻'),
bullet('一批重大工程紧锣密鼓,奏响"春日奋进曲"'),
bullet('王毅表示:这场战争本不该发生,更没必要再打下去'),
bullet('"投资者保护"首次写入十五五,证监会火速部署清除"拦路虎"'),
bullet('商办最低首付调至3成,北上广杭蓉锁定一线城市'),
bullet('多地宣布27年缩减中考计分科目'),
h2('国际热点'),
bullet('伊朗外长怒斥以色列"暗杀常态化",称国际社会不应"双标"'),
bullet('美媒:世卫组织官员正为中东地区可能发生的"核灾难"做准备'),
bullet('牛弹琴:重大转折点,战争进入疯狂阶段'),
bullet('中国代表:中东局势正被推向危险深渊'),
h2('科技财经'),
bullet('Agent推高需求,全球云计算巨头集体涨价'),
bullet('腾讯2025年营收增长14%,今年AI新产品投入至少翻倍'),
bullet('苹果CEO库克现身成都,锚定中国供应链'),
bullet('金价大跌,市民排队买金(现货黄金跌破4900美元关口)'),
h2('社会民生'),
bullet('前中超球员邱忠辉:送外卖不丢人,凭劳动挣钱最光荣'),
bullet('央媒:是时候打破眼镜行业的价格"滤镜"了'),
bullet('胖东来169元1克拉方糖戒指再上架,每人限购5枚'),
h2('体育速递'),
bullet('唐钱婷刷新50蛙亚洲纪录(预赛29秒49)'),
bullet('东契奇&库里膝盖出血:地板擦伤留下的真实血迹'),
text('—— 以上新闻整理自新浪网,采集时间:2026年3月19日'),
]
url2 = 'https://open.feishu.cn/open-apis/docx/v1/documents/' + doc_token + '/blocks/' + root_id + '/children'
data = json.dumps({'children': news}).encode('utf-8')
req2 = urllib.request.Request(url2, data=data, method='POST', headers={'Content-Type': 'application/json; charset=utf-8', 'Authorization': 'Bearer ' + token})
try:
with urllib.request.urlopen(req2) as r:
result2 = json.loads(r.read())
code = result2.get('code')
children = result2.get('data', {}).get('children', [])
print('code:', code, '| children count:', len(children))
if code != 0:
print('msg:', result2.get('msg'))
except urllib.error.HTTPError as e:
body = e.read().decode()
print('HTTP Error:', e.code)
print('Body:', body[:500])
FILE:references/api_ref.md
# 飞书 API 参考
## 认证
```
POST https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal
Body: {"app_id": "...", "app_secret": "..."}
返回: {"code": 0, "tenant_access_token": "..."}
```
## 文档 API
### 创建文档
```
POST https://open.feishu.cn/open-apis/docx/v1/documents
Headers: Authorization: Bearer <token>
Body: {"title": "文档标题"}
返回: {"code": 0, "data": {"document": {"document_id": "xxx"}}}
```
### 读取文档 blocks
```
GET https://open.feishu.cn/open-apis/docx/v1/documents/<doc_token>/blocks?page_size=500
返回: {"code": 0, "data": {"items": [block, ...]}}
```
### 查询单个 block
```
GET https://open.feishu.cn/open-apis/docx/v1/documents/<doc_token>/blocks/<block_id>
```
### 追加 blocks(到父 block)
```
POST https://open.feishu.cn/open-apis/docx/v1/documents/<doc_token>/blocks/<parent_block_id>/children
Body: {"children": [block, ...], "index": 0}
```
### 更新 block 文本
```
PATCH https://open.feishu.cn/open-apis/docx/v1/documents/<doc_token>/blocks/<block_id>
Body: {"update_text_elements": {"elements": [...]}}
```
### 删除 block
```
DELETE https://open.feishu.cn/open-apis/docx/v1/documents/<doc_token>/blocks/<block_id>
```
## 消息 API
### 发送消息
```
POST https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=<type>
Headers: Authorization: Bearer <token>
Body: {
"receive_id": "<id>",
"msg_type": "text",
"content": "{\"text\": \"消息内容\"}",
"uuid": "<uuid>"
}
```
- `receive_id_type`: `open_id`(用户)或 `chat_id`(群)
### 搜索用户
```
GET https://open.feishu.cn/open-apis/contact/v3/users/search?query=<关键词>&page_size=20
```
### 搜索群聊
```
GET https://open.feishu.cn/open-apis/im/v1/chats?search_key=<关键词>&page_size=20
```
## Block 类型参考
| block_type | 类型 | 键名 |
|-----------|------|------|
| 1 | Page | `page` |
| 2 | Text | `text` |
| 3 | Heading 1 | `heading1` |
| 4 | Heading 2 | `heading2` |
| 5 | Heading 3 | `heading3` |
| 6-11 | Heading 4-9 | `heading4`...`heading9` |
| 12 | Bullet List | `bullet` |
| 13 | Ordered List | `ordered` |
| 14 | Code | `code` |
| 15 | Quote | `quote` |
| 17 | Todo | `todo` |
| 31 | Table | `table` |
| 32 | Table Cell | `table_cell` |
## 表格 block 内部结构
表格 block (type=11) 包含 `table_rows`,每个 row 包含 `cells`。
查询表格内容需要:
1. `GET /blocks` 获取文档 blocks
2. 找到 `block_type == 11` 的表格 block
3. `GET /blocks/<table_block_id>` 获取表格详情(含 rows/cells)
4. 每个 cell 是独立的 block,可用 `update_text_elements` 更新
## 错误码
| code | 含义 |
|------|------|
| 0 | 成功 |
| 99991661 | 无权限 |
| 230001 | 文档不存在 |
| 230002 | block 不存在 |