Skip to content

Channel Adapter

ChannelAdapter 接口定义 GolemBot 如何连接 IM 平台。

ChannelAdapter 接口

typescript
interface MentionTarget {
  name: string;        // 显示名称(如 "小舟")
  platformId: string;  // 平台用户 ID
}

interface ReplyOptions {
  mentions?: MentionTarget[];  // 已解析的 @mention 目标,用于原生渲染
}

interface ChannelAdapter {
  readonly name: string;
  /** 可选:覆盖该 channel 的默认消息分割长度限制(默认 4000 字符)。 */
  readonly maxMessageLength?: number;
  start(onMessage: (msg: ChannelMessage) => void | Promise<void>): Promise<void>;
  reply(msg: ChannelMessage, text: string, options?: ReplyOptions): Promise<void>;
  stop(): Promise<void>;
  /** 可选:向聊天发送"正在输入…"指示器。在 AI 调用前触发,每 4 秒刷新一次,
   *  AI 回复完毕后自动停止。 */
  typing?(msg: ChannelMessage): Promise<void>;
  /** 可选:获取群成员列表用于 @mention 支持。
   *  返回 displayName → platformId 的 Map。
   *  当 AI 回复包含 @name 时由 Gateway 调用。 */
  getGroupMembers?(chatId: string): Promise<Map<string, string>>;
  /** 可选:用户阅读 bot 消息时的回调。目前飞书适配器支持。 */
  readReceiptHandler?: (receipt: ReadReceipt) => void;
  /** 可选:主动发送消息到指定会话(无需入站消息上下文)。
   *  被定时任务系统用于将结果推送到 IM 通道。 */
  send?(chatId: string, text: string): Promise<void>;
  /** 该 Adapter 是否支持主动 send()。未定义时默认为 send() 存在则为 true。 */
  readonly canSend?: boolean;
  /** 可选:拉取 since 之后的历史消息,用于重启后的离线消息追回。 */
  fetchHistory?(chatId: string, since: Date, limit?: number): Promise<ChannelMessage[]>;
  /** 可选:列出 Bot 加入的所有会话,用于历史抓取时发现需要轮询的会话。 */
  listChats?(): Promise<Array<{ chatId: string; chatType: 'dm' | 'group' }>>;
}

ReadReceipt 类型

typescript
interface ReadReceipt {
  channelType: string;   // 'feishu'
  messageId: string;     // 被阅读的消息 ID
  readerId: string;      // 阅读者用户 ID
  chatId: string;        // 会话 ID
  readTime: string;      // 时间戳(毫秒级 epoch)
}

ImageAttachment 类型

typescript
interface ImageAttachment {
  mimeType: string;    // 如 'image/png'、'image/jpeg'、'image/webp'
  data: Buffer;        // 原始图片字节数据
  fileName?: string;   // 原始文件名(如果有)
}

用于 ChannelMessage.imagesassistant.chat() 的 opts 参数。全部 6 个内置 Adapter 在用户发送图片消息时都会填充此字段。

ChannelMessage 类型

typescript
interface ChannelMessage {
  channelType: string;     // 'feishu' | 'dingtalk' | 'wecom' | 'slack' | 'telegram' | 'discord' | …
  senderId: string;        // 平台上的用户 ID
  senderName?: string;     // 显示名称
  chatId: string;          // 会话/群组 ID
  chatType: 'dm' | 'group';
  text: string;            // 消息文本
  messageId?: string;      // 平台原生消息 ID(用于引用回复)
  images?: ImageAttachment[];  // 图片附件(如果有)
  raw: unknown;            // 原始 SDK 事件对象
  /**
   * 由能通过平台原生方式检测到 @mention 的 Adapter 设置(如 Discord 的 <@userId> token)。
   * 设置后,Gateway 将该消息视为 @mention,无需依赖文本模式匹配。
   */
  mentioned?: boolean;
}

辅助函数

buildSessionKey(msg)

从通道消息生成会话 Key:${channelType}:${chatId}:${senderId}

stripMention(text)

移除消息中的 @ 提及,处理 <at ...>...</at> XML 格式和 @BotName 纯文本格式。

通过 golem.yaml 配置自定义 Adapter

不需要修改框架代码,任何消息源(邮件、GitHub Issue、Cron 触发等)都可以接入 GolemBot。在 golem.yaml 里声明自定义 channel,并用 _adapter 字段指向你的适配器文件或 npm 包:

yaml
name: my-assistant
engine: claude-code

channels:
  # 内置 channel(无需 _adapter)
  slack:
    botToken: ${SLACK_BOT_TOKEN}
    appToken: ${SLACK_APP_TOKEN}

  # 自定义 channel — 本地文件(相对 assistant 目录解析)
  my-email:
    _adapter: ./adapters/email-adapter.js
    host: imap.gmail.com
    token: ${EMAIL_TOKEN}

  # 自定义 channel — npm 包
  my-teams:
    _adapter: golembot-teams-adapter
    tenantId: ${TEAMS_TENANT_ID}
    clientSecret: ${TEAMS_CLIENT_SECRET}

路径解析规则:

  • ./ 开头 → 相对 assistant 目录解析
  • 其他情况 → 视为 npm 包名,由 Node.js 负责解析

编写 Adapter

Adapter 文件需要 export default 一个实现 ChannelAdapter 接口的类。golem.yaml 中的所有配置字段都会作为构造函数参数传入:

typescript
import type { ChannelAdapter, ChannelMessage } from 'golembot';

export default class EmailAdapter implements ChannelAdapter {
  readonly name: string;
  readonly maxMessageLength = 10000; // 可选,覆盖默认的 4000 字符限制

  constructor(private config: Record<string, unknown>) {
    this.name = (config.channelName as string) ?? 'email';
  }

  async start(onMessage: (msg: ChannelMessage) => void | Promise<void>): Promise<void> {
    // 开始监听(IMAP、Webhook、轮询等)
    // 收到消息时调用 onMessage:
    onMessage({
      channelType: 'email',
      senderId: email.from,
      senderName: email.fromName,
      chatId: email.threadId,
      chatType: 'dm',
      text: email.body,
      raw: email,
    });
  }

  async reply(msg: ChannelMessage, text: string): Promise<void> {
    // 发送回复(SMTP、API 调用等)
  }

  async stop(): Promise<void> {
    // 清理连接资源
  }

  // 可选:在 AI 处理期间发送"正在输入…"指示器
  async typing(msg: ChannelMessage): Promise<void> {
    await this.client.sendTyping(msg.chatId).catch(() => {});
  }
}

Adapter 加载后,GolemBot 自动处理消息路由、Session 管理和长消息分割,无需额外配置。

在代码中手动集成 Adapter

如果你是在自己的应用里嵌入 GolemBot,也可以不通过 golem.yaml,直接实现接口并配合 createAssistant() 使用:

typescript
import type { ChannelAdapter, ChannelMessage } from 'golembot';
import { createAssistant, buildSessionKey, stripMention } from 'golembot';

class MyAdapter implements ChannelAdapter {
  readonly name = 'my-channel';

  async start(onMessage: (msg: ChannelMessage) => void | Promise<void>) {
    // 连接并监听,收到消息时调用 onMessage
  }

  async reply(msg: ChannelMessage, text: string) {
    // 发送回复
  }

  async stop() { /* 断开连接 */ }
}

const assistant = createAssistant({ dir: './my-bot' });
const adapter = new MyAdapter();

await adapter.start(async (msg) => {
  let reply = '';
  for await (const ev of assistant.chat(stripMention(msg.text), {
    sessionKey: buildSessionKey(msg),
  })) {
    if (ev.type === 'text') reply += ev.content;
  }
  await adapter.reply(msg, reply);
});

内置 Adapter

AdapterChannel 类型SDK
FeishuAdapterfeishu@larksuiteoapi/node-sdk
DingtalkAdapterdingtalkdingtalk-stream
WecomAdapterwecom@wecom/aibot-node-sdk
SlackAdapterslack@slack/bolt
TelegramAdaptertelegramgrammy
DiscordAdapterdiscorddiscord.js

内置 Adapter 由 gateway 服务内部使用。在 golem.yaml 里配置对应的 channel 类型即可,无需写 _adapter 字段。

历史抓取支持

AdapterfetchHistorylistChats说明
Feishuim.v1.message.list + im.v1.chat.list
Slackconversations.history + conversations.list
Discordchannel.messages.fetch + guilds.cache
TelegramBot API 无历史消息接口
DingTalk暂未实现
WeCom暂未实现

未实现这些方法的 Adapter 会被历史抓取器静默跳过。

基于 MIT 协议 发布。