feishu-card-callback-bridge
v0.1.17
Published
Bridges Feishu card button callbacks back into the chat session as visible bot messages
Readme
Feishu Card Callback Bridge
一个独立的 OpenClaw 插件,把飞书卡片按钮的点击回调、表单提交回调以机器人消息的形式注入回事件来源的飞书会话,让该会话里的 AI 能够"收到"这次交互。
与官方 @larksuite/openclaw-lark 飞书插件配合工作;不修改官方插件源码。
它解决什么问题
OpenClaw 官方飞书插件已经把 card.action.trigger 事件路由到了 SDK 的 interactive dispatch 管道,但没有内置的兜底 handler——如果你发的卡片按钮 value.action 没有任何业务插件认领,事件会被静默丢弃。
典型场景:
- 在 main session 里让 OpenClaw 跑
openclaw message send把一张带按钮(或表单)的卡片发到群 B - 群 B 用户点按钮 / 提交表单
- 想让群 B 当前 session 的 AI 收到这次交互并继续后续动作
只要把按钮(或表单提交按钮)的 value.action 写成 custom-button:<payload>,本插件就会:
- 拦截到
card.action.trigger事件 - 调用 SDK 标准的
respond.followUp让机器人在群 B 发出一条文本,例如:[card-callback] action=custom-button:close payload=close sender=ou_xxx formValue=表单场景:
[card-callback] action=custom-button:submit payload=submit sender=ou_xxx formValue={"color":"red","note":"hi"} - 这条文本会被飞书插件自身的 inbound 流当作群里的新消息消费,群 B 的 session 里 AI 自然就"收到"了
工作原理
群 B 用户点按钮 / 提交表单
│
▼
飞书开放平台推送 card.action.trigger 事件
│
▼
官方 openclaw-lark 插件 (interactive-dispatch.ts)
│ 按 value.action 的 "<namespace>:<payload>" 找已注册的 handler
▼
本插件注册的 handler (namespace = "custom-button")
│ 从 ctx.rawEvent.action.form_value 提取表单数据(如有)
│ 调用 ctx.respond.followUp({ text: "[card-callback] ..." })
▼
机器人在群 B 发出一条普通消息
│
▼
群 B 的 session 里 AI 收到消息 → 继续推理详细看:
- 路由源头:interactive-dispatch.ts:194-226
- handler 上下文(
ctx.respond.reply/followUp/editMessage):interactive-dispatch.ts:55-73
安装
前置:本机已安装并能运行 OpenClaw(Node.js >= 22)。
OpenClaw 的 plugins install 命令要求插件提供已编译的运行时产物(不接受 .ts 入口),所以本插件的 package.json 把 openclaw.extensions 指向 ./dist/index.js,仓库里已经包含了一份手写好的 dist/index.js,可直接安装。
方式一:本地路径安装(推荐用于开发/自用)
openclaw plugins install /Users/chenzeping/projects/github.com/openclaw-lark/custom-plugins/feishu-card-callback-bridge如果你修改了 index.ts,需要重新生成 dist/index.js:
cd custom-plugins/feishu-card-callback-bridge
npm install --no-package-lock --silent typescript@5
npx tsc -p tsconfig.json或者直接手动同步修改到 dist/index.js(仓库当前提交的就是手写版本,逻辑等价于 index.ts,只是去掉了类型注解)。
方式二:发布到 npm 或 ClawHub
发布前注意:
- 把
package.json的private: true去掉 - 把
name改成你的命名空间,例如@your-org/feishu-card-callback-bridge - 注意:
name改了之后 OpenClaw 仍以 manifest 里的id(feishu-card-callback-bridge)作为 config key,安装日志里会出现一条提示,这是正常的
# npm 发布
cd custom-plugins/feishu-card-callback-bridge
npm publish --access public
# 安装
openclaw plugins install @your-org/feishu-card-callback-bridge或走 ClawHub:
clawhub package publish your-org/feishu-card-callback-bridge
openclaw plugins install clawhub:@your-org/feishu-card-callback-bridge验证安装
openclaw plugins list输出里应能看到 feishu-card-callback-bridge。
启动 OpenClaw 后日志里也会出现该插件的注册痕迹(找类似 registered interactive handler namespace=custom-button channel=feishu 的记录)。
配置
可选;不配置时使用下面这套默认值:
| 字段 | 类型 | 默认值 | 说明 |
| ----------------- | ------------------ | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| namespaces | string[] | ["custom-button"] | 要拦截的 value.action 命名空间。卡片按钮里 value.action 必须以 <namespace>: 开头。 |
| echoMode | "message" \| "card" \| "silent" | "message" | 处理方式:message = 在事件来源 chat 发文本(推荐);card = 替换原卡片;silent = 仅 toast。 |
| messageTemplate | string | {botMention} [card-callback] action={action} payload={payload} sender={senderId} formValue={formValue} | echoMode = message 时的文本模板。 |
| toastContent | string | 已收到 | 点击者看到的 toast 提示。 |
| botOpenId | string | "" | 可选。机器人自身的 open_id(形如 ou_xxx)。默认留空,插件会通过 OpenClaw 全局配置中的 channels.feishu.accounts.<accountId>.appId/appSecret 自动调用飞书 /open-apis/bot/v3/info API 拿到并缓存,无需手填。仅当你想覆盖自动探测的结果或宿主访问不到飞书 API 时才需要显式配置。配置后,模板里 {botMention} 占位符会渲染为 <at user_id="ou_xxx">Bot</at>,让 followUp 出去的消息被群里识别为"@机器人"。 |
模板支持以下占位符:
{action}—— 完整value.action,如custom-button:close{namespace}——custom-button{payload}—— 命名空间冒号之后的部分,如close{senderId}—— 点击者 open_id{conversationId}—— 事件来源 chat_id{messageId}—— 卡片消息的 message_id{formValue}—— 表单提交时的表单值(JSON 字符串),普通按钮点击时为空字符串{botMention}—— 默认走自动探测:插件首次收到事件时会调用一次飞书/open-apis/bot/v3/info拿到当前 account 的 bot open_id 并缓存,渲染为<at user_id="ou_xxx">Bot</at>;显式配置botOpenId时优先使用该值;探测失败/无凭据时降级为空字符串
配置示例
把以下片段加到 OpenClaw 全局配置里(具体路径以你的环境为准):
{
"plugins": {
"feishu-card-callback-bridge": {
"namespaces": ["custom-button", "ticket", "approval"],
"echoMode": "message",
"messageTemplate": "{botMention} 用户 {senderId} 在群 {conversationId} 点击了按钮: payload={payload} formValue={formValue}",
"toastContent": "已记录"
}
}
}关于
botOpenId:默认无需配置。插件首次收到回调时会用宿主已有的appId/appSecret自动调一次飞书/open-apis/bot/v3/info拿到 bot open_id 并按accountId缓存。日志会出现:feishu-card-callback-bridge: auto-detected bot open_id=ou_xxx for account=<accountId>仅在以下场景需要显式
"botOpenId": "ou_xxx":
- 宿主网络受限调不到飞书 OpenAPI
- 你想 mention 的对象不是当前消息所属机器人本身(少见)
使用一:发一张带回调按钮的卡片
在 main session 里让 OpenClaw 跑下面这条命令(机器人身份发到目标群):
openclaw message send --channel feishu \
--target "chat:oc_360fb72810559756e6567fd11c61dc59" \
--message '{
"schema": "2.0",
"config": { "wide_screen_mode": true, "update_multi": true },
"body": {
"elements": [
{ "tag": "markdown", "content": "请处理工单 #123" },
{
"tag": "column_set",
"columns": [
{
"tag": "column",
"elements": [{
"tag": "button",
"text": { "tag": "plain_text", "content": "关闭" },
"type": "default",
"behaviors": [{
"type": "callback",
"value": { "action": "custom-button:close", "ticket": "123" }
}]
}]
},
{
"tag": "column",
"elements": [{
"tag": "button",
"text": { "tag": "plain_text", "content": "派单" },
"type": "primary",
"behaviors": [{
"type": "callback",
"value": { "action": "custom-button:dispatch", "ticket": "123" }
}]
}]
}
]
}
]
}
}'要点:
- 顶层必须是
{"schema":"2.0", ...}形式,不要外层再裹{"card": ...} behaviors[].type必须是callback(写成open_url不会触发回调)value.action必须是<namespace>:<payload>形式,且namespace是本插件注册的(默认custom-button)value里其他字段(如ticket)会随事件透传,可在模板里用{payload}等读取或自行扩展 handler
使用二:发一张带表单提交的卡片
CardKit v2 的表单容器(tag: "form")支持把多个输入控件聚合后一次性提交。本插件会自动从事件里提取 action.form_value(也兼容顶层 form_value)并通过 {formValue} 占位符暴露出来。
openclaw message send --channel feishu \
--target "chat:oc_360fb72810559756e6567fd11c61dc59" \
--message '{
"schema": "2.0",
"config": { "wide_screen_mode": true, "update_multi": true },
"body": {
"elements": [
{ "tag": "markdown", "content": "请选择颜色并填写备注" },
{
"tag": "form",
"name": "color_form",
"elements": [
{
"tag": "select_static",
"name": "color",
"placeholder": { "tag": "plain_text", "content": "选择颜色" },
"options": [
{ "text": { "tag": "plain_text", "content": "红色" }, "value": "red" },
{ "text": { "tag": "plain_text", "content": "蓝色" }, "value": "blue" }
]
},
{
"tag": "input",
"name": "note",
"placeholder": { "tag": "plain_text", "content": "备注" }
},
{
"tag": "button",
"name": "submit_color_btn",
"text": { "tag": "plain_text", "content": "提交" },
"type": "primary",
"form_action_type": "submit",
"behaviors": [{
"type": "callback",
"value": { "action": "custom-button:submit-color" }
}]
}
]
}
]
}
}'要点:
- 把要一次性提交的输入控件包到
tag: "form"容器里,并给每个输入控件、提交按钮都设置唯一的name(form 容器内所有交互组件都必须有name,缺一个飞书客户端就会拒绝提交并报code:200530) - 提交按钮要标
form_action_type: "submit",且behaviors[].type = "callback"、value.action = "custom-button:<payload>" - 用户提交后,飞书会把容器里所有输入控件的当前值组装成
action.form_value(形如{"color":"red","note":"hi"})随事件推送 - 本插件把这个对象
JSON.stringify后作为{formValue}注入到messageTemplate;下游 AI 在群 B session 里会看到这条文本,可直接解析使用
发送时如何构造 JSON 参考同仓库下:send-lark-group-msg-as-bot/SKILL.md。
故障排查
| 现象 | 排查方向 |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| 卡片发出去了但内容是 JSON 文本,不是卡片 | --message 顶层 JSON 必须含 "schema":"2.0"(参考 send-lark-group-msg-as-bot SKILL) |
| 用户点按钮后群里没有"机器人 followUp 消息" | 1) 检查 value.action 是否以已注册命名空间开头 2) 启动日志里确认本插件已加载 3) 飞书后台查看「卡片回调地址」是否启用并指向 OpenClaw 的 WS 连接 |
| 表单提交后 {formValue} 是空 | 1) 提交按钮要在 tag: "form" 容器内并标 form_action_type: "submit" 2) 每个输入控件都要有唯一的 name |
| 表单提交时弹 出错了,请稍后重试 code:200530 | 绝大多数情况是卡片 JSON 不合法导致客户端本地校验失败:1) tag: "form" 容器内所有交互组件(包括 submit 按钮、input、select 等)都必须有唯一的 name —— 漏一个就会触发;2) submit 按钮必须有 behaviors[].value.action;3) 整个 form 不能嵌套在另一个 form 或 table 里,只能挂在卡片 root;4) 客户端确实没问题再排查服务端 3 秒响应——本插件已用 setImmediate 把 followUp/editMessage 甩到下一 tick 异步执行。 |
| 点了按钮只看到 toast,没文本 | echoMode 被改成了 silent 或 card;改回 message |
| AI 收到了消息但不会接续动作 | 群里 OpenClaw 默认要求被 @ 才会响应。推荐做法:在配置里填上机器人的 botOpenId,模板里保留 {botMention} 占位符(默认模板已包含),followUp 出去的消息会带 <at user_id="ou_xxx">Bot</at>,群里的 AI 直接被触发;或者修改群配置允许机器人消息触发 AI。 |
| 启动报 Cannot find module 'openclaw/plugin-sdk/...' | 该子路径由宿主 openclaw 提供;确认本机 OpenClaw 版本满足 peerDependencies 要求(>= 2026.5.4) |
文件结构
feishu-card-callback-bridge/
├── README.md # 本文件
├── package.json # 声明 openclaw.extensions = ["./dist/index.js"]
├── openclaw.plugin.json # 插件清单 + 配置 schema
├── tsconfig.json # 让 IDE 把这个子目录视为独立 TS 项目
├── index.ts # 核心实现(TS 源码)
└── dist/
└── index.js # 已编译的运行时入口(手写 ESM JS,逻辑等价于 index.ts)与官方飞书插件的关系
- 本插件不替代也不修改官方
@larksuite/openclaw-lark - 它只是注册了一个 SDK 标准的
registerInteractiveHandler,认领custom-button命名空间 - 升级官方飞书插件不会影响本插件
- 想接其他业务命名空间(如
ticket、approval),把它们加到namespaces配置数组即可,handler 会用同一段逻辑共享处理
License
MIT
