Skip to content

Feat/add card action#395

Open
chenxiaoyufisher-hash wants to merge 4 commits intolarksuite:mainfrom
chenxiaoyufisher-hash:feat/add_card_action
Open

Feat/add card action#395
chenxiaoyufisher-hash wants to merge 4 commits intolarksuite:mainfrom
chenxiaoyufisher-hash:feat/add_card_action

Conversation

@chenxiaoyufisher-hash
Copy link
Copy Markdown

No description provided.

实现业务自定义卡片交互的转发功能,平台将 card.action.trigger 事件转发给业务插件注册的 handler。新增 card-action-forward 模块提供注册、注销和分发功能,完善卡片交互的处理流程。
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@chenxiaoyufisher-hash
Copy link
Copy Markdown
Author

chenxiaoyufisher-hash commented Apr 7, 2026

增加飞书卡片针对card.action.trigger事件的处理逻辑,在现有飞书卡片的交互逻辑中,卡片的点击触发事件会在飞书插件侧拦截并处理。
当用户openclaw存在其他插件输出飞书卡片的情况下,card.action.trigger事件只会由飞书插件拦截并处理,不会转发给业务插件

改造点:增加飞书插件的Action处理逻辑,若飞书插件无法处理该事件,则转发出去,由其他业务方插件自行接收并决定是否要处理

Copy link
Copy Markdown
Collaborator

@HanShaoshuai-k HanShaoshuai-k left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 应该包含 senderIdconversationIdrespond 方法(reply / editMessage 等),否则插件拿到事件也无法有效回复用户。

4. 异常静默吞掉

handler 抛异常后返回 undefined,用户侧无任何反馈。建议至少返回一个 error toast。

建议方案

参照 telegram/discord/slack 的统一模式:

  1. 新建 src/channel/interactive-dispatch.ts,定义 FeishuInteractiveHandlerContext 类型和 dispatchFeishuPluginInteractiveHandler() wrapper
  2. handleCardActionEvent 末尾 fallback 调用该 wrapper(这一步的位置和当前 PR 一致)
  3. 业务插件通过 api.registerInteractiveHandler({ channel: 'feishu', namespace: '...', handler }) 标准方式注册

改动量和当前 PR 差不多,但能白拿 namespace 路由、去重缓存、标准化上下文等能力,也和其他 channel 保持一致。

移除旧的卡片动作转发实现,改用 OpenClaw SDK 的标准交互分发管道
@chenxiaoyufisher-hash
Copy link
Copy Markdown
Author

chenxiaoyufisher-hash commented Apr 8, 2026

改动背景

  • openclaw-lark 之前缺少“业务自定义卡片交互分发”的标准管道,和 telegram/discord/slack 三个主流 channel 不一致。
  • 原先的实现是通过 globalThis 自建注册表(Symbol.for + __openclaw_lark_card_action__)来做 action→handler 的转发,这会带来:
  • 缺少 namespace 隔离(不同插件 action 容易冲突)
  • 缺少 SDK 内置的去重缓存能力(5min TTL 的重复回调抑制)
  • handler 上下文不标准(插件难以复用跨 channel 的交互处理逻辑)
  • 异常容易静默吞掉,缺少统一的“交互失败反馈”

本次改动做了什么

  • 把飞书 card.action.trigger 的“业务兜底分发”从“自建 registry”切换为 OpenClaw SDK 标准 interactive dispatch 管道
  • 插件侧:通过 api.registerInteractiveHandler({ channel, namespace, handler }) 注册(统一模型)
  • channel 侧:由 openclaw-lark 把 Feishu 的 event.action.value.action 作为 namespace:payload 路由 key,转给 SDK 的 dispatchPluginInteractiveHandler(...) 进行匹配与分发
  • 同时保留“宿主内建能力优先”的处理顺序:先 AskUserQuestion、再 auto-auth,都不匹配时才进入业务插件 interactive dispatch

落地改动点(代码层)

  • 新增:Feishu interactive 分发适配器 interactive-dispatch.ts
  • 修改:在 event-handlers.ts 的 handleCardActionEvent 末尾,把原本的自建分发替换为调用 dispatchFeishuPluginInteractiveHandler(...)
  • 删除:旧的自建注册表文件 src/channel/card-action-forward.ts(以及相关 import)

行为变化

  • 业务插件交互接入方式标准化:
  • 业务侧不再依赖 globalThis.__openclaw_lark_card_action__.*,改为使用 SDK 的 registerInteractiveHandler 注册
  • “action 路由”升级为 namespace:payload 约定(namespace 作为路由键,payload 作为透传参数),减少不同插件之间的冲突风险
  • 交互处理能力更贴近其他 channel:
  • 使用 SDK 的分发逻辑,天然复用其路由匹配与去重机制(避免同一交互事件重放/重复触发导致多次处理)
  • 插件 handler 获得更标准的交互上下文(本次在 Feishu 侧以 Slack interactive 契约进行桥接,便于插件复用现有 slack 交互处理代码)
  • Feishu 侧交互更新语义更正确:
  • respond.editMessage() 已尽量映射为更新原卡片(updateCardFeishu),避免“edit 变成新发消息”的 UI 异常
  • 失败反馈更明确:
  • interactive dispatch 异常时返回统一 toast(用户可感知交互失败,而不是静默无响应)

兼容性与注意事项

  • 这是“管道切换”而不是“完全等价替换”:旧的 globalThis 注册方式被移除,业务插件需要同步迁移到 SDK registerInteractiveHandler
  • 当前 SDK 的 interactive handler 类型只原生支持 telegram/discord/slack,因此 Feishu 暂采用 Slack 交互契约桥接channel: 'slack'),业务插件在注册时应以此为准(例如注册 channel: 'slack' 的 interactive handler),直到 SDK 提供 feishu 的原生 interactive 通道再做后续演进。

Copy link
Copy Markdown
Collaborator

@HanShaoshuai-k HanShaoshuai-k left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 回调,ctxrespond 不是 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 强转。建议定义 FeishuInteractiveHandlerContextFeishuInteractiveHandlerRegistration 类型(参考 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 提供的 namespacepayload

4. 测试和类型导出

  • 建议补充单元测试(namespace 解析边界、handler 匹配/不匹配/异常、返回值传递)
  • 导出 FeishuInteractiveHandlerRegistration 类型,让业务插件有类型提示

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants