Files
nanoclaw/docs/answers/08-channel-adapters.md

8.8 KiB
Raw Blame History

Q20-Q21: Channel 适配器


Q20: 如果要加一个新的 channel比如钉钉需要实现什么接口、改哪些文件

答案

ChannelAdapter 接口(完整合约)

定义在 src/channels/adapter.ts:111-167

必需属性:

属性 类型 说明
name string 适配器显示名称
channelType string 唯一类型标识(如 'dingtalk'
supportsThreads boolean true=平台以线程为主要对话单元,false=频道本身就是对话

必需方法:

方法 签名 说明
setup(config) (config: ChannelSetup) => Promise<void> 初始化连接,注册事件 handler
teardown() () => Promise<void> 优雅关闭
isConnected() () => boolean 连接状态
deliver(platformId, threadId, message) (string, string|null, OutboundMessage) => Promise<string|undefined> 发送出站消息;如有则返回平台消息 ID

可选方法:

方法 何时实现 用途
setTyping?(platformId, threadId) 有 typing 指示器的聊天平台 显示 "bot is typing..."
syncConversations?() 有频道发现的平台 列出所有可访问的对话
resolveChannelName?(platformId) 显示需要 频道 ID 的人类可读名称
subscribe?(platformId, threadId) 有线程订阅的平台 订阅 bot 到线程;幂等——调用两次是 no-op
openDM?(userHandle) 需要区分 user ID 和 DM channel ID 的平台 打开或获取 DM 频道,返回 DM 的 platform_id。仅 Discord、Slack、Teams、Webex、gChat 需要。Telegram、WhatsApp、iMessage 等跳过——user handle 就是 DM chat ID

ChannelSetup 接口(适配器拿到的回调)

定义在 adapter.ts:9-26

interface ChannelSetup {
  onInbound(platformId, threadId, message: InboundMessage): void | Promise<void>;  // 入站消息
  onInboundEvent(event: InboundEvent): void;            // CLI/admin 传输
  onMetadata(platformId, name?, isGroup?): void;        // 频道元数据
  onAction(questionId, selectedOption, userId): void;   // 按钮点击
}

两种实现模式

模式 1原生 Adapter — 直接实现 ChannelAdapter处理平台原始协议HTTP API、WebSocket 等。示例WhatsApp (Baileys)、Signal、iMessage。

模式 2Chat SDK Bridge — 用 createChatSdkBridge(config)chat-sdk-bridge.ts:122)把已有 Chat SDK Adapter 包装成 ChannelAdapter。用于 Discord、Slack、Telegram、Teams、GitHub、Linear、Webex、Matrix、Google Chat。

加新 Channel 要创建/修改的文件

  1. 创建适配器模块:

    • 如果用 Chat SDKsrc/channels/dingtalk.tscreateChatSdkBridge({...})
    • 如果用原生:src/channels/dingtalk.ts 直接实现 ChannelAdapter
  2. 自注册:registerChannelAdapter('dingtalk', { factory, containerConfig? })

  3. 接入导入:src/channels/index.ts 添加 import './dingtalk.js';(启动时加载所有适配器的 barrel

  4. Container config可选 如果适配器需要在 agent 容器内额外 mount 或 env var

    registerChannelAdapter('dingtalk', {
      factory: () => createDingTalkAdapter(),
      containerConfig: {
        mounts: [{ hostPath: '/path/to/certs', containerPath: '/certs', readonly: true }],
        env: { DINGTALK_APP_KEY: process.env.DINGTALK_APP_KEY! },
      },
    });
    
  5. Skill 打包:CONTRIBUTING.md 规范写成 channel install skill

    • skills/add-dingtalk/SKILL.md — 面向用户的指引
    • Skill 从 channels 分支复制 src/channels/dingtalk.ts,追加 import 到 barrelpnpm install <pkg>

关键边界情况

情况 如何处理
缺少凭据 Factory 返回 nullinitChannelAdapters 跳过并 warnchannel-registry.ts:57-59
Setup 时网络错误 指数退避重试:[2s, 5s, 10s]channel-registry.ts:10,68-87
重复注册 registerChannelAdapterline 26registry.set() 静默覆盖
isMention flag 传播 知道平台 mention 语义的适配器设置 InboundMessage.isMention——router 用它替代正则 name-matching

Q21: Chat SDK bridge 是什么?为什么 Discord/Slack/Telegram 等共用它?

答案

什么是 Chat SDK Bridge

Chat SDK bridgesrc/channels/chat-sdk-bridge.ts680 行)是一个通用适配器 shim,把任何 Chat SDK Adapter 实例包装成 NanoClaw 兼容的 ChannelAdapter。它一次性处理平台无关的 concern让每个具体 channel 模块只需提供平台特定配置。

