跳转到内容

07-实战 · 飞书卡片回调处理器

目标:让飞书卡片按钮真正驱动 OpenClaw,但只执行你允许的动作,不把回调入口变成任意命令执行入口。


卡片按钮点击后,飞书会把事件发给你的机器人回调地址。OpenClaw 侧通常由 openclaw-lark 接住事件,再交给一个 handler 判断要做什么。

handler 不应该“很聪明”。它只做 5 件事:

  1. 校验事件来源和签名
  2. 解析按钮 value
  3. 检查 action 是否在白名单
  4. 检查卡片是否过期、task_id 是否存在
  5. 调用固定脚本或固定 agent 动作

不要让 handler 根据用户传入的字符串拼 shell 命令。


flowchart TD
callback[飞书 callback] --> verify[签名 / token 校验]
verify --> parse[解析 value]
parse --> whitelist{action 白名单?}
whitelist -->|否| reject[记录并拒绝]
whitelist -->|是| state{task_id 存在?}
state -->|否| expired[提示卡片过期或失效]
state -->|是| dispatch[分发到固定动作]
dispatch --> merge[merge 脚本]
dispatch --> rollback[rollback 脚本]
dispatch --> resend[重发卡片]
merge --> audit[写审计日志]
rollback --> audit
resend --> audit

先定义一张白名单表,而不是在代码里到处写 if:

{
"memory-merge-high": {
"command": "memory-merge-weekly.py",
"args": ["--tier", "high"],
"dangerous": true,
"confirm_required": false
},
"memory-rollback-preview": {
"command": "memory-merge-weekly.py",
"args": ["--rollback", "--dry-run"],
"dangerous": false,
"confirm_required": false
},
"memory-rollback-confirm": {
"command": "memory-merge-weekly.py",
"args": ["--rollback"],
"dangerous": true,
"confirm_required": true
},
"cancel": {
"command": "noop",
"dangerous": false,
"confirm_required": false
}
}

真实实现可以写在 JS/TS 对象里,也可以放 JSON 配置。关键是:用户回调只能选择这张表里的动作。


下面是结构示意,不依赖具体插件版本:

const ALLOWED_ACTIONS = new Map([
['memory-merge-high', { script: 'memory-merge-weekly.py', args: ['--tier', 'high'] }],
['memory-rollback-preview', { script: 'memory-merge-weekly.py', args: ['--rollback', '--dry-run'] }],
['cancel', { script: null, args: [] }],
]);
export async function handleCardAction(event) {
const value = event?.action?.value;
const action = value?.action;
if (!ALLOWED_ACTIONS.has(action)) {
return reject('unknown_action', { action });
}
const taskId = value?.task_id;
const state = await loadState(taskId);
if (!state) {
return reject('unknown_task', { taskId });
}
if (isExpired(value?.card_meta?.issued_at, value?.card_meta?.expires_in_sec)) {
return reject('expired_card', { taskId });
}
await appendAuditLog({ action, taskId, operator: event.operator, ts: Date.now() });
return dispatch(ALLOWED_ACTIONS.get(action), value, state);
}

生产实现还要加签名校验、异常捕获、超时控制和日志脱敏。


handler 调脚本时,参数必须来自白名单和经过校验的字段:

可以:

memory-merge-weekly.py --tier high
memory-merge-weekly.py --rollback --dry-run --tier review

不要:

memory-merge-weekly.py ${event.action.value.raw_args}
sh -c ${event.action.value.command}

如果需要传 hash 列表,建议写入临时 JSON 文件,再把文件路径传给脚本:

Terminal window
python3 memory-merge-weekly.py --include-hashes-file /tmp/openclaw-card-action-xxx.json

这样比把长数组拼进命令行更容易审计。


每次点击都写一行 JSONL:

{"ts":"2026-04-24T10:15:00+08:00","action":"memory-merge-high","task_id":"weekly-2026-04-24","operator":"ou_xxx","result":"accepted"}

建议记录:

字段用途
ts排障排序
action用户点了哪个按钮
task_id对应哪张卡
operator谁点的,注意脱敏
resultaccepted / rejected / failed
reason拒绝或失败原因
card_id后续更新卡片

不要记录完整 token、appSecret、原始 headers。


卡片过期后,危险动作应该拒绝,但可以给用户一个温和提示:

  • “这张周审卡片已过期,请回复 重发周审卡片。”
  • “本次合并状态已变化,请打开最新卡片操作。”
  • “回滚窗口已关闭,请在终端执行 dry-run 后人工确认。”

过期策略建议:

动作过期后
查看详情可以允许
重发卡片可以允许
合并记忆拒绝
回滚拒绝或要求管理员确认
取消可以允许,但只更新 UI

有些动作可以派给 AI,有些不应该。

适合交给 agent:

  • 总结本周主题
  • 解释某条候选为什么进入高价值
  • 生成合并后的自然语言报告
  • 帮用户定位失败日志

不适合直接交给 agent:

  • 删除长期记忆文件
  • 执行任意 shell
  • 修改 OpenClaw 主配置
  • 绕过二次确认做回滚

规则很简单:AI 可以“建议”,状态变更必须走脚本和白名单。


帮我写一个飞书卡片 action handler。
要求:
1. 输入是飞书回调事件 JSON。
2. 只允许处理白名单 action:memory-merge-high、memory-rollback-preview、cancel。
3. value 必须包含 action、task_id、card_meta.issued_at。
4. 卡片超过 7 天拒绝危险动作。
5. 不允许使用 sh -c,不允许执行用户传入的 command。
6. 每次处理都写 JSONL 审计日志,敏感字段脱敏。
7. 给出 8 个单元测试 case。

如果 AI 生成了 eval、exec(raw)、sh -c,直接让它重写。


handler 写完后,不要急着上线。继续看 飞书卡片验证与排障,先用模拟 callback 把每个 action 跑一遍。