
Feishu Lark Agent
- 382 installs
- 50 repo stars
- Updated March 22, 2026
- joeseesun/feishu-lark-agent
Automate Feishu/Lark operations including docs, tables, calendars, and task management through comprehensive API wrapper.
About
Skill providing command wrappers for Feishu Lark open platform APIs. Developers use it to automate document creation, multidimensional tables, calendar events, and task management.
- Covers docs, tables, calendars, tasks, and approvals workflows
- Includes token management and field validation for safe operations
Feishu Lark Agent by the numbers
- 382 all-time installs (skills.sh)
- +3 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #455 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/joeseesun/feishu-lark-agent --skill feishu-lark-agentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 382 |
|---|---|
| repo stars | ★ 50 |
| Last updated | March 22, 2026 |
| Repository | joeseesun/feishu-lark-agent ↗ |
What it does
Automate Feishu/Lark operations including docs, tables, calendars, and task management through comprehensive API wrapper.
Files
Feishu Lark Agent
Run
source ~/.zshrc && python3 ~/.claude/skills/feishu-lark-agent/feishu.py <category> <action> [--key value ...]---
ID 格式速查(关键)
| 前缀 | 类型 | 用途 |
|---|---|---|
ou_ | open_id | 用户 ID,用于 --to |
oc_ | chat_id | 群聊 ID,用于 --chat |
| 邮箱 | 用于 --email,自动解析为 open_id | |
docx/xxx URL 后半段 | document_id | 飞书文档 ID |
base/xxx URL 后半段 | app_token | 多维表格 App Token |
如果只有姓名:先 user search --name "张三" 获取 open_id,再执行目标操作。
---
命令速查
消息 msg
msg send --to <open_id|chat_id> --text "..." # open_id → ou_, chat_id → oc_
msg send --email <email> --text "..." # 用邮箱发
msg send --chat <chat_id> --text "..." # 发群
msg send --to <open_id> --file /path # 发文件内容
msg reply --to <message_id> --text "..."
msg history --chat <chat_id> [--limit 20]
msg search --query "关键词" [--limit 20]
msg chats [--limit 50] # 列出所有群聊用户 user
user search --name "张三" # 模糊搜索,返回 open_id
user get --email <email> # 精确查询
user get --id <open_id>文档 doc
doc create --title "标题" [--content "markdown内容"] [--file /path.md]
doc get --id <document_id> # 从 URL feishu.cn/docx/DOC_ID 取
doc list [--folder <token>] [--limit 50]创建后自动授权 FEISHU_OWNER_OPEN_ID 编辑权限。多维表格 table
table tables --app <token> # 先列出所有 table
table fields --app <token> --table <id> # 必须先查字段!
table records --app <token> --table <id> [--filter 'AND(CurrentValue.[状态]="进行中")'] [--limit 100]
table add --app <token> --table <id> --data '{"字段名":"值"}'
table update --app <token> --table <id> --record <recXXX> --data '{"字段名":"新值"}'
table delete --app <token> --table <id> --record <recXXX>App token 在 URL 中:feishu.cn/base/APP_TOKEN日历 cal
cal calendars # ⚠️ 第一步:列出 Bot 可访问的日历,获取 cal_id
cal list --calendar <cal_id> [--days 7]
cal add --calendar <cal_id> --title "..." --start "YYYY-MM-DD HH:MM" --end "YYYY-MM-DD HH:MM" [--location "..."] [--attendees "a@x.com,b@x.com"]
cal delete --calendar <cal_id> --id <event_id>⚠️ 重要:Bot 无法访问个人日历(primary会报错)。必须先cal calendars获取 Bot 自己的日历 ID,再用该 ID 操作。
任务 task
task list [--completed true] [--limit 50]
task add --title "..." [--due "YYYY-MM-DD"] [--note "..."]
task done --id <task_guid>
task delete --id <task_guid>---
高频场景(直接复制执行)
1. 按姓名发消息
# step1 获取 open_id
python3 feishu.py user search --name "张三"
# step2 发消息
python3 feishu.py msg send --to ou_xxx --text "你好"2. 给群发消息(已知群名 → 先查 chat_id)
python3 feishu.py msg chats # 找到目标群的 chat_id (oc_xxx)
python3 feishu.py msg send --chat oc_xxx --text "通知内容"3. 添加多维表记录(先查字段避免字段名写错)
python3 feishu.py table fields --app APP_TOKEN --table TABLE_ID
python3 feishu.py table add --app APP_TOKEN --table TABLE_ID --data '{"标题":"xxx","状态":"进行中"}'4. 创建文档并写入内容
python3 feishu.py doc create --title "会议记录" --content "# 会议记录\n\n## 议题\n- 内容"5. 查看近期日程(先查可用日历)
python3 feishu.py cal calendars # 获取 Bot 的日历 ID
python3 feishu.py cal list --calendar <cal_id> --days 76. 创建日程(先查日历 ID)
python3 feishu.py cal calendars # 先获取 cal_id
python3 feishu.py cal add --calendar <cal_id> --title "周会" --start "2026-03-20 10:00" --end "2026-03-20 11:00"---
错误速查
| 错误码 | 原因 | 解决 |
|---|---|---|
99991671 | 权限未开通 | 飞书开放平台添加权限 |
230006 | 日历权限缺失 | 开通 calendar:calendar |
1254043 | Bitable 未找到 | 检查 URL 中的 app_token |
191001 | 日历 ID 错误 | 不能用 primary,用真实 cal_id |
Missing FEISHU_APP_ID | 环境变量未加载 | source ~/.zshrc |
__pycache__/
*.pyc
*.pyo
.DS_Store
#!/usr/bin/env python3
"""
feishu-lark-agent - Comprehensive Feishu/Lark API CLI
Based on capabilities from larksuite/openclaw-lark official plugin.
Usage: python3 feishu.py <category> <action> [--key value ...]
Categories:
msg - Messages (send, history, search, chats, reply)
user - Users (get, search)
doc - Documents (create, get, list)
table - Bitable/multi-dimensional tables (records, add, update, delete, tables)
cal - Calendar (calendars, list, add, delete)
task - Tasks (list, add, done, delete)
"""
import os, sys, json, urllib.request, urllib.error, urllib.parse
import time, tempfile, argparse, mimetypes
from pathlib import Path
from datetime import datetime
APP_ID = os.environ.get('FEISHU_APP_ID')
APP_SECRET = os.environ.get('FEISHU_APP_SECRET')
OWNER_OPEN_ID = os.environ.get('FEISHU_OWNER_OPEN_ID')
# Support comma-separated owner IDs for multi-user auto-grant
OWNER_IDS = [x.strip() for x in OWNER_OPEN_ID.split(',') if x.strip()] if OWNER_OPEN_ID else []
BASE = 'https://open.feishu.cn/open-apis'
_cache = Path(tempfile.gettempdir()) / f'.feishu_tok_{(APP_ID or "")[-8:]}.json'
def _http(method, path, data=None, params=None, token=None):
url = BASE + path
if params:
url += '?' + urllib.parse.urlencode({k: v for k, v in params.items() if v is not None})
body = json.dumps(data, ensure_ascii=False).encode() if data is not None else b''
headers = {'Content-Type': 'application/json', 'Content-Length': str(len(body))}
if token:
headers['Authorization'] = f'Bearer {token}'
req = urllib.request.Request(url, data=body or None, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
r = json.loads(resp.read())
if r.get('code', 0) != 0:
sys.exit(f"API error {r.get('code')}: {r.get('msg')}")
return r.get('data', r)
except urllib.error.HTTPError as e:
sys.exit(f"HTTP {e.code}: {e.read().decode()[:500]}")
except Exception as e:
sys.exit(f"Request failed: {e}")
def _token():
if _cache.exists():
try:
c = json.loads(_cache.read_text())
if c.get('expire', 0) > time.time() + 120:
return c['token']
except Exception:
pass
if not APP_ID or not APP_SECRET:
sys.exit('Missing FEISHU_APP_ID or FEISHU_APP_SECRET. Add to ~/.zshrc and reload.')
r = _http('POST', '/auth/v3/tenant_access_token/internal',
{'app_id': APP_ID, 'app_secret': APP_SECRET})
tok = r['tenant_access_token']
_cache.write_text(json.dumps({'token': tok, 'expire': time.time() + r.get('expire', 7200)}))
return tok
def api(method, path, data=None, params=None):
return _http(method, path, data, params, token=_token())
def out(obj):
print(json.dumps(obj, ensure_ascii=False, indent=2))
def die(msg):
print(f'Error: {msg}', file=sys.stderr)
sys.exit(1)
def parse_dt(s):
for fmt in ['%Y-%m-%d %H:%M', '%Y-%m-%d', '%Y/%m/%d %H:%M', '%Y/%m/%d']:
try:
return int(datetime.strptime(s, fmt).timestamp())
except ValueError:
pass
die(f'Cannot parse datetime: {s} (expected: YYYY-MM-DD or YYYY-MM-DD HH:MM)')
def resolve_user(email_or_id):
if not email_or_id:
return None
if email_or_id.startswith('ou_'):
return email_or_id
r = api('POST', '/contact/v3/users/batch_get_id',
{'emails': [email_or_id]}, {'user_id_type': 'open_id'})
users = r.get('user_list', [])
if not users:
die(f'User not found: {email_or_id}')
return users[0].get('user_id') or users[0].get('open_id')
# ==================== MSG ====================
def msg_send(a):
target, id_type = _resolve_target(a)
text = a.text or (open(a.file).read() if a.file else None)
if not text:
die('Specify --text or --file')
r = api('POST', '/im/v1/messages',
{'receive_id': target, 'msg_type': 'text', 'content': json.dumps({'text': text})},
{'receive_id_type': id_type})
out({'ok': True, 'message_id': r.get('message_id')})
def _resolve_target(a):
"""Resolve target (open_id or chat_id) from --to/--email/--chat flags."""
if a.email:
return resolve_user(a.email), 'open_id'
elif a.to:
return a.to, ('chat_id' if a.to.startswith('oc_') else 'open_id')
elif a.chat:
return a.chat, 'chat_id'
else:
die('Specify --to <open_id|chat_id>, --email <email>, or --chat <chat_id>')
def _upload_image(filepath):
"""Upload image to Feishu, return image_key."""
tok = _token()
with open(filepath, 'rb') as f:
data = f.read()
boundary = f'----FeishuBoundary{int(time.time() * 1000)}'
filename = os.path.basename(filepath)
mime = mimetypes.guess_type(filename)[0] or 'image/png'
body = (
f'--{boundary}\r\n'
f'Content-Disposition: form-data; name="image_type"\r\n\r\nmessage\r\n'
f'--{boundary}\r\n'
f'Content-Disposition: form-data; name="image"; filename="{filename}"\r\n'
f'Content-Type: {mime}\r\n\r\n'
).encode() + data + f'\r\n--{boundary}--\r\n'.encode()
req = urllib.request.Request(
BASE + '/im/v1/images', data=body, method='POST',
headers={
'Content-Type': f'multipart/form-data; boundary={boundary}',
'Authorization': f'Bearer {tok}',
})
try:
with urllib.request.urlopen(req, timeout=60) as resp:
r = json.loads(resp.read())
if r.get('code', 0) != 0:
die(f"Image upload error {r.get('code')}: {r.get('msg')}")
return r['data']['image_key']
except urllib.error.HTTPError as e:
die(f"Image upload HTTP {e.code}: {e.read().decode()[:500]}")
def msg_send_image(a):
"""Upload a local image and send it as an image message."""
if not a.image:
die('Specify --image <path-to-image>')
filepath = os.path.expanduser(a.image)
if not os.path.isfile(filepath):
die(f'Image not found: {filepath}')
target, id_type = _resolve_target(a)
image_key = _upload_image(filepath)
r = api('POST', '/im/v1/messages',
{'receive_id': target, 'msg_type': 'image',
'content': json.dumps({'image_key': image_key})},
{'receive_id_type': id_type})
# Optionally send caption text
msg_id = r.get('message_id')
if a.text:
api('POST', '/im/v1/messages',
{'receive_id': target, 'msg_type': 'text',
'content': json.dumps({'text': a.text})},
{'receive_id_type': id_type})
out({'ok': True, 'message_id': msg_id, 'image_key': image_key})
def msg_history(a):
if not a.chat:
die('Specify --chat <chat_id>')
r = api('GET', '/im/v1/messages', params={
'container_id_type': 'chat', 'container_id': a.chat,
'page_size': a.limit or 20})
msgs = []
for m in r.get('items', []):
try:
body = json.loads(m.get('body', {}).get('content', '{}'))
except Exception:
body = m.get('body', {}).get('content', '')
ts_ms = int(m.get('create_time', 0))
msgs.append({
'id': m.get('message_id'),
'sender': m.get('sender', {}).get('id'),
'time': datetime.fromtimestamp(ts_ms / 1000).strftime('%Y-%m-%d %H:%M') if ts_ms else '',
'type': m.get('msg_type'),
'content': body,
})
out({'messages': msgs})
def msg_reply(a):
if not a.to:
die('Specify --to <message_id>')
text = a.text or (open(a.file).read() if a.file else None)
if not text:
die('Specify --text or --file')
r = api('POST', f'/im/v1/messages/{a.to}/reply',
{'msg_type': 'text', 'content': json.dumps({'text': text})})
out({'ok': True, 'message_id': r.get('message_id')})
def msg_search(a):
if not a.query:
die('Specify --query')
r = api('POST', '/im/v1/messages/search', {'query': a.query},
{'page_size': a.limit or 20})
out({'messages': r.get('items', [])})
def msg_chats(a):
r = api('GET', '/im/v1/chats', params={'page_size': a.limit or 50})
chats = [{'id': c.get('chat_id'), 'name': c.get('name'),
'type': c.get('chat_type'), 'members': c.get('member_count')}
for c in r.get('items', [])]
out({'chats': chats})
# ==================== USER ====================
def user_get(a):
if a.email:
r = api('POST', '/contact/v3/users/batch_get_id',
{'emails': [a.email]}, {'user_id_type': 'open_id'})
out({'users': r.get('user_list', [])})
elif a.id:
r = api('GET', f'/contact/v3/users/{a.id}',
params={'user_id_type': 'open_id'})
out({'user': r.get('user')})
else:
die('Specify --email <email> or --id <open_id>')
def user_search(a):
if not a.name:
die('Specify --name')
r = api('GET', '/contact/v3/users/search',
params={'query': a.name, 'page_size': a.limit or 20})
out({'users': r.get('results', [])})
# ==================== DOC ====================
def _md_to_blocks(text):
"""Convert simple Markdown text to Feishu docx block list."""
blocks = []
for line in text.split('\n'):
line = line.rstrip()
if line.startswith('### '):
blocks.append({'block_type': 5, 'heading3': {'style': {}, 'elements': [{'text_run': {'content': line[4:], 'text_element_style': {}}}]}})
elif line.startswith('## '):
blocks.append({'block_type': 4, 'heading2': {'style': {}, 'elements': [{'text_run': {'content': line[3:], 'text_element_style': {}}}]}})
elif line.startswith('# '):
blocks.append({'block_type': 3, 'heading1': {'style': {}, 'elements': [{'text_run': {'content': line[2:], 'text_element_style': {}}}]}})
elif line.startswith('- ') or line.startswith('* '):
blocks.append({'block_type': 12, 'bullet': {'style': {}, 'elements': [{'text_run': {'content': line[2:], 'text_element_style': {}}}]}})
elif line and line[0].isdigit() and '. ' in line[:4]:
blocks.append({'block_type': 13, 'ordered': {'style': {}, 'elements': [{'text_run': {'content': line.split('. ', 1)[1], 'text_element_style': {}}}]}})
elif line:
blocks.append({'block_type': 2, 'text': {'style': {}, 'elements': [{'text_run': {'content': line, 'text_element_style': {}}}]}})
return blocks
def _grant_doc_permission(doc_id, open_id, perm='full_access'):
"""Grant a user edit access to a document."""
try:
api('POST', f'/drive/v1/permissions/{doc_id}/members',
{'member_type': 'openid', 'member_id': open_id, 'perm': perm},
{'type': 'docx', 'need_notification': 'false'})
except SystemExit as e:
print(f"⚠ Permission grant failed for {open_id}: {e}", file=sys.stderr)
def _write_blocks(doc_id, blocks):
"""Write blocks to a document, chunked to avoid API limits."""
chunk = 50
for i in range(0, len(blocks), chunk):
api('POST', f'/docx/v1/documents/{doc_id}/blocks/{doc_id}/children',
{'children': blocks[i:i+chunk], 'index': i})
def doc_create(a):
r = api('POST', '/docx/v1/documents',
{'title': a.title or '新文档', 'folder_token': a.folder or ''})
doc = r.get('document', {})
doc_id = doc.get('document_id')
url = doc.get('document_uri') or f"https://feishu.cn/docx/{doc_id}"
# Write content if provided
content = a.content or (open(a.file).read() if a.file else None)
if content and doc_id:
blocks = _md_to_blocks(content)
if blocks:
_write_blocks(doc_id, blocks)
# Auto-grant edit permission to owners
share_to = getattr(a, 'share_to', None)
if doc_id:
if share_to:
_grant_doc_permission(doc_id, share_to)
for oid in OWNER_IDS:
if oid != share_to:
_grant_doc_permission(doc_id, oid)
out({'ok': True, 'document_id': doc_id, 'url': url})
def doc_get(a):
if not a.id:
die('Specify --id <document_id>')
r = api('GET', f'/docx/v1/documents/{a.id}/raw_content', params={'lang': 0})
out({'content': r.get('content')})
def doc_list(a):
params = {'page_size': a.limit or 50, 'order_by': 'EditedTime', 'direction': 'DESC'}
if a.folder:
params['folder_token'] = a.folder
r = api('GET', '/drive/v1/files', params=params)
out({'files': r.get('files', [])})
# ==================== TABLE (BITABLE) ====================
def table_records(a):
if not a.app or not a.table:
die('Specify --app <app_token> and --table <table_id>')
params = {'page_size': a.limit or 100}
if a.filter:
params['filter'] = a.filter
if a.sort:
params['sort'] = a.sort
r = api('GET', f'/bitable/v1/apps/{a.app}/tables/{a.table}/records', params=params)
out({'records': r.get('items', []), 'total': r.get('total')})
def table_add(a):
if not a.app or not a.table:
die('Specify --app and --table')
raw = a.data or (open(a.file).read() if a.file else None)
if not raw:
die('Specify --data \'{"field":"value"}\' or --file <json-file>')
r = api('POST', f'/bitable/v1/apps/{a.app}/tables/{a.table}/records',
{'fields': json.loads(raw)})
out({'ok': True, 'record_id': r.get('record', {}).get('record_id')})
def table_update(a):
if not a.app or not a.table or not a.record:
die('Specify --app, --table, and --record')
if not a.data:
die('Specify --data \'{"field":"value"}\'')
r = api('PUT', f'/bitable/v1/apps/{a.app}/tables/{a.table}/records/{a.record}',
{'fields': json.loads(a.data)})
out({'ok': True, 'record': r.get('record')})
def table_delete(a):
if not a.app or not a.table or not a.record:
die('Specify --app, --table, and --record')
api('DELETE', f'/bitable/v1/apps/{a.app}/tables/{a.table}/records/{a.record}')
out({'ok': True})
def table_tables(a):
if not a.app:
die('Specify --app <app_token>')
r = api('GET', f'/bitable/v1/apps/{a.app}/tables', params={'page_size': 50})
out({'tables': r.get('items', [])})
def table_fields(a):
if not a.app or not a.table:
die('Specify --app and --table')
r = api('GET', f'/bitable/v1/apps/{a.app}/tables/{a.table}/fields',
params={'page_size': 100})
out({'fields': r.get('items', [])})
# ==================== CALENDAR ====================
def cal_calendars(a):
"""List all calendars the bot can access."""
r = api('GET', '/calendar/v4/calendars', params={'page_size': 50})
cals = [{'id': c.get('calendar_id'), 'summary': c.get('summary'),
'type': c.get('type'), 'role': c.get('role')}
for c in r.get('calendar_list', [])]
out({'calendars': cals})
def cal_list(a):
now = int(time.time())
days = int(a.days or 7)
cal_id = a.calendar or 'primary'
r = api('GET', f'/calendar/v4/calendars/{cal_id}/events', params={
'start_time': str(now), 'end_time': str(now + days * 86400), 'page_size': 50})
events = []
for e in r.get('items', []):
st, et = e.get('start_time', {}), e.get('end_time', {})
def fmt_time(t):
if t.get('date'):
return t['date']
ts = t.get('timestamp')
return datetime.fromtimestamp(int(ts)).strftime('%Y-%m-%d %H:%M') if ts else ''
events.append({'id': e.get('event_id'), 'title': e.get('summary'),
'start': fmt_time(st), 'end': fmt_time(et),
'location': e.get('location', {}).get('name'),
'description': e.get('description')})
out({'events': events})
def cal_add(a):
if not a.title or not a.start or not a.end:
die('Specify --title, --start "YYYY-MM-DD HH:MM", --end "YYYY-MM-DD HH:MM"')
cal_id = a.calendar or 'primary'
data = {
'summary': a.title,
'start_time': {'timestamp': str(parse_dt(a.start)), 'timezone': 'Asia/Shanghai'},
'end_time': {'timestamp': str(parse_dt(a.end)), 'timezone': 'Asia/Shanghai'},
}
if a.desc:
data['description'] = a.desc
if a.location:
data['location'] = {'name': a.location}
r = api('POST', f'/calendar/v4/calendars/{cal_id}/events', data)
event_id = r.get('event', {}).get('event_id')
if event_id:
attendees = []
# Auto-add owners as attendees
for oid in OWNER_IDS:
attendees.append({'type': 'user', 'user_id': oid})
# Add extra attendees from --attendees flag
if a.attendees:
emails = [e.strip() for e in a.attendees.split(',')]
attendees += [{'type': 'third_party', 'third_party_email': e} for e in emails]
if attendees:
try:
api('POST', f'/calendar/v4/calendars/{cal_id}/events/{event_id}/attendees',
{'attendees': attendees}, {'user_id_type': 'open_id'})
except SystemExit:
pass # Non-fatal
out({'ok': True, 'event_id': event_id})
def cal_delete(a):
if not a.id:
die('Specify --id <event_id>')
api('DELETE', f'/calendar/v4/calendars/{a.calendar or "primary"}/events/{a.id}')
out({'ok': True})
# ==================== TASK ====================
def task_list(a):
params = {'page_size': a.limit or 50, 'user_id_type': 'open_id'}
if a.completed:
params['completed'] = 'true'
r = api('GET', '/task/v2/tasks', params=params)
tasks = []
for t in r.get('items', []):
due = t.get('due', {})
ts = due.get('timestamp')
tasks.append({
'id': t.get('guid'),
'title': t.get('summary'),
'due': due.get('date') or (datetime.fromtimestamp(int(ts)).strftime('%Y-%m-%d') if ts else None),
'done': bool(t.get('completed_at')),
})
out({'tasks': tasks})
def task_add(a):
if not a.title:
die('Specify --title')
data = {'summary': a.title}
if a.due:
ts = parse_dt(a.due)
# Task API uses milliseconds (unlike Calendar which uses seconds)
data['due'] = {'timestamp': str(ts * 1000), 'is_all_day': ':' not in a.due}
if a.note:
data['description'] = a.note
# Auto-assign to all owners if FEISHU_OWNER_OPEN_ID is set
if OWNER_IDS:
data['members'] = [{'id': oid, 'type': 'user', 'role': 'assignee'} for oid in OWNER_IDS]
r = api('POST', '/task/v2/tasks', data, {'user_id_type': 'open_id'})
out({'ok': True, 'task_id': r.get('task', {}).get('guid')})
def task_done(a):
if not a.id:
die('Specify --id <task_guid>')
api('POST', f'/task/v2/tasks/{a.id}/complete')
out({'ok': True})
def task_delete(a):
if not a.id:
die('Specify --id')
api('DELETE', f'/task/v2/tasks/{a.id}')
out({'ok': True})
# ==================== DISPATCH ====================
COMMANDS = {
'msg send': msg_send,
'msg send_image': msg_send_image,
'msg history': msg_history,
'msg reply': msg_reply,
'msg search': msg_search,
'msg chats': msg_chats,
'user get': user_get,
'user search': user_search,
'doc create': doc_create,
'doc get': doc_get,
'doc list': doc_list,
'table records': table_records,
'table add': table_add,
'table update': table_update,
'table delete': table_delete,
'table tables': table_tables,
'table fields': table_fields,
'cal calendars': cal_calendars,
'cal list': cal_list,
'cal add': cal_add,
'cal delete': cal_delete,
'task list': task_list,
'task add': task_add,
'task done': task_done,
'task delete': task_delete,
}
ATTRS = ['to', 'email', 'text', 'file', 'chat', 'id', 'query', 'name', 'title',
'content', 'folder', 'app', 'table', 'record', 'data', 'filter', 'sort', 'revision_id',
'limit', 'days', 'start', 'end', 'desc', 'location', 'calendar', 'note',
'due', 'completed', 'tasklist', 'attendees', 'share_to', 'image']
def main():
args = sys.argv[1:]
if len(args) < 2:
print('Usage: feishu.py <category> <action> [--key value ...]\n')
print('Commands:')
for k in COMMANDS:
print(f' {k}')
sys.exit(0)
key = f'{args[0]} {args[1]}'
if key not in COMMANDS:
die(f'Unknown command: {key}\nAvailable: {", ".join(COMMANDS)}')
ns = argparse.Namespace(**{a: None for a in ATTRS})
rest = args[2:]
i = 0
while i < len(rest):
if rest[i].startswith('--'):
k = rest[i][2:].replace('-', '_')
if i + 1 < len(rest) and not rest[i + 1].startswith('--'):
setattr(ns, k, rest[i + 1])
i += 2
else:
setattr(ns, k, True)
i += 1
else:
i += 1
COMMANDS[key](ns)
if __name__ == '__main__':
main()
MIT License
Copyright (c) 2026 joeseesun
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
feishu-lark-agent
---
中文文档
让 Claude Code 直接操控飞书——发消息、建文档、管日程、写多维表格,全部用自然语言完成。
零依赖 Python 脚本,调用飞书开放 API,配合 Claude Code 使用效果最佳。
安装
npx skills add joeseesun/feishu-lark-agent---
自然语言对话示例
安装并配置完成后,直接和 Claude Code 说话就好,不需要记任何命令。
---
💬 发消息
「发飞书消息给 zhang@company.com,说今天的站会改到11点」
Claude 自动通过邮箱查到 open_id,发送消息,返回确认。
---
「给乔木龙虾俱乐部群发一条消息:周五下午茶,大家来玩」
Claude 先列出群聊找到 chat_id,再发送到群组。
---
📄 文档
「帮我创建一个飞书文档,标题「2026 Q2 OKR」,内容从这个文件读:/tmp/okr.md」
Claude 创建文档,写入内容,自动将你加为编辑者,返回文档链接。
---
「读一下这个飞书文档的内容:https://xxx.feishu.cn/docx/DOC_ID」
Claude 提取 URL 中的 document_id,调用接口返回全文。
---
📅 日历
「明天下午3点到4点,帮我创建一个「产品评审会」日程,地点3楼大会议室」
Claude 解析时间、创建日程,并自动发送日历邀请给你(需配置 FEISHU_OWNER_OPEN_ID)。
---
「帮我看看本周还有哪些日程」
Claude 查询未来7天的日历事件,整理后展示给你。
---
📊 多维表格
「在我的读书记录表格里加一条:《穷查理宝典》,查理·芒格,状态在读」
(表格链接:https://xxx.feishu.cn/base/APP_TOKEN)
Claude 先查表格字段结构,再按字段名写入记录。
---
「把多维表格里所有状态为「待审核」的记录都列出来」
Claude 用 Feishu 过滤语法查询并展示结果。
---
✅ 任务
「帮我创建一个飞书任务:准备季度述职,截止日期3月25日,备注要准备PPT和数据」
Claude 调用 Task API 创建任务并返回任务 ID。
---
🔍 复合场景
「查一下公司有没有叫王芳的同事,找到了就给她发一条飞书消息,说明天的设计评审希望她参加」
Claude 先用 user search 找人,确认后再发消息,两步自动完成。
---
「帮我把 /tmp/meeting_notes.md 的内容创建成飞书文档,然后发消息给 product-team@company.com 告诉他们文档链接」
Claude 创建文档拿到 URL,再发消息附上链接,一气呵成。
---
第一步:创建飞书自建应用,获取 App ID 和 App Secret
这是唯一需要手动操作的步骤,约 5 分钟完成。
1. 进入飞书开发者后台
打开 https://open.feishu.cn/app,用飞书账号登录。
2. 创建自建应用
点击右上角 「创建企业自建应用」,填写应用名称(如「我的 AI 助手」)和描述。
3. 获取 App ID 和 App Secret
创建完成后,进入应用 → 「凭证与基础信息」,复制:
App ID: cli_xxxxxxxxxxxxxxxxx
App Secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx4. 开通权限
进入 「权限管理」,搜索并开通以下权限(按需开通):
| 权限标识 | 用途 |
|---|---|
im:message | 读取消息 |
im:message:send_as_bot | 发送消息 |
docx:document | 创建/读取文档 |
drive:drive | 文档授权管理 |
bitable:app | 多维表格操作 |
calendar:calendar | 日历读写 |
task:task:write | 任务管理 |
contact:user.id:readonly | 按邮箱查用户 |
5. 发布应用
进入 「版本管理与发布」 → 创建版本 → 申请发布。企业管理员审批后生效(自己是管理员可立即通过)。
---
第二步:获取你的 Open ID(可选但推荐)
Open ID 是飞书中每个用户的唯一标识符(ou_ 开头)。设置后,创建文档/日程时会自动将你加为参与者,无需手动共享。
方法一:通过接口获取(推荐)
先完成第三步配置,再运行:
python3 ~/.claude/skills/feishu-lark-agent/feishu.py user get --email 你的飞书邮箱
# 返回结果中的 user_id 字段即为 Open ID(ou_ 开头)方法二:通过群聊获取
如果你是某个群的群主:
python3 ~/.claude/skills/feishu-lark-agent/feishu.py msg chats
# 找到你创建的群,owner_id 即为你的 Open ID方法三:通过开发者后台获取
在飞书应用后台 → 「事件与回调」 中,向 Bot 发一条消息,查看回调日志里的 sender.sender_id.open_id。
---
第三步:配置环境变量
将以下内容添加到 ~/.zshrc(或 ~/.bashrc):
export FEISHU_APP_ID=cli_你的AppID
export FEISHU_APP_SECRET=你的AppSecret
# 可选:设置后,创建文档/日程时自动将你加为参与者
export FEISHU_OWNER_OPEN_ID=ou_你的OpenID执行生效:
source ~/.zshrc验证配置是否成功:
python3 ~/.claude/skills/feishu-lark-agent/feishu.py msg chats
# 看到群列表说明配置成功 ✓---
与 Claude Code 配合使用
安装并配置完成后,直接用自然语言对 Claude Code 说:
发飞书消息给 zhang@company.com,说「今天的会议改到下午3点」创建一个飞书文档,标题「Q2 OKR 规划」明天上午10点到11点,创建日程「产品评审会」,地点「3楼大会议室」在多维表格里查看状态为「进行中」的记录Claude Code 会自动识别并调用对应的飞书操作。
---
常用场景演示
场景一:发飞书消息
# 通过邮箱发消息
python3 feishu.py msg send --email colleague@company.com --text "周报已提交,请查收"
# 发消息到群组(chat_id 通过 msg chats 获取)
python3 feishu.py msg send --chat oc_xxxxxxxx --text "今日站会10分钟后开始"
# 发长文本(从文件读取)
python3 feishu.py msg send --email boss@company.com --file /tmp/report.txt
# 查看群聊列表
python3 feishu.py msg chats场景二:创建飞书文档
# 创建空文档(若配置了 FEISHU_OWNER_OPEN_ID,自动授权你编辑)
python3 feishu.py doc create --title "2026 Q2 规划"
# 创建文档并写入 Markdown 内容
python3 feishu.py doc create --title "会议记录" --file /tmp/meeting.md
# 查看文档内容(document_id 在 URL 中)
python3 feishu.py doc get --id DOC_ID场景三:管理日历
# 查看最近7天的日程
python3 feishu.py cal list --calendar YOUR_CALENDAR_ID
# 创建日程(若配置了 FEISHU_OWNER_OPEN_ID,自动发送日历邀请给你)
python3 feishu.py cal add \
--title "周例会" \
--start "2026-03-16 10:00" \
--end "2026-03-16 11:00" \
--location "3楼会议室" \
--calendar YOUR_CALENDAR_ID
# 删除日程
python3 feishu.py cal delete --id EVENT_ID --calendar YOUR_CALENDAR_ID如何获取 Calendar ID:Bot 使用租户令牌,运行以下命令获取 Bot 的日历 ID:
```bash
python3 -c "
import sys, os, json; sys.path.insert(0, os.path.expanduser('~/.claude/skills/feishu-lark-agent'))
import feishu
r = feishu.api('GET', '/calendar/v4/calendars', params={'page_size': 50})
print(json.dumps(r, ensure_ascii=False, indent=2))
"
```
场景四:多维表格(Bitable)
# App Token 在 URL 中:feishu.cn/base/APP_TOKEN
# 先查看字段结构,再操作
python3 feishu.py table fields --app bASc1234xxx --table tblXXXX
# 查询记录
python3 feishu.py table records --app bASc... --table tbl...
# 按条件过滤(支持飞书筛选语法)
python3 feishu.py table records --app bASc... --table tbl... \
--filter 'AND(CurrentValue.[状态]="进行中")'
# 新增记录
python3 feishu.py table add --app bASc... --table tbl... \
--data '{"书名":"深度工作","作者":"Cal Newport","状态":"在读"}'
# 更新记录
python3 feishu.py table update --app bASc... --table tbl... \
--record recXXXX --data '{"状态":"已读"}'场景五:任务管理
# 查看当前任务
python3 feishu.py task list
# 创建任务并设置截止日期
python3 feishu.py task add --title "完成Q2复盘报告" --due "2026-03-20"
# 带备注的任务
python3 feishu.py task add \
--title "准备产品演示" \
--due "2026-03-18 09:00" \
--note "需要准备30页PPT和数据看板"
# 完成任务
python3 feishu.py task done --id TASK_GUID---
完整命令速查
| 分类 | 命令 | 说明 |
|---|---|---|
| 消息 | msg send | 发送消息 |
msg history | 获取群聊记录 | |
msg reply | 回复消息 | |
msg search | 搜索消息 | |
msg chats | 列出所有群聊 | |
| 用户 | user get | 按邮箱/ID 查用户 |
user search | 按姓名搜索用户 | |
| 文档 | doc create | 创建文档 |
doc get | 读取文档内容 | |
doc list | 列出文档 | |
| 多维表格 | table records | 查询记录 |
table add | 新增记录 | |
table update | 更新记录 | |
table delete | 删除记录 | |
table tables | 列出所有表 | |
table fields | 查看字段结构 | |
| 日历 | cal list | 查看日程 |
cal add | 创建日程 | |
cal delete | 删除日程 | |
| 任务 | task list | 查看任务 |
task add | 创建任务 | |
task done | 完成任务 | |
task delete | 删除任务 |
---
常见报错
| 错误码 | 原因 | 解决方法 |
|---|---|---|
99991671 | 应用未授权该接口 | 在开发者后台开通对应权限并重新发布 |
230006 | 无日历访问权限 | 开通 calendar:calendar 权限 |
1254043 | 多维表格 App Token 错误 | 检查 URL 中的 App Token |
191001 | 日历 ID 无效 | 使用 Bot 真实日历 ID,不要用 primary |
Missing FEISHU_APP_ID | 环境变量未加载 | 执行 source ~/.zshrc |
---
更新日志
v1.1.0
- 修复
task add截止时间显示为 1970 年的问题(飞书任务 API 使用毫秒时间戳,与日历 API 的秒级时间戳不同) task add现在自动将任务分配给FEISHU_OWNER_OPEN_ID指定的用户,无需手动指定负责人
---
English Docs
Give Claude Code full control over Feishu/Lark — send messages, create docs, manage calendar events, and write to multi-dimensional tables, all through natural language.
Zero-dependency Python CLI using the Feishu Open API. Works best with Claude Code.
Installation
npx skills add joeseesun/feishu-lark-agent---
Natural Language Examples
Once installed and configured, just talk to Claude Code naturally — no commands to memorize.
---
💬 Messaging
"Send a Feishu message to zhang@company.com: standup moved to 11am today"
Claude looks up the open_id by email, sends the message, and confirms.
---
"Post in the team group chat: Friday afternoon tea, everyone's invited"
Claude finds the chat_id from your chat list and sends to the group.
---
📄 Documents
"Create a Feishu doc titled '2026 Q2 OKR', content from /tmp/okr.md"
Claude creates the doc, writes the content, auto-grants you edit access, and returns the link.
---
"Read the content of this Feishu doc: https://xxx.feishu.cn/docx/DOC_ID"
Claude extracts the document_id from the URL and returns the full text.
---
📅 Calendar
"Schedule 'Product Review' tomorrow 3-4pm in Conference Room 3F"
Claude parses the time, creates the event, and auto-invites you (requires FEISHU_OWNER_OPEN_ID).
---
"What's on my calendar this week?"
Claude queries the next 7 days of events and summarizes them.
---
📊 Bitable
"Add a record to my reading list: Poor Charlie's Almanack, Charlie Munger, status: reading"
(Table URL: https://xxx.feishu.cn/base/APP_TOKEN)
Claude checks the field schema first, then writes the record with correct field names.
---
"Show me all records in the Bitable where status is 'Pending Review'"
Claude queries with Feishu filter syntax and displays the results.
---
✅ Tasks
"Create a task: Prepare quarterly review, due March 25, note: need PPT and data dashboard"
Claude calls the Task API, creates the task, and returns the task ID.
---
🔍 Multi-step
"Find a colleague named Wang Fang and send her a message saying I'd like her to join tomorrow's design review"
Claude searches for the user, confirms the match, then sends the message — two steps automatically chained.
---
"Create a Feishu doc from /tmp/meeting_notes.md, then message product-team@company.com with the doc link"
Claude creates the doc, gets the URL, then sends the message with the link attached — all in one go.
---
Step 1: Create a Feishu App and get App ID + App Secret
This is the only manual step. Takes about 5 minutes.
1. Open Feishu Developer Console
Go to https://open.feishu.cn/app and log in with your Feishu account.
2. Create a custom app
Click "Create Enterprise Self-Built App" in the top right. Fill in the app name (e.g. "My AI Assistant") and description.
3. Get App ID and App Secret
After creation, go to "Credentials & Basic Info" and copy:
App ID: cli_xxxxxxxxxxxxxxxxx
App Secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx4. Enable permissions
Go to "Permission Management" and enable the permissions you need:
| Permission | Purpose |
|---|---|
im:message | Read messages |
im:message:send_as_bot | Send messages as bot |
docx:document | Create/read documents |
drive:drive | Manage document permissions |
bitable:app | Bitable operations |
calendar:calendar | Calendar read/write |
task:task:write | Task management |
contact:user.id:readonly | Look up users by email |
5. Publish the app
Go to "Version Management & Release" → Create version → Submit for release. The enterprise admin needs to approve it (if you're the admin, you can approve immediately).
---
Step 2: Get your Open ID (optional but recommended)
Your Open ID (ou_ prefix) is your unique identifier in Feishu. When set, documents and calendar events are automatically shared with you — no manual sharing needed.
Method 1: Via API (recommended)
Complete Step 3 first, then run:
python3 ~/.claude/skills/feishu-lark-agent/feishu.py user get --email your@email.com
# The user_id field in the result is your Open ID (starts with ou_)Method 2: Via group chat
If you own a group chat:
python3 ~/.claude/skills/feishu-lark-agent/feishu.py msg chats
# Find a group you created — owner_id is your Open ID---
Step 3: Set environment variables
Add to ~/.zshrc (or ~/.bashrc):
export FEISHU_APP_ID=cli_your_app_id
export FEISHU_APP_SECRET=your_app_secret
# Optional: auto-add yourself to docs/calendar events
export FEISHU_OWNER_OPEN_ID=ou_your_open_idReload:
source ~/.zshrcVerify setup:
python3 ~/.claude/skills/feishu-lark-agent/feishu.py msg chats
# If you see a list of chats, you're all set ✓---
Using with Claude Code
Once installed, just tell Claude Code in natural language:
Send a Feishu message to zhang@company.com: "Meeting moved to 3pm today"Create a Feishu doc titled "Q2 OKR Planning"Schedule "Product Review" tomorrow at 10am-11am in Conference Room 3FShow me all "In Progress" records in my BitableClaude Code will automatically handle the rest.
---
Common Scenarios
Messaging
# Send to a user by email
python3 feishu.py msg send --email colleague@company.com --text "Report submitted, please review"
# Send to a group chat
python3 feishu.py msg send --chat oc_xxxxxxxx --text "Standup in 10 minutes"
# Send from a file
python3 feishu.py msg send --email boss@company.com --file /tmp/report.txt
# List all chats
python3 feishu.py msg chatsDocuments
# Create a document (auto-grants you edit access if FEISHU_OWNER_OPEN_ID is set)
python3 feishu.py doc create --title "Q2 Planning"
# Create from Markdown file
python3 feishu.py doc create --title "Meeting Notes" --file /tmp/meeting.md
# Read document content
python3 feishu.py doc get --id DOC_ID_FROM_URLCalendar
# List upcoming events
python3 feishu.py cal list --calendar YOUR_CALENDAR_ID
# Create an event (auto-invites you if FEISHU_OWNER_OPEN_ID is set)
python3 feishu.py cal add \
--title "Weekly Sync" \
--start "2026-03-16 10:00" \
--end "2026-03-16 11:00" \
--location "Conference Room 3F" \
--calendar YOUR_CALENDAR_IDHow to get Calendar ID: The bot uses a tenant access token and can only access its own calendar. Run this to find the real calendar ID:
```bash
python3 -c "
import sys, os, json; sys.path.insert(0, os.path.expanduser('~/.claude/skills/feishu-lark-agent'))
import feishu
r = feishu.api('GET', '/calendar/v4/calendars', params={'page_size': 50})
print(json.dumps(r, indent=2))
"
```
Bitable (Multi-dimensional Tables)
# App Token is in the URL: feishu.cn/base/APP_TOKEN
# Check fields first before writing
python3 feishu.py table fields --app bASc1234xxx --table tblXXXX
# Query records with filter
python3 feishu.py table records --app bASc... --table tbl... \
--filter 'AND(CurrentValue.[Status]="In Progress")'
# Add a record
python3 feishu.py table add --app bASc... --table tbl... \
--data '{"Name":"New Project","Status":"Todo","Owner":"John"}'Tasks
# List active tasks
python3 feishu.py task list
# Create a task with due date
python3 feishu.py task add --title "Submit Q2 Report" --due "2026-03-20"
# Mark as done
python3 feishu.py task done --id TASK_GUID---
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
99991671 | App not authorized | Enable the required permission in developer console |
230006 | No calendar access | Enable calendar:calendar permission |
1254043 | Bitable App Token not found | Check the App Token in the URL |
191001 | Invalid calendar ID | Use the bot's real calendar ID, not primary |
Missing FEISHU_APP_ID | Env vars not loaded | Run source ~/.zshrc |
---
Changelog
v1.1.0
- Fixed
task addshowing 1970 as due date (Feishu Task API uses millisecond timestamps, unlike Calendar API which uses seconds) task addnow automatically assigns the task to the user specified inFEISHU_OWNER_OPEN_ID— no need to manually set the assignee
---
📱 关注作者 / Follow the Author
- X (Twitter): @vista8
- 微信公众号「向阳乔木推荐看」:
<p align="center"> <img src="https://github.com/joeseesun/terminal-boost/raw/main/assets/wechat-qr.jpg?raw=true" alt="向阳乔木推荐看公众号二维码" width="300"> </p>
License
MIT