核心工厂函数 createChatSdkBridge(config)line 122返回完整实现所有必需和可选方法的 ChannelAdapter 对象。

Bridge 集中化的 10 项 concern

  1. 四种 dispatch 路径line 213-266——Chat SDK 区分四种入站消息bridge 把它们映射为正确的 isMention flag

    • onSubscribedMessageonInbound(channelId, threadId, message, isMention=message.isMention)
    • onNewMentiononInbound(channelId, threadId, message, isMention=true)
    • onDirectMessageonInbound(channelId, threadId, message, isMention=true, isGroup=false)
    • onNewMessage(/[\s\S]*/)onInbound(channelId, threadId, message, isMention=false, isGroup=true)
  2. 附件下载line 138-163——序列化消息到 JSON 前先下载附件为 base64使其在 inbound.db content 列中存活

  3. Reply context 提取line 164-169——平台特定 hookextractReplyContext

  4. Sender 字段归一化line 173-180——把 Chat SDK 的嵌套 author 对象投影为 router 需要的扁平 senderId/sender/senderName

  5. 卡片渲染line 387-470——把 ask_questionsend_card MCP tool payload 渲染为带按钮的 Card。处理按钮编码整数 index vs 全值,适配 Telegram 64 字节 callback_data 限制)

  6. 文本拆分line 480-497splitForLimit line 106-120——按 paragraph→line→space→char 边界拆分消息,适配 maxTextLengthDiscord 2000Telegram 4096

  7. Gateway listenerline 303-359——对支持 Gateway 的适配器Discord启动本地 HTTP server 接收转发事件,指数退避重启(上限 1h

  8. Webhook 注册line 362——对非 gateway 适配器Slack、Teams、GitHub注册到共享 webhook server

  9. openDM 委托line 534-549——直接委托 adapter.openDM() 而非 chat.openDM()

  10. Transform hookline 124——transformOutboundText 允许 per-platform 文本清理

原生 Adapter vs Chat SDK Bridge

方面 Chat SDK Bridge 原生 Adapter
平台协议 由 Chat SDK 的 Adapter 处理 直接处理(如 Baileys 处理 WhatsApp WebSocket
消息解析 Chat SDK 归一化为 Message 类型bridge 序列化为 JSON Adapter 解析原始平台 payload直接构造 InboundMessage
Dispatch Bridge 映射 4 个 SDK dispatch 路径到 onInbound Adapter 从自己的 event handler 直接调用 onInbound
卡片/按钮渲染 Bridge 用 Chat SDK 的 Card/Button/Actions 组件渲染 Adapter 必须实现平台特定的交互式消息渲染
Gateway/Webhook Bridge 处理 Gateway listener 生命周期 Adapter 自己处理连接生命周期
openDM 委托 adapter.openDM() Adapter 通过平台 API 直接实现
Channel ID 编码 adapter.channelIdFromThreadId() Adapter 用自定义 ID 方案
示例 Discord、Slack、Telegram、Teams、GitHub、Linear、Webex、Matrix、Google Chat、Resend WhatsApp (Baileys)、Signal、iMessage

Bridge 处理的边界情况

情况 位置
Gateway crash → IP block 保护 Line 314-357指数退避上限 1h5min 健康运行后重置计数器
Telegram 64 字节 callback_data 限制 Line 400-408把 button value 编码为整数 index
Discord interaction 更新 Line 607-668通过 Discord REST API 更新卡片,再 dispatch 到 onAction
序列化时附件数据丢失 Line 138-163调用 toJSON() 先下载附件数据为 base64
媒体 only 空文本消息 Line 263onNewMessage(/[\s\S]*/) 匹配所有消息包括空文本
chat.openDM 对非标准 user ID 抛错 Line 534-549直接委托 adapter.openDM()
Raw message 字段过大 Line 183serialized.raw = undefined 省 DB 空间

user-dm.ts 兜底

对于没有 openDM 支持的 channelTelegram、WhatsApp、iMessage、email、Matrixsrc/user-dm.ts 直接把 user handle 当 DM platform_id——user 本身就是 DM chat。Bridge 仅当底层 adapter.openDM 存在时才附着 openDMline 544