Conversation
实现业务自定义卡片交互的转发功能,平台将 card.action.trigger 事件转发给业务插件注册的 handler。新增 card-action-forward 模块提供注册、注销和分发功能,完善卡片交互的处理流程。
|
|
|
增加飞书卡片针对card.action.trigger事件的处理逻辑,在现有飞书卡片的交互逻辑中,卡片的点击触发事件会在飞书插件侧拦截并处理。 改造点:增加飞书插件的Action处理逻辑,若飞书插件无法处理该事件,则转发出去,由其他业务方插件自行接收并决定是否要处理 |
HanShaoshuai-k
left a comment
There was a problem hiding this comment.
Review 建议
感谢提交!让 openclaw-lark 支持业务自定义卡片交互分发,这个需求是合理且必要的——目前 telegram/discord/slack 三个 channel 都已接入 SDK 的 interactive dispatch 管道,openclaw-lark 是唯一缺失的主流 channel。
不过实现方式上建议调整,核心问题是:SDK 已经提供了 dispatchPluginInteractiveHandler(从 openclaw/plugin-sdk/plugin-runtime 导出),不需要自建注册表。
具体问题
1. 自建 globalThis 注册表 vs SDK 标准管道
当前 PR 通过 Symbol.for + globalThis 自建了一套注册/分发机制,但 openclaw SDK 已经有现成的:
api.registerInteractiveHandler({ channel, namespace, handler })— 插件侧注册dispatchPluginInteractiveHandler()— channel 侧分发- 内置 namespace 路由(
namespace:payload格式)和去重缓存(TTL 5min)
参考 telegram/discord/slack 的做法,每个 channel 都有一个 interactive-dispatch.ts,包装 dispatchPluginInteractiveHandler 并构造 channel-specific 的上下文。
2. 缺少 namespace 隔离
三个参考 channel 全部使用 channel:namespace 作为路由 key。当前 PR 只用裸 action 字符串路由,不同插件之间容易冲突。
3. 插件 handler 上下文过于简陋
当前只传 { accountId, data }。参考其他 channel,handler context 应该包含 senderId、conversationId、respond 方法(reply / editMessage 等),否则插件拿到事件也无法有效回复用户。
4. 异常静默吞掉
handler 抛异常后返回 undefined,用户侧无任何反馈。建议至少返回一个 error toast。
建议方案
参照 telegram/discord/slack 的统一模式:
- 新建
src/channel/interactive-dispatch.ts,定义FeishuInteractiveHandlerContext类型和dispatchFeishuPluginInteractiveHandler()wrapper - 在
handleCardActionEvent末尾 fallback 调用该 wrapper(这一步的位置和当前 PR 一致) - 业务插件通过
api.registerInteractiveHandler({ channel: 'feishu', namespace: '...', handler })标准方式注册
改动量和当前 PR 差不多,但能白拿 namespace 路由、去重缓存、标准化上下文等能力,也和其他 channel 保持一致。
移除旧的卡片动作转发实现,改用 OpenClaw SDK 的标准交互分发管道
|
改动背景
本次改动做了什么
落地改动点(代码层)
行为变化
兼容性与注意事项
|
HanShaoshuai-k
left a comment
There was a problem hiding this comment.
Review — 第二轮
感谢更新!整体方向已经对了——移除了自建 globalThis 注册表,改用 SDK 的 dispatchPluginInteractiveHandler,这和 telegram/discord/slack 的模式一致。不过当前代码还有一些问题需要修复。
先说一个上次 review 的遗漏:上次我建议直接对齐 SDK 管道,但没有提到飞书和其他 channel 在响应模型上有本质差异,导致这次适配时踩了坑。这里补充说明一下,后面的建议也会给出具体的解法。
一、架构适配问题:飞书同步返回值 vs SDK 异步模型
飞书卡片交互的响应方式和 telegram/slack/discord 不同:
| 飞书 | Telegram / Slack / Discord | |
|---|---|---|
| 响应机制 | handler 返回 { toast, card } 作为 HTTP response body,同步生效 |
handler 通过 respond.reply() 等异步 API 调用发送响应 |
SDK 的 dispatchPluginInteractiveHandler 返回 InteractiveDispatchResult:
{ matched: boolean; handled: boolean; duplicate: boolean }这是 dispatch 元信息,不携带 handler 的业务返回值。handler 返回的 { toast, card } 会在 invoke 内部被丢弃。
当前代码(第 197-201 行)把 dispatch 结果直接当作卡片响应返回给飞书,这是不对的——飞书拿到的会是 { matched: true, handled: true } 而不是 toast/card。
建议做法: 在 invoke 里用闭包捕获 handler 的实际返回值:
let cardResponse: unknown;
const result = await dispatchPluginInteractiveHandler<FeishuInteractiveHandlerRegistration>({
channel: 'feishu',
data: basics.action,
dedupeId: ...,
invoke: async ({ registration, namespace, payload }) => {
cardResponse = await registration.handler(handlerCtx);
return { handled: true };
},
});
if (!result.matched) return undefined;
return cardResponse;这样飞书插件 handler 可以直接 return { toast: { type: 'success', content: '...' } } 做即时反馈,和现有的 handleAskUserAction / handleCardAction 体验一致。
二、代码 Bug
1. channel: 'slack' — copy-paste 错误(第 113 行、第 199 行)
两处都写成了 'slack',应为 'feishu'。注册在 feishu channel 的 handler 全部无法命中。
2. dispatchPluginInteractiveHandler 调用签名错误(第 197-201 行)
当前传的是 { channel, data, ctx, respond },但 SDK 签名是:
{ channel, data, dedupeId?, onMatched?, invoke }缺少核心的 invoke 回调,ctx 和 respond 不是 SDK 认识的参数。as any 绕过了类型检查,掩盖了这个问题。参考 telegram/slack 的写法,应在 invoke 内部组装 context + respond 传给 handler。
3. 缺少 dedupeId
飞书卡片事件可能重复投递。telegram 用 callbackId,slack/discord 用 interactionId 去重。建议用 openMessageId + action 或飞书事件 token 作为去重 key。
三、其他改进建议
1. as any 滥用(第 112 行、第 197 行)
ctx 直接声明为 any,dispatch 调用也用 as any 强转。建议定义 FeishuInteractiveHandlerContext 和 FeishuInteractiveHandlerRegistration 类型(参考 telegram/slack 的做法),并导出供业务插件使用。
2. respond 方法定位
当前的 respond.reply() / respond.editMessage() 是异步发消息,作为辅助手段没问题,但飞书卡片交互的主要响应路径是 handler 的同步返回值(toast/card)。建议在注释或文档中明确说明这个差异,让业务插件开发者知道应该优先用 return 而非 respond。
3. parseRouteKey 与 SDK 重复
SDK 内部已经在 resolvePluginInteractiveNamespaceMatch 里做了 namespace:payload 解析。外层的 parseRouteKey 逻辑重复,可以移除,只在构建 handler context 时使用 invoke 回调里 match 提供的 namespace 和 payload。
4. 测试和类型导出
- 建议补充单元测试(namespace 解析边界、handler 匹配/不匹配/异常、返回值传递)
- 导出
FeishuInteractiveHandlerRegistration类型,让业务插件有类型提示
No description provided.