Skip to content

feat(approval): exec/plugin approval card with command session dispatch - #489

Draft
BrilliantWang wants to merge 30 commits into
mainfrom
feat/approval-v2
Draft

feat(approval): exec/plugin approval card with command session dispatch#489
BrilliantWang wants to merge 30 commits into
mainfrom
feat/approval-v2

Conversation

@BrilliantWang

@BrilliantWang BrilliantWang commented Apr 3, 2026

Copy link
Copy Markdown
Collaborator

feat(approval): DingTalk native approval support

背景

OpenClaw 的 ExecApproval / Plugin Approval 功能此前在 DingTalk channel 上没有原生支持。当 agent 命中需要审批的命令或工具时,DingTalk 用户只能通过 Web UI / 终端 UI 完成审批,无法在 DingTalk 会话内处理。

目标

在 DingTalk 会话内完整承接审批流程:

  • 审批请求以交互式 AI 卡片送达 agent 所在会话/群
  • 三个按钮(允许一次 / 永久允许 / 拒绝)直接提交决策
  • 所有状态反馈(待审批 / 已允许 / 已拒绝 / 已过期)在同一张卡片内更新
  • 支持 /approve <id> <decision> 文本命令作为备用入口

解决方案

实现 DingTalk 的 ChannelApprovalCapability,对齐 OpenClaw v2026.4.7 引入的 ChannelApprovalNativeRuntimeAdapter 契约(openclaw#62135)。Core 驱动整个 pending → resolved 生命周期,channel 实现 5 个 sub-adapter:

Sub-adapter DingTalk 实现
availability 仅处理 turnSourceChannel === "dingtalk" 的请求
presentation 构造卡片 paramMap(含 CardBtn[] 三按钮)
transport 调 DingTalk createAndDeliver + card streaming;resolved/expired 时更新卡片 UI
interactions —(未使用)
observe 投递成功 / 失败日志

投递模型:仅支持 origin surface(审批卡片发到 agent 所在的 DingTalk 会话),不走 approver DM 分发。DingTalk 的企业群/私聊本身就有天然的可见性边界。

按钮回调TOPIC_CARD 回调直接调 SDK 的 resolveApprovalOverGateway 提交决策,不经过任何本地 store 或文本命令中转。

/approve 文本命令:在 inbound-handler 入口早期 intercept,同样直接调 resolveApprovalOverGateway此 bypass 是必需的:Plugin Approval 的 waitDecision 阻塞在 dispatchReply 内并持有 DingTalk 自己的 session lock,若 /approve 走正常 inbound 路径会 session lock 死锁,120s 超时失败。

重要决策

使用 nativeRuntime adapter,不使用 approver-restricted 上层工厂

Telegram / Discord / Slack / Matrix 都通过 createApproverRestrictedNativeApprovalCapability 装配 capability,围绕 "approver 列表" 语义设计。DingTalk 不引入 approver 配置概念,直接装配最小 capability。未来如有需要切到工厂方案,5 个 sub-adapter 可以原封复用。

没有本地 store

Core 在 activeEntries 内存 Map 中管理 pending entry 生命周期,transport.deliverPending 返回的 entry(含 outTrackId 等)会自动带回给 transport.updateEntry。channel 侧不需要自建 approvalCardStore。进程重启会丢失 pending 审批——这个行为和 core 一致,不是 channel 的责任。

卡片内完成全部状态反馈

transport.updateEntry 在 resolved/expired 时将按钮替换为状态文本("✅ 已允许"/"❌ 已拒绝"/"⏱️ 已过期")并置 hasAction=false。不发额外的群聊文本回复,界面整洁。/approve 文本命令同理。

Breaking Change

peerDependencies.openclaw>=2026.3.28 bump 到 >=2026.4.7,需要 v2026.4.7 提供的 ChannelApprovalNativeRuntimeAdapter 契约和 resolveApprovalOverGateway 公开 API。

依赖

依赖 PR #480(AI Card template v2 / CardBtn[] with sendCardRequest)合并后才能在真机上验证按钮交互。当前实现已按 PR #480 的回调格式编码。

测试

  • 单元测试 970 全绿(含 13 个 nativeRuntime adapter 测试)
  • lint 0 errors(与 baseline 一致)
  • 手工验证 TODO(等 PR feat(card): implement card template v2 with rich block content and action btns #480 merge 后补):
    • ExecApproval:命令审批卡片投递、三按钮点击、resolved 卡片更新
    • PluginApproval:同上
    • /approve 文本命令在直接消息和群聊(带 @bot 前缀)两种场景下生效
    • 审批超时后卡片显示"已过期"

@He-Pin

He-Pin commented Apr 3, 2026

Copy link
Copy Markdown

非常不错,我最近也在做这个,你是阿里的同学吗? 我目前是在服务器端做的。

@BrilliantWang

Copy link
Copy Markdown
Collaborator Author

非常不错,我最近也在做这个,你是阿里的同学吗? 我目前是在服务器端做的。

我不是哈哈, 服务端是什么思路啊?

@vercel

vercel Bot commented Apr 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openclaw-channel-dingtalk Ready Ready Preview, Comment Apr 8, 2026 2:26am

- Add card-approve-command.ts using CommandTargetSessionKey pattern
- Intercept /approve text command before session lock in inbound-handler
- Refactor handleApprovalCardCallback to use command session dispatch
- Remove gateway client (getGatewayClient, prewarmGatewayClient, resolveApprovalDecision)
- Expand ApprovalCardEntry with agentId/sessionKey for command routing
- Update tests for new dispatch pattern
Implement DingTalk's ChannelApprovalNativeRuntimeAdapter (5 sub-adapters:
availability / presentation / transport / interactions / observe) plus a
ChannelApprovalNativeAdapter declaring the origin-only delivery surface.

- src/approval/approval-native-adapter.ts: handles createAndDeliver,
  card streaming, and resolved/expired updates. Returns a typed pending
  entry that core persists across the resolve lifecycle, removing the
  need for a local approvalCardStore.
- src/approval/approval-capability.ts: assembles the capability with
  origin-surface delivery; resolveOriginTarget reads turnSourceTo from
  the request as the raw conversationId/userId.

Aligns DingTalk with the SDK contract introduced in openclaw 2026.4.7
(PR #62135), matching the model used by discord/telegram/slack/matrix.
…time

Wire the new ChannelApprovalCapability into the channel and remove the
legacy approval delivery path. SDK now drives the entire pending → resolved
lifecycle through the native runtime adapter; channel.sendPayload no
longer special-cases approval payloads.

Card button callbacks now resolve approvals directly via
resolveApprovalOverGateway from openclaw/plugin-sdk/approval-gateway-runtime,
removing the local handleApprovalCardCallback indirection. The /approve
text command intercept in inbound-handler does the same — it stays in place
because DingTalk's own session-lock would deadlock plugin-approval
waitDecision if /approve went through the normal inbound pipeline.

Changes
- src/channel.ts: replace `approvals: { render: ... }` block (which was
  already a type error against the SDK channel plugin shape) with
  `approvalCapability: dingtalkApprovalCapability`. Drop the
  `_dingtalkApproval` channelData hack from sendPayload.
- src/inbound-handler.ts: /approve early intercept calls
  resolveApprovalOverGateway directly with parsed approvalId/decision/
  senderId; no command session dispatch indirection.
- src/approval-card-service.ts: trim down to card param map builders +
  callback parsers. Card delivery and updates moved to the adapter.
- src/command/card-approve-command.ts: deleted (functionality replaced
  by direct resolveApprovalOverGateway calls in two sites).
- tests/unit/approval-card-callback.test.ts: deleted (covered by adapter
  tests + inbound-handler /approve tests).
- tests/unit/approval-card-service.test.ts: drop store/sendApprovalCard
  tests; the adapter tests cover the API surface.
- tests/unit/inbound-handler.test.ts: rewrite /approve tests to assert
  on resolveApprovalOverGateway invocation instead of command session
  dispatch.
- package.json: bump peerDep openclaw to >=2026.4.7 (the version that
  ships ChannelApprovalNativeRuntimeAdapter from PR #62135).
- tests/unit/{plugin-manifest,sdk-import-structure}.test.ts: align
  expected min versions with the bump.
…vate types

`defineChannelPluginEntry` from openclaw/plugin-sdk/core returns an
internal `DefinedChannelPluginEntry<TPlugin>` type that is not exported
publicly. With `declaration: true` in tsconfig, TypeScript tries to emit
a portable name for the inferred `default` export type and fails with
TS2742 once the plugin type transitively references any non-public SDK
internal (which happens as soon as we add `approvalCapability` referencing
the new approval types).

This project sets `noEmit: true`, so `declaration` and `declarationMap`
only run as extra static checks — no .d.ts files are produced. Removing
them trades that strictness for compatibility with the SDK's private
internal types and matches how other channels in this ecosystem build.
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.

2 participants