一键导入
feishu-card
飞书交互卡片(schema 2.0):构建和发送卡片消息,含 @提及、流式思考态、原地更新、按钮/选择器交互。通过 lark-cli --as bot 发送,无需手动管理 token。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
飞书交互卡片(schema 2.0):构建和发送卡片消息,含 @提及、流式思考态、原地更新、按钮/选择器交互。通过 lark-cli --as bot 发送,无需手动管理 token。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
飞书Bot文件操作安全规范:白名单写入 ~/feishu_bot/cache/,禁止操作敏感路径和脚本文件,所有写入必须 flock+jq 原子替换。
飞书 lark-cli 操作完整指南:消息/文档/表格/群组/用户/日历/任务,内嵌所有关键规则和典型流程。
飞书Bot缓存管理:读写 ~/feishu_bot/cache/ 下的 tokens/groups/users JSON,含TTL校验、flock并发写入、name_cache同步、旧cache.json迁移。
| name | feishu-card |
| version | 3.0.0 |
| description | 飞书交互卡片(schema 2.0):构建和发送卡片消息,含 @提及、流式思考态、原地更新、按钮/选择器交互。通过 lark-cli --as bot 发送,无需手动管理 token。 |
| metadata | {"requires":{"bins":["lark-cli","jq","python3"]}} |
body.elements[],不是顶层 elements[]--as bot,--msg-type interactive,--content 传 JSON 字符串<at id="ou_xxx">姓名</at>(不是 <at user_id>)action 容器,button 直接放进 body.elements,交互用 behaviors 数组message_id,Claude 回答后 PATCH 原地更新,避免刷屏note tag 在 schema 2.0 中已不支持(报错 200861),备注改用 div + lark_md(<font color=grey>备注文字</font>)$(python3 -c "...") ——内容含反引号或特殊字符时 bash 会报语法错误{
"schema": "2.0",
"config": {
"streaming_mode": false,
"enable_forward": true,
"update_multi": true,
"width_mode": "default"
},
"card_link": {
"url": "https://...",
"pc_url": "https://...",
"ios_url": "https://...",
"android_url": "https://..."
},
"header": { "...": "..." },
"body": {
"direction": "vertical",
"padding": "0px",
"elements": []
}
}
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
streaming_mode | bool | false | 流式更新态(思考中占位用) |
enable_forward | bool | true | 允许转发卡片 |
update_multi | bool | false | JSON 2.0 共享卡片模式(多人同步更新需设 true) |
width_mode | string | default(600px) | compact(400px)/ fill(全宽响应)/ default |
{
"header": {
"title": {"tag": "plain_text", "content": "标题"},
"subtitle": {"tag": "plain_text", "content": "副标题(可选)"},
"template": "blue",
"icon": {
"tag": "standard_icon",
"token": "robot-outlined",
"color": "blue"
},
"text_tag_list": [
{"tag": "text_tag", "text": {"tag": "plain_text", "content": "New"}, "color": "blue"}
]
}
}
template 颜色:blue / wathet / turquoise / green / yellow / orange / red / carmine / violet / purple / indigo / grey
所有支持颜色枚举的字段(background_color、text_color、icon color 等)均使用以下格式,不能只写基础色名(如 "blue" 单独使用在某些组件会报错),需带层级后缀:
| 色系 | 浅色(背景推荐) | 中色 | 深色(文字推荐) |
|---|---|---|---|
| 蓝 blue | blue-50 blue-100 blue-200 | blue-300~blue-500 | blue-600~blue-900 |
| 水蓝 wathet | wathet-50 wathet-100 wathet-200 | wathet-300~wathet-500 | wathet-600~wathet-900 |
| 青 turquoise | turquoise-50 turquoise-100 | turquoise-300~turquoise-500 | turquoise-600~turquoise-900 |
| 绿 green | green-50 green-100 green-200 | green-300~green-500 | green-600~green-900 |
| 黄绿 lime | lime-50 lime-100 | lime-300~lime-500 | lime-600~lime-900 |
| 黄 yellow | yellow-50 yellow-100 | yellow-300~yellow-500 | yellow-600~yellow-900 |
| 橙 orange | orange-50 orange-100 | orange-300~orange-500 | orange-600~orange-900 |
| 红 red | red-50 red-100 | red-300~red-500 | red-600~red-900 |
| 粉红 carmine | carmine-50 carmine-100 | carmine-300~carmine-500 | carmine-600~carmine-900 |
| 紫 purple | purple-50 purple-100 | purple-300~purple-500 | purple-600~purple-900 |
| 蓝紫 violet | violet-50 violet-100 | violet-300~violet-500 | violet-600~violet-900 |
| 靛 indigo | indigo-50 indigo-100 | indigo-300~indigo-500 | indigo-600~indigo-900 |
| 灰 grey | grey-00 grey-50 grey-100 | grey-300~grey-500 | grey-600~grey-1000 |
| 白 | white bg-white | — | — |
常用浅色背景推荐(collapsible_panel、column 背景等):
blue-100 wathet-100 green-100 yellow-100
orange-100 red-100 purple-100 carmine-100
turquoise-100 violet-100 indigo-100 lime-100
| tag | 类型 | 说明 |
|---|---|---|
div | 内容 | 文本块(plain_text / lark_md),支持前缀 icon |
markdown | 内容 | 纯 Markdown 文本(不支持 @,@用 div+lark_md) |
img | 内容 | 图片 |
table | 内容 | 数据表格(最多 5 个/卡片,不可嵌套) |
hr | 布局 | 分割线 |
note | 布局 | ❌ schema 2.0 已废弃,改用 div + lark_md + <font color=grey> |
column_set | 容器 | 多列布局 |
collapsible_panel | 容器 | 折叠面板 |
button | 交互 | 按钮(JSON 2.0 直接放 elements,不用 action 容器) |
input | 交互 | 文本输入框(须在 form 内使用) |
select_static | 交互 | 静态下拉选择器 |
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": "**加粗** _斜体_ `代码`",
"text_size": "normal",
"text_color": "default",
"text_align": "left",
"lines": 3
},
"icon": {"tag": "standard_icon", "token": "robot-outlined", "color": "blue"}
}
text.tag:plain_text(纯文本)或 lark_md(Markdown + @)
text_size:heading-0 / heading / normal(默认)/ notation / x-small
text_align:left / center / right
{
"tag": "img",
"img_key": "img_xxx",
"alt": {"tag": "plain_text", "content": "悬停说明"},
"title": {"tag": "plain_text", "content": "图片标题"},
"scale_type": "crop_center",
"size": "medium",
"corner_radius": "4px",
"preview": true
}
scale_type:crop_center / crop_top / fit_horizontal
size:stretch(全宽)/ large / medium / small / tiny / "300px 200px"(自定义)
JSON 2.0 不支持
stretch_without_padding,全宽效果改用负 margin
{
"tag": "column_set",
"flex_mode": "bisect",
"horizontal_spacing": "medium",
"background_style": "default",
"columns": [
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [{"tag": "div", "text": {"tag": "plain_text", "content": "左列"}}]
},
{
"tag": "column",
"width": "weighted",
"weight": 1,
"elements": [{"tag": "div", "text": {"tag": "plain_text", "content": "右列"}}]
}
]
}
flex_mode:none(按比例压缩)/ stretch(窄屏纵排)/ flow(自动换行)/ bisect(等宽两列)/ trisect(等宽三列)
horizontal_spacing:small(4px) / medium(8px) / large(12px) / extra_large(16px)
column.width:auto / weighted(配合 weight 1-5)/ "200px"
最大嵌套 5 层,列内不可放 form 容器和 table
{
"tag": "collapsible_panel",
"expanded": false,
"background_color": "default",
"header": {
"title": {"tag": "plain_text", "content": "点击展开"},
"background_color": "grey",
"icon": {"tag": "standard_icon", "token": "arrow-down-outlined"},
"icon_expanded_angle": -180
},
"elements": [
{"tag": "div", "text": {"tag": "plain_text", "content": "折叠内容"}}
]
}
{
"tag": "table",
"page_size": 5,
"row_height": "low",
"freeze_first_column": false,
"header_style": {
"text_align": "left",
"text_size": "normal",
"background_style": "grey",
"bold": true
},
"columns": [
{"name": "name", "display_name": "姓名", "data_type": "text", "width": "auto"},
{"name": "status", "display_name": "状态", "data_type": "options", "width": "120px"},
{"name": "count", "display_name": "数量", "data_type": "number",
"format": {"precision": 0, "separator": true}},
{"name": "ts", "display_name": "时间", "data_type": "date",
"format": "YYYY/MM/DD HH:mm"}
],
"rows": [
{"name": "张三", "status": [{"name": "进行中", "color": "blue"}], "count": 42, "ts": 1700000000000}
]
}
data_type:text / lark_md / markdown / options / number / persons / date
row_height:low / middle / high / auto / "48px"(32-124px)
每张卡片最多 5 个 table,不可嵌套
{
"tag": "button",
"text": {"tag": "plain_text", "content": "确认"},
"type": "primary",
"size": "medium",
"width": "default",
"disabled": false,
"behaviors": [
{
"type": "callback",
"value": {"action": "confirm", "data": "payload"}
}
],
"confirm": {
"title": {"tag": "plain_text", "content": "二次确认"},
"text": {"tag": "plain_text", "content": "确认执行此操作?"}
}
}
type:default(灰)/ primary(蓝)/ danger(红)/ text / primary_text / danger_text / primary_filled / danger_filled / laser
size:tiny / small / medium / large
behaviors[].type:callback(触发事件)/ open_url(跳转链接)
JSON 2.0 变化:不再有
action容器,button 直接放body.elements或column.elements;交互用behaviors替代旧的value
{
"tag": "input",
"name": "field_name",
"placeholder": {"tag": "plain_text", "content": "请输入..."},
"default_value": "",
"input_type": "text",
"max_length": 200,
"required": false,
"disabled": false,
"label": {"tag": "plain_text", "content": "标签"},
"label_position": "top",
"width": "fill"
}
input_type:text / multiline_text / password
用户提交触发 card.action.trigger 事件,事件含 input_value
| 消息类型 | @人语法 | @全体 |
|---|---|---|
卡片 interactive lark_md | <at id="ou_xxx">姓名</at> | <at id="all">所有人</at> |
文本 text / post | <at user_id="ou_xxx">姓名</at> | <at user_id="all"></at> |
卡片内 @mention 经验证可用的元素结构(div + lark_md,不要用裸 markdown tag):
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": "<at id=\"ou_xxx\">姓名</at> 消息内容"
}
}
**加粗** *斜体* ~~删除线~~ `行内代码`
<font color=red>红色</font>
<font color=green>绿色</font>
<font color=grey>灰色</font>
<font color=blue>蓝色</font>
<font color=orange>橙色</font>
<font color=wathet>水蓝</font>
<font color=purple>紫色</font>
完整颜色:red / green / grey / blue / orange / wathet / purple / yellow / turquoise / indigo / violet / lime
<text_tag color='red'>标签</text_tag>
<number_tag>99</number_tag>
<local_datetime millisecond='1700000000000' format_type='date_num'></local_datetime>
format_type:date_num(2023/11/15)/ date_full(2023年11月15日)/ datetime(含时分秒)
# H1 ## H2 ### H3 #### H4 ##### H5 ###### H6
- 无序 1. 有序 > 引用
```python
print("hello")
```

| 列1 | 列2 |
|-----|-----|
| A | B |
[文字](https://url) [电话](tel://13800138000)
--- 或 <hr>
<br/> 或 两次回车(段落换行)
:WAITING:、:PROCESSING:)会渲染成纯文本,禁止使用。Fire 不是 FIRE,Trophy 不是 TROPHY,Shrug 不是 SHRUG)。<text_tag> 内部不渲染 :KEY:,须改用 Unicode emoji(如 ✅)。:KEY: 仅在 lark_md 普通文本区域生效。:DONE: :OK: :THUMBSUP: :APPLAUSE:
:PARTY: :FIREWORKS: :MUSCLE: :THANKS:
:Trophy: :Fire: :Hundred: :CheckMark:
:LGTM: :OnIt: :OneSecond: :Get:
:ERROR: :CrossMark: :Pin: :Alarm:
:Loudspeaker: :OKR: :Yes: :No:
:HEADSET: :Typing: :MeMeMe: :Sigh:
:Lemon: :AWESOMEN: :VRHeadset: :YouAreTheBest:
:SALUTE: :HIGHFIVE: :FISTBUMP: :FINGERHEART:
:WAVE: :ThumbsDown:
:SMILE: :LAUGH: :LOL: :LOVE:
:WINK: :PROUD: :WITTY: :JOYFUL:
:WOW: :BLUSH: :HUG: :BeamingFace:
:Delighted: :ThanksFace: :SaluteFace: :PRAISE:
:CLAP: :JIAYI: :FACEPALM: :YEAH:
:CRY: :SOB: :ANGRY: :SHOCKED:
:Shrug: :THINKING: :DIZZY: :SLEEP:
:YAWN: :SICK: :SWEAT: :EMBARRASSED:
:SKULL: :RAINBOWPUKE: :PUKE: :ClownFace:
:ColdSweat: :SMUG: :SILENT: :TERROR:
:PETRIFIED: :BETRAYED: :WRONGED:
:PARTY: :FIREWORKS: :REDPACKET: :FORTUNE:
:FIRECRACKER: :XmasTree: :XmasHat: :Snowman:
:Pumpkin: :Mooncake: :MoonRabbit: :Partying:
:StickyRiceBalls: :JubilantRabbit: :LUCK:
:FullMoonFace: :GoGoGo: :HappyDragon:
:ROSE: :HEART: :BEER: :CAKE:
:GIFT: :BOMB: :Fire: :Trophy:
:Coffee: :BubbleTea: :Drumstick: :Pepper:
:CUCUMBER: :CANDIEDHAWS: :Soccer: :Basketball:
:Music: :CLEAVER: :EatingFood: :LIPS:
:GeneralDoNotDisturb: :GeneralInMeetingBusy:
:StatusReading: :GeneralWorkFromHome:
:GeneralBusinessTrip: :StatusInFlight:
:GeneralSun: :GeneralMoonRest:
:StatusFlashOfInspiration: :Status_PrivateMessage:
:GeneralTravellingCar: :StatusBus:
🎉 ✅ ❌ ⚠️ 🚀 📦 💬 ⏳ 🔔 📝 🛠️ 🔍 👋 🎯 💡 🌟
👥 📅 🤖 🏆 🔥 💯 📣 🎁 ☕ 🍺
推荐风格:标题用 Unicode emoji 增强视觉,状态/反馈用飞书
:DONE::LGTM:等专属语法
CARD=$(python3 -c "
import json
card = {
'schema': '2.0',
'header': {'title': {'tag': 'plain_text', 'content': '标题'}, 'template': 'blue'},
'body': {'elements': [{'tag': 'markdown', 'content': '内容'}]}
}
print(json.dumps(card))
")
lark-cli im +messages-send \
--chat-id oc_xxx \
--msg-type interactive \
--content "$CARD" \
--as bot
MSG_ID=$(lark-cli im +messages-reply \
--message-id om_xxx \
--msg-type interactive \
--content "$CARD" \
--as bot \
--jq '.data.message_id')
lark-cli im +messages-reply \
--message-id om_xxx \
--msg-type interactive \
--content "$CARD" \
--reply-in-thread \
--as bot
lark-cli im +messages-send \
--chat-id oc_xxx \
--msg-type interactive \
--content "$CARD" \
--idempotency-key "deploy-654.0-notify" \
--as bot
PAYLOAD=$(jq -n --argjson card "$CARD" '{"msg_type":"interactive","content":($card|tojson)}')
lark-cli api PATCH "/open-apis/im/v1/messages/${MSG_ID}" \
--data "$PAYLOAD" \
--as bot
make_thinking_card() {
local name="$1" question="$2"
jq -n --arg name "$name" --arg q "$question" '{
schema:"2.0",
header:{title:{tag:"plain_text",content:("💬 "+$name+" 问")},template:"grey"},
config:{streaming_mode:true},
body:{elements:[
{tag:"markdown",content:("> "+$q)},
{tag:"hr"},
{tag:"markdown",content:"⏳ **思考中…**"}
]}
}'
}
make_reply_card() {
local name="$1" question="$2" answer="$3" duration="$4"
jq -n --arg name "$name" --arg q "$question" --arg ans "$answer" --arg dur "$duration" '{
schema:"2.0",
header:{title:{tag:"plain_text",content:("💬 "+$name+" 问")},template:"blue"},
config:{streaming_mode:false},
body:{elements:[
{tag:"markdown",content:("> "+$q)},
{tag:"hr"},
{tag:"markdown",content:$ans},
{tag:"hr"},
{tag:"markdown",content:("✅ 耗时 **"+$dur+"**")}
]}
}'
}
make_error_card() {
local msg="${1:-抱歉,我暂时无法回答,请稍后再试。}" hint="${2:-}"
local content="$msg"
[[ -n "$hint" ]] && content+="\\n\\n💡 _${hint}_"
jq -n --arg content "$content" '{
schema:"2.0",
header:{title:{tag:"plain_text",content:"❌ 出错了"},template:"red"},
config:{streaming_mode:false},
body:{elements:[{tag:"markdown",content:$content}]}
}'
}
make_success_card() {
jq -n --arg title "$1" --arg content "$2" '{
schema:"2.0",
header:{title:{tag:"plain_text",content:$title},template:"green"},
config:{streaming_mode:false},
body:{elements:[{tag:"markdown",content:$content}]}
}'
}
make_noperm_card() {
jq -n '{
schema:"2.0",
header:{title:{tag:"plain_text",content:"⛔ 权限不足"},template:"red"},
config:{streaming_mode:false},
body:{elements:[{tag:"markdown",content:"抱歉,你没有使用该 Bot 的权限。"}]}
}'
}
布局:顶部三列信息栏(版本号/环境/状态)+ 版本负责人 + 活动负责人。
图标一律用 Unicode emoji 写在 lark_md content 里,禁止用 icon.token(token 名称不可靠,会静默消失)。
send_deploy_notify() {
local chat_id="$1" version="$2" env="$3"
shift 3
# 剩余参数格式:open_id1 name1 open_id2 name2 ...
local at_owners=""
while [[ $# -ge 2 ]]; do
at_owners+="<at id=\"$1\">$2</at> "
shift 2
done
local now; now=$(date '+%Y-%m-%d %H:%M')
python3 /tmp/_deploy_card.py "$version" "$env" "$now" "$at_owners" > /tmp/_deploy_card.json
lark-cli im +messages-send \
--chat-id "$chat_id" \
--msg-type interactive \
--content "$(cat /tmp/_deploy_card.json)" \
--as bot
}
/tmp/_deploy_card.py(构建脚本,避免 heredoc 内变量展开问题):
import json, sys
version, env, now, at_owners = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
card = {
"schema": "2.0",
"config": {"wide_screen_mode": True},
"header": {
"title": {"tag": "plain_text", "content": f"🚀 {version} · {env} 发版通知"},
"template": "wathet",
"subtitle": {"tag": "plain_text", "content": f"MLA · {env} · {now}"}
},
"body": {"elements": [
{
"tag": "column_set", "flex_mode": "trisect",
"columns": [
{"tag": "column", "elements": [{"tag": "div", "text": {"tag": "lark_md", "content": f"<font color=grey>版本号</font>\n**{version}**"}}]},
{"tag": "column", "elements": [{"tag": "div", "text": {"tag": "lark_md", "content": f"<font color=grey>环境</font>\n<text_tag color='blue'>{env}</text_tag>"}}]},
{"tag": "column", "elements": [{"tag": "div", "text": {"tag": "lark_md", "content": "<font color=grey>状态</font>\n<text_tag color='green'>:DONE: 已发好</text_tag>"}}]},
]
},
{"tag": "hr"},
{"tag": "div", "text": {"tag": "lark_md",
"content": '👥 **版本负责人知悉**\n<at id="ou_f4d8024b035c78431a9bfdfce82a4cfb">王同乐(Atlas)</at> <at id="ou_0bf729df814255c9ba18191d077ecfb1">张超(Scott)</at>\n请知悉~'}},
{"tag": "hr"},
{"tag": "div", "text": {"tag": "lark_md",
"content": f"📅 **活动配置请跟进**\n{at_owners}\n麻烦配置活动,辛苦了!:THUMBSUP:"}},
{"tag": "hr"},
{"tag": "div", "text": {"tag": "lark_md", "content": f"<font color=grey>:DONE: {version} · {env} · 由 lucky后端ai 自动发送</font>"}}
]}
}
print(json.dumps(card, ensure_ascii=False))
jq -n '{
schema:"2.0",
header:{title:{tag:"plain_text",content:"请确认"},template:"orange"},
body:{elements:[
{tag:"markdown",content:"是否执行此操作?"},
{tag:"button",
text:{tag:"plain_text",content:"确认"},
type:"primary",
behaviors:[{type:"callback",value:{action:"confirm"}}]},
{tag:"button",
text:{tag:"plain_text",content:"取消"},
type:"default",
behaviors:[{type:"callback",value:{action:"cancel"}}]}
]}
}'
result=$(lark-cli api PATCH "/open-apis/im/v1/messages/${MSG_ID}" \
--data "$PAYLOAD" --as bot 2>/dev/null)
code=$(echo "$result" | jq -r '.code // 0')
if [[ "$code" != "0" ]]; then
# PATCH 失败(消息过期/权限问题),降级为发新消息
lark-cli im +messages-send --chat-id "$CHAT_ID" --msg-type interactive \
--content "$CARD" --as bot
fi
常见错误码:
| code | 含义 | 处理 |
|---|---|---|
| 230002 | 没有操作权限 | 检查 bot 是否在群内 |
| 10012 | 消息不存在或已过期 | 降级发新消息 |
| 99991663 | token 过期 | 重新 lark-cli auth login --as bot |