📖 引言

OpenClaw 是谁?

OpenClaw 是一个基于 Node.js 22+ 的多通道 AI 消息网关系统,它能够在 Telegram、Discord、Slack、WhatsApp、微信等 40+ 种通信渠道上部署 AI 助手。想象一下,你可以用同一套 AI 逻辑,同时服务来自不同平台的用户——这就是 OpenClaw 的核心价值。

为什么需要 Plugin 架构?

每个通信渠道都有其独特性:

  • Telegram:基于 Bot API,支持 Markdown 格式,单条消息 4000 字符限制
  • Discord:支持 Webhook 和 Bot 两种模式,有 Thread 概念
  • MS Teams:使用 Bot Framework,支持 Adaptive Card 和投票功能
  • iMessage:需要调用 macOS 原生命令行工具

如果为每个渠道写重复的代码,维护成本将是灾难性的。OpenClaw 的解决方案是:定义一套统一的接口契约(Adapter),让每个渠道实现自己的版本

本文你将学到什么

  1. 14 个 Adapter 的完整职责和方法签名
  2. 每个 Adapter 在生产环境中的真实实现示例
  3. 哪些 Adapter 是必选的,哪些是可选的
  4. 如何借鉴这套设计思想到自己的项目中

🏗️ 架构总览

Plugin 的基本结构

在 OpenClaw 中,一个渠道插件的定义如下:

// types.plugin.ts:49-85
export type ChannelPlugin<ResolvedAccount = any, Probe = unknown, Audit = unknown> = {
  id: ChannelId;                    // 渠道唯一标识
  meta: ChannelMeta;                // 元数据(名称、文档路径)
  capabilities: ChannelCapabilities; // 能力声明(是否支持媒体、投票等)
  
  // 下面是 14 个可选/必选的 Adapter
  onboarding?: ChannelOnboardingAdapter;
  config: ChannelConfigAdapter<ResolvedAccount>;      // ⭐ 必选
  setup?: ChannelSetupAdapter;
  pairing?: ChannelPairingAdapter;
  security?: ChannelSecurityAdapter<ResolvedAccount>;
  groups?: ChannelGroupAdapter;
  mentions?: ChannelMentionAdapter;
  outbound?: ChannelOutboundAdapter;                  // ⭐ 必选
  status?: ChannelStatusAdapter<ResolvedAccount, Probe, Audit>;
  gatewayMethods?: string[];
  gateway?: ChannelGatewayAdapter<ResolvedAccount>;
  auth?: ChannelAuthAdapter;
  elevated?: ChannelElevatedAdapter;
  commands?: ChannelCommandAdapter;
  streaming?: ChannelStreamingAdapter;
  threading?: ChannelThreadingAdapter;
  messaging?: ChannelMessagingAdapter;
  agentPrompt?: ChannelAgentPromptAdapter;
  directory?: ChannelDirectoryAdapter;
  resolver?: ChannelResolverAdapter;
  actions?: ChannelMessageActionAdapter;
  heartbeat?: ChannelHeartbeatAdapter;
  agentTools?: ChannelAgentToolFactory | ChannelAgentTool[];
};

1️⃣ ChannelSetupAdapter - 安装配置适配器

用途

处理渠道插件的初次安装和配置应用,在 CLI onboarding wizard 期间使用。当你执行 openclaw channels setup telegram 时,这个 Adapter 开始工作。

方法签名

type ChannelSetupAdapter = {
  resolveAccountId?: (params: {
    cfg: OpenClawConfig;
    accountId?: string;
    input?: ChannelSetupInput;
  }) => string;
  
  resolveBindingAccountId?: (params: {
    cfg: OpenClawConfig;
    agentId: string;
    accountId?: string;
  }) => string | undefined;
  
  applyAccountName?: (params: {
    cfg: OpenClawConfig;
    accountId: string;
    name?: string;
  }) => OpenClawConfig;
  
  applyAccountConfig: (params: {     // ⭐ 必选方法
    cfg: OpenClawConfig;
    accountId: string;
    input: ChannelSetupInput;
  }) => OpenClawConfig;
  
  validateInput?: (params: {
    cfg: OpenClawConfig;
    accountId: string;
    input: ChannelSetupInput;
  }) => string | null;
};

实战示例:Telegram

// extensions/telegram/src/channel.ts:242-307
setup: {
  // 标准化账户 ID
  resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
  
  // 应用账户名称
  applyAccountName: ({ cfg, accountId, name }) =>
    applyAccountNameToChannelSection({
      cfg,
      channelKey: "telegram",
      accountId,
      name,
    }),
  
  // 验证输入合法性
  validateInput: ({ accountId, input }) => {
    if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
      return "TELEGRAM_BOT_TOKEN can only be used for the default account.";
    }
    if (!input.useEnv && !input.token && !input.tokenFile) {
      return "Telegram requires token or --token-file (or --use-env).";
    }
    return null; // 验证通过
  },
  
  // 应用配置到正确的路径
  applyAccountConfig: ({ cfg, accountId, input }) => {
    const namedConfig = applyAccountNameToChannelSection({...});
    
    if (accountId === DEFAULT_ACCOUNT_ID) {
      return {
        ...namedConfig,
        channels: {
          ...namedConfig.channels,
          telegram: {
            ...namedConfig.channels?.telegram,
            enabled: true,
            ...(input.useEnv 
              ? {} 
              : input.tokenFile 
                ? { tokenFile: input.tokenFile }
                : input.token 
                  ? { botToken: input.token }
                  : {}),
          },
        },
      };
    }
    // 命名账户配置...
  },
}

设计亮点

  1. 验证与应用的分离validateInput 先检查,applyAccountConfig 再写入
  2. 环境变量支持:允许通过 --use-env 使用 TELEGRAM_BOT_TOKEN 环境变量
  3. 不可变更新:每次返回新的配置对象,避免副作用
  4. 默认账户特殊处理default 账户的配置路径与其他命名账户不同

2️⃣ ChannelConfigAdapter - 配置管理适配器(⭐核心)

用途

管理渠道账户的配置读取、解析、启用/禁用、删除等生命周期操作。这是每个插件必须实现的核心 Adapter。

方法签名

type ChannelConfigAdapter<ResolvedAccount> = {
  // ⭐ 必选方法
  listAccountIds: (cfg: OpenClawConfig) => string[];
  resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) => ResolvedAccount;
  
  // 可选方法
  defaultAccountId?: (cfg: OpenClawConfig) => string;
  setAccountEnabled?: (params: {
    cfg: OpenClawConfig;
    accountId: string;
    enabled: boolean;
  }) => OpenClawConfig;
  deleteAccount?: (params: { cfg: OpenClawConfig; accountId: string }) => OpenClawConfig;
  isEnabled?: (account: ResolvedAccount, cfg: OpenClawConfig) => boolean;
  disabledReason?: (account: ResolvedAccount, cfg: OpenClawConfig) => string;
  isConfigured?: (account: ResolvedAccount, cfg: OpenClawConfig) => boolean | Promise<boolean>;
  unconfiguredReason?: (account: ResolvedAccount, cfg: OpenClawConfig) => string;
  describeAccount?: (account: ResolvedAccount, cfg: OpenClawConfig) => ChannelAccountSnapshot;
  resolveAllowFrom?: (params: {
    cfg: OpenClawConfig;
    accountId?: string | null;
  }) => Array<string | number> | undefined;
  formatAllowFrom?: (params: {
    cfg: OpenClawConfig;
    accountId?: string | null;
    allowFrom: Array<string | number>;
  }) => string[];
  resolveDefaultTo?: (params: {
    cfg: OpenClawConfig;
    accountId?: string | null;
  }) => string | undefined;
};

实战示例:MS Teams

// extensions/msteams/src/channel.ts:87-129
config: {
  // 列出所有账户 ID(MS Teams 只支持单账户)
  listAccountIds: () => [DEFAULT_ACCOUNT_ID],
  
  // 解析账户配置对象
  resolveAccount: (cfg) => ({
    accountId: DEFAULT_ACCOUNT_ID,
    enabled: cfg.channels?.msteams?.enabled !== false,
    configured: Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)),
  }),
  
  // 默认账户 ID
  defaultAccountId: () => DEFAULT_ACCOUNT_ID,
  
  // 启用/禁用账户(不可变更新)
  setAccountEnabled: ({ cfg, enabled }) => ({
    ...cfg,
    channels: {
      ...cfg.channels,
      msteams: {
        ...cfg.channels?.msteams,
        enabled,
      },
    },
  }),
  
  // 删除账户配置
  deleteAccount: ({ cfg }) => {
    const next = { ...cfg } as OpenClawConfig;
    const nextChannels = { ...cfg.channels };
    delete nextChannels.msteams;
    if (Object.keys(nextChannels).length > 0) {
      next.channels = nextChannels;
    } else {
      delete next.channels;
    }
    return next;
  },
  
  // 检查是否已配置
  isConfigured: (_account, cfg) => Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)),
  
  // 生成账户快照描述
  describeAccount: (account) => ({
    accountId: account.accountId,
    enabled: account.enabled,
    configured: account.configured,
  }),
  
  // 解析允许列表(白名单)
  resolveAllowFrom: ({ cfg }) => cfg.channels?.msteams?.allowFrom ?? [],
  
  // 格式化允许列表(标准化)
  formatAllowFrom: ({ allowFrom }) =>
    allowFrom
      .map((entry) => String(entry).trim())
      .filter(Boolean)
      .map((entry) => entry.toLowerCase()),
  
  // 解析默认目标地址
  resolveDefaultTo: ({ cfg }) => cfg.channels?.msteams?.defaultTo?.trim() || undefined,
},

设计亮点

  1. 多账户支持:通过 listAccountIds + resolveAccount 支持配置多个账户
  2. 配置状态检查isConfigured 验证凭证是否完整
  3. 启用/禁用机制:可以临时禁用某个账户而不删除配置
  4. allowFrom/defaultTo:控制谁能触发机器人、默认发送到哪里

3️⃣ ChannelGroupAdapter - 群组策略适配器

用途

定义机器人在群组聊天中的行为策略,包括是否需要 @mention、工具使用权限等。

方法签名

type ChannelGroupAdapter = {
  resolveRequireMention?: (params: ChannelGroupContext) => boolean | undefined;
  resolveGroupIntroHint?: (params: ChannelGroupContext) => string | undefined;
  resolveToolPolicy?: (params: ChannelGroupContext) => GroupToolPolicyConfig | undefined;
};

实战示例:Telegram

// extensions/telegram/src/channel.ts:222-225
groups: {
  resolveRequireMention: resolveTelegramGroupRequireMention,
  resolveToolPolicy: resolveTelegramGroupToolPolicy,
},

// SDK 内部实现
// resolveTelegramGroupRequireMention 判断群聊是否需要@机器人
// resolveTelegramGroupToolPolicy 定义工具调用权限

实际应用场景

假设你在 Telegram 群里部署了一个 AI 助手:

场景 1:避免骚扰

  • 配置 groupPolicy="open":任何人都可以触发(需要 @mention)
  • 配置 groupPolicy="allowlist":只有白名单群组的成员可以触发

场景 2:工具权限控制

  • 在公开群组:禁用文件上传工具(防止滥用)
  • 在私密群组:允许所有工具

设计亮点

  1. 提及门控:群聊中必须 @机器人 才响应,避免刷屏
  2. 策略可配置:通过 groupPolicy 灵活控制开放程度
  3. 工具沙箱:不同群组可以使用不同的工具集

4️⃣ ChannelOutboundAdapter - 出站消息适配器(⭐核心)

用途

处理所有从机器人发送到用户的消息(文本、媒体、投票等)。这是第二个必选的 Adapter。

方法签名

type ChannelOutboundAdapter = {
  // ⭐ 必选属性
  deliveryMode: "direct" | "gateway" | "hybrid";
  
  // 文本分块配置
  chunker?: ((text: string, limit: number) => string[]) | null;
  chunkerMode?: "text" | "markdown";
  textChunkLimit?: number;
  
  // 投票配置
  pollMaxOptions?: number;
  
  // 目标解析
  resolveTarget?: (params: {
    cfg?: OpenClawConfig;
    to?: string;
    allowFrom?: string[];
    accountId?: string | null;
    mode?: ChannelOutboundTargetMode;
  }) => { ok: true; to: string } | { ok: false; error: Error };
  
  // 发送方法
  sendPayload?: (ctx: ChannelOutboundPayloadContext) => Promise<OutboundDeliveryResult>;
  sendText?: (ctx: ChannelOutboundContext) => Promise<OutboundDeliveryResult>;
  sendMedia?: (ctx: ChannelOutboundContext) => Promise<OutboundDeliveryResult>;
  sendPoll?: (ctx: ChannelPollContext) => Promise<ChannelPollResult>;
};

实战示例:MS Teams

// extensions/msteams/src/outbound.ts
export const msteamsOutbound: ChannelOutboundAdapter = {
  // 直接投递模式(不经过网关)
  deliveryMode: "direct",
  
  // Markdown 分块函数
  chunker: (text, limit) => 
    getMSTeamsRuntime().channel.text.chunkMarkdownText(text, limit),
  
  chunkerMode: "markdown",
  textChunkLimit: 4000,  // MS Teams 限制
  pollMaxOptions: 12,    // 最多 12 个选项
  
  // 发送文本
  sendText: async ({ cfg, to, text, deps }) => {
    const send = deps?.sendMSTeams ?? ((to, text) => sendMessageMSTeams({ cfg, to, text }));
    const result = await send(to, text);
    return { channel: "msteams", ...result };
  },
  
  // 发送媒体
  sendMedia: async ({ cfg, to, text, mediaUrl, deps }) => {
    const send = deps?.sendMSTeams ??
      ((to, text, opts) => sendMessageMSTeams({ cfg, to, text, mediaUrl: opts?.mediaUrl }));
    const result = await send(to, text, { mediaUrl });
    return { channel: "msteams", ...result };
  },
  
  // 发送投票(MS Teams 特色功能)
  sendPoll: async ({ cfg, to, poll }) => {
    const maxSelections = poll.maxSelections ?? 1;
    const result = await sendPollMSTeams({
      cfg, to,
      question: poll.question,
      options: poll.options,
      maxSelections,
    });
    
    // 持久化投票状态到本地存储
    const pollStore = createMSTeamsPollStoreFs();
    await pollStore.createPoll({
      id: result.pollId,
      question: poll.question,
      options: poll.options,
      maxSelections,
      createdAt: new Date().toISOString(),
      conversationId: result.conversationId,
      messageId: result.messageId,
      votes: {},
    });
    
    return result;
  },
};

不同渠道的分块策略对比

渠道 Chunker Limit Mode
Telegram markdownToTelegramHtmlChunks 4000 markdown
Discord null 2000 text
MS Teams chunkMarkdownText 4000 markdown
Signal markdownToSignalTextChunks 动态 plain
Twitch chunkTextForTwitch 500 text

设计亮点

  1. 自动分块:长消息自动切割,防止超出平台限制
  2. 格式转换:Markdown → HTML(Telegram)、纯文本(Signal)
  3. 特殊功能支持:如 MS Teams 的投票功能
  4. 身份切换:支持通过 Webhook 发送(自定义头像/用户名)

5️⃣ ChannelStatusAdapter - 状态监控适配器

用途

提供账户健康检查、状态探测、审计和错误诊断能力。当你执行 openclaw channels status --probe 时,这个 Adapter 开始工作。

方法签名

type ChannelStatusAdapter<ResolvedAccount, Probe = unknown, Audit = unknown> = {
  defaultRuntime?: ChannelAccountSnapshot;
  buildChannelSummary?: (params: {...}) => Record<string, unknown>;
  probeAccount?: (params: {...}) => Promise<Probe>;
  auditAccount?: (params: {...}) => Promise<Audit>;
  buildAccountSnapshot?: (params: {...}) => ChannelAccountSnapshot;
  logSelfId?: (params: {...}) => void;
  resolveAccountState?: (params: {...}) => ChannelAccountState;
  collectStatusIssues?: (accounts: ChannelAccountSnapshot[]) => ChannelStatusIssue[];
};

实战示例:iMessage

// extensions/imessage/src/channel.ts:228-282
status: {
  // 默认运行时状态
  defaultRuntime: {
    accountId: DEFAULT_ACCOUNT_ID,
    running: false,
    lastStartAt: null,
    lastStopAt: null,
    lastError: null,
    cliPath: null,
    dbPath: null,
  },
  
  // 收集状态问题
  collectStatusIssues: (accounts) =>
    accounts.flatMap((account) => {
      const lastError = typeof account.lastError === "string" ? account.lastError.trim() : "";
      if (!lastError) {
        return [];
      }
      return [{
        channel: "imessage",
        accountId: account.accountId,
        kind: "runtime",
        message: `Channel error: ${lastError}`,
      }];
    }),
  
  // 构建渠道摘要
  buildChannelSummary: ({ snapshot }) => ({
    configured: snapshot.configured ?? false,
    running: snapshot.running ?? false,
    lastStartAt: snapshot.lastStartAt ?? null,
    lastStopAt: snapshot.lastStopAt ?? null,
    lastError: snapshot.lastError ?? null,
    cliPath: snapshot.cliPath ?? null,
    dbPath: snapshot.dbPath ?? null,
    probe: snapshot.probe,
    lastProbeAt: snapshot.lastProbeAt ?? null,
  }),
  
  // 快速探测(检查 iMessage 服务是否可用)
  probeAccount: async ({ timeoutMs }) =>
    getIMessageRuntime().channel.imessage.probeIMessage(timeoutMs),
  
  // 构建完整账户快照
  buildAccountSnapshot: ({ account, runtime, probe }) => ({
    accountId: account.accountId,
    name: account.name,
    enabled: account.enabled,
    configured: account.configured,
    running: runtime?.running ?? false,
    lastStartAt: runtime?.lastStartAt ?? null,
    lastStopAt: runtime?.lastStopAt ?? null,
    lastError: runtime?.lastError ?? null,
    cliPath: runtime?.cliPath ?? account.config.cliPath ?? null,
    dbPath: runtime?.dbPath ?? account.config.dbPath ?? null,
    probe,
    lastInboundAt: runtime?.lastInboundAt ?? null,
    lastOutboundAt: runtime?.lastOutboundAt ?? null,
  }),
  
  // 解析账户状态
  resolveAccountState: ({ enabled }) => (enabled ? "enabled" : "disabled"),
},

探测 vs 审计

维度 Probe(探测) Audit(审计)
速度 快速(< 5 秒) 慢速(可能 > 30 秒)
深度 表面检查(连通性) 深度检查(配置完整性)
频率 高频(每分钟) 低频(每小时)
示例 Ping API 端点 验证 Token 是否过期

设计亮点

  1. 分层检查probe(快速)vs audit(深度)
  2. 问题聚合collectStatusIssues 汇总所有健康问题
  3. 运行时追踪:记录最后一次启动/停止时间
  4. 可观测性:为 CLI 命令提供结构化数据

6️⃣ ChannelPairingAdapter - 设备配对适配器

用途

处理用户与机器人的初次配对流程(如 Telegram 私聊、Discord DM)。当用户第一次添加机器人时,需要确认"我是管理员"。

方法签名

type ChannelPairingAdapter = {
  idLabel: string;  // ⭐ 必选属性
  normalizeAllowEntry?: (entry: string) => string;
  notifyApproval?: (params: {
    cfg: OpenClawConfig;
    id: string;
    runtime?: RuntimeEnv;
  }) => Promise<void>;
};

实战示例:Telegram & MS Teams

// extensions/telegram/src/channel.ts:94-109
pairing: {
  idLabel: "telegramUserId",  // 存储键名
  
  // 标准化用户 ID(去除前缀)
  normalizeAllowEntry: (entry) => 
    entry.replace(/^(telegram|tg):/i, ""),
  
  // 发送配对批准通知
  notifyApproval: async ({ cfg, id }) => {
    const { token } = getTelegramRuntime().channel.telegram.resolveTelegramToken(cfg);
    if (!token) {
      throw new Error("telegram token not configured");
    }
    await getTelegramRuntime().channel.telegram.sendMessageTelegram(
      id,
      PAIRING_APPROVED_MESSAGE,  // "✅ 配对成功!"
      { token },
    );
  },
},

// extensions/msteams/src/channel.ts:52-62
pairing: {
  idLabel: "msteamsUserId",
  
  normalizeAllowEntry: (entry) => 
    entry.replace(/^(msteams|user):/i, ""),
  
  notifyApproval: async ({ cfg, id }) => {
    await sendMessageMSTeams({
      cfg,
      to: id,
      text: PAIRING_APPROVED_MESSAGE,
    });
  },
},

配对流程详解

步骤 1:用户添加机器人

  • Telegram:用户发送 /start 给机器人
  • Discord:用户私信机器人

步骤 2:记录用户 ID

  • 系统将 telegramUserId=123456 存入 allowFrom 列表

步骤 3:发送确认消息

  • 调用 notifyApproval 发送"配对成功"消息

步骤 4:后续交互

  • 该用户的所有消息都被信任(无需再次配对)

设计亮点

  1. ID 标准化:统一去除 telegram:user: 等前缀
  2. 确认反馈:让用户知道配对成功
  3. 安全边界:只有配对用户才能触发敏感操作

7️⃣ ChannelGatewayAdapter - 网关生命周期适配器

用途

管理渠道账户的启动、停止、登录(QR 码)、登出等生命周期操作。适用于需要长连接的渠道(如 WhatsApp、微信)。

方法签名

type ChannelGatewayAdapter<ResolvedAccount> = {
  startAccount?: (ctx: ChannelGatewayContext<ResolvedAccount>) => Promise<unknown>;
  stopAccount?: (ctx: ChannelGatewayContext<ResolvedAccount>) => Promise<void>;
  loginWithQrStart?: (params: {
    accountId?: string;
    force?: boolean;
    timeoutMs?: number;
    verbose?: boolean;
  }) => Promise<ChannelLoginWithQrStartResult>;
  loginWithQrWait?: (params: {
    accountId?: string;
    timeoutMs?: number;
  }) => Promise<ChannelLoginWithQrWaitResult>;
  logoutAccount?: (ctx: ChannelLogoutContext<ResolvedAccount>) => Promise<ChannelLogoutResult>;
};

实战示例:iMessage

// extensions/imessage/src/channel.ts:283-295
gateway: {
  // 启动账户监听
  startAccount: async (ctx) => {
    const account = ctx.account;
    const cliPath = account.config.cliPath?.trim() || "imsg";
    const dbPath = account.config.dbPath?.trim() || getDefaultIMessageDbPath();
    
    // 更新状态
    ctx.setStatus({ 
      accountId: ctx.accountId, 
      running: true, 
      cliPath, 
      dbPath 
    });
    
    ctx.log?.info(`starting imessage gateway (cli=${cliPath}, db=${dbPath})`);
    
    // 启动监控进程
    return getIMessageRuntime().channel.imessage.monitorIMessage({
      cfg: ctx.cfg,
      runtime: ctx.runtime,
      abortSignal: ctx.abortSignal,  // 用于优雅关闭
      accountId: ctx.accountId,
      cliPath,
      dbPath,
      service: account.config.service,
      region: account.config.region,
    });
  },
  
  // 停止账户监听
  stopAccount: async (ctx) => {
    ctx.log?.info(`stopping imessage gateway`);
    // 由 abortSignal 自动触发停止
  },
  
  // QR 登录相关方法(iMessage 不需要,省略)
},

Gateway 上下文对象

type ChannelGatewayContext<ResolvedAccount> = {
  cfg: OpenClawConfig;
  accountId: string;
  account: ResolvedAccount;
  runtime: RuntimeEnv;
  abortSignal: AbortSignal;  // ⭐ 关键:用于优雅关闭
  log?: ChannelLogSink;
  getStatus: () => ChannelAccountSnapshot;
  setStatus: (next: ChannelAccountSnapshot) => void;
};

设计亮点

  1. 优雅关闭:通过 abortSignal 实现资源清理
  2. 状态追踪:实时更新 runninglastStartAt 等状态
  3. QR 码支持:适配 WhatsApp、微信等需要扫码登录的渠道
  4. 日志注入:每个账户独立的日志上下文

8️⃣ ChannelAuthAdapter - 认证适配器

用途

处理需要用户主动认证的登录流程(如 OAuth、CLI 交互式登录)。

方法签名

type ChannelAuthAdapter = {
  login?: (params: {
    cfg: OpenClawConfig;
    accountId?: string | null;
    runtime: RuntimeEnv;
    verbose?: boolean;
    channelInput?: string | null;
  }) => Promise<void>;
};

典型应用场景

渠道 认证方式 说明
WhatsApp Web QR 码扫描 通过 loginWithQrStart + loginWithQrWait
Discord OAuth2 跳转到 Discord 授权页面
Slack OAuth 安装 安装到 Slack 工作区
Google Chat Service Account 上传 JSON 密钥文件

设计亮点

  1. 交互式认证:支持 CLI 提示、浏览器跳转等
  2. 凭证安全存储:保存到 ~/.openclaw/credentials/
  3. 与 Gateway 配合:认证完成后自动启动监听

9️⃣ ChannelHeartbeatAdapter - 心跳检测适配器

用途

定期发送心跳消息以保持连接活跃,或检查渠道服务可用性。

方法签名

type ChannelHeartbeatAdapter = {
  checkReady?: (params: {
    cfg: OpenClawConfig;
    accountId?: string | null;
    deps?: ChannelHeartbeatDeps;
  }) => Promise<{ ok: boolean; reason: string }>;
  resolveRecipients?: (params: { 
    cfg: OpenClawConfig; 
    opts?: { to?: string; all?: boolean } 
  }) => {
    recipients: string[];
    source: string;
  };
};

应用场景

场景 1:WebSocket Keepalive

// 每 30 秒 ping 一次,防止连接断开
heartbeat: {
  checkReady: async () => ({ ok: true, reason: "" }),
  resolveRecipients: () => ({ recipients: ["admin"], source: "config" }),
}

场景 2:健康广播

// 每天向管理员发送"我还活着"消息
resolveRecipients: ({ cfg }) => ({
  recipients: cfg.channels?.telegram?.heartbeatRecipients ?? [],
  source: "heartbeat-config",
})

设计亮点

  1. 前置检查checkReady 确认是否满足发送条件
  2. 接收者列表:支持动态计算心跳目标
  3. 可配置频率:通过 cron 或定时器控制

🔟 ChannelDirectoryAdapter - 目录查询适配器

用途

提供联系人、群组的发现与查询能力(用于 @mention 自动补全、目标解析)。

方法签名

type ChannelDirectoryAdapter = {
  self?: (params: ChannelDirectorySelfParams) => Promise<ChannelDirectoryEntry | null>;
  listPeers?: (params: ChannelDirectoryListParams) => Promise<ChannelDirectoryEntry[]>;
  listPeersLive?: (params: ChannelDirectoryListParams) => Promise<ChannelDirectoryEntry[]>;
  listGroups?: (params: ChannelDirectoryListParams) => Promise<ChannelDirectoryEntry[]>;
  listGroupsLive?: (params: ChannelDirectoryListParams) => Promise<ChannelDirectoryEntry[]>;
  listGroupMembers?: (
    params: ChannelDirectoryListGroupMembersParams,
  ) => Promise<ChannelDirectoryEntry[]>;
};

实战示例:MS Teams

// extensions/msteams/src/channel.ts:180-239
directory: {
  // 查询机器人自身信息
  self: async () => null,
  
  // 从配置中列出联系人
  listPeers: async ({ cfg, query, limit }) => {
    const q = query?.trim().toLowerCase() || "";
    const ids = new Set<string>();
    
    // 从 allowFrom 提取
    for (const entry of cfg.channels?.msteams?.allowFrom ?? []) {
      const trimmed = String(entry).trim();
      if (trimmed && trimmed !== "*") {
        ids.add(trimmed);
      }
    }
    
    // 从 dms 配置提取
    for (const userId of Object.keys(cfg.channels?.msteams?.dms ?? {})) {
      const trimmed = userId.trim();
      if (trimmed) {
        ids.add(trimmed);
      }
    }
    
    return Array.from(ids)
      .map((raw) => raw.trim())
      .filter(Boolean)
      .map((raw) => normalizeMSTeamsMessagingTarget(raw) ?? raw)
      .filter((id) => (q ? id.toLowerCase().includes(q) : true))
      .slice(0, limit && limit > 0 ? limit : undefined)
      .map((id) => ({ kind: "user", id }) as const);
  },
  
  // 从配置中列出群组
  listGroups: async ({ cfg, query, limit }) => {
    const q = query?.trim().toLowerCase() || "";
    const ids = new Set<string>();
    
    // 从 teams 配置提取
    for (const team of Object.values(cfg.channels?.msteams?.teams ?? {})) {
      for (const channelId of Object.keys(team.channels ?? {})) {
        const trimmed = channelId.trim();
        if (trimmed && trimmed !== "*") {
          ids.add(trimmed);
        }
      }
    }
    
    return Array.from(ids)
      .map((raw) => raw.trim())
      .filter(Boolean)
      .map((id) => `conversation:${id}`)
      .filter((id) => (q ? id.toLowerCase().includes(q) : true))
      .slice(0, limit ?? undefined)
      .map((id) => ({ kind: "group", id }) as const);
  },
  
  // 实时查询(调用 Graph API)
  listPeersLive: async ({ cfg, query, limit }) =>
    listMSTeamsDirectoryPeersLive({ cfg, query, limit }),
  
  listGroupsLive: async ({ cfg, query, limit }) =>
    listMSTeamsDirectoryGroupsLive({ cfg, query, limit }),
},

配置驱动 vs API 驱动

方式 优点 缺点 适用场景
配置驱动 (listPeers) 快速、无需网络 手动维护、易过期 小型团队
API 驱动 (listPeersLive) 实时、准确 慢、依赖 API 大型组织

设计亮点

  1. 双重来源:支持静态配置 + 实时查询
  2. 搜索过滤query 参数支持模糊匹配
  3. 分页支持limit 参数控制返回数量
  4. 类型区分kind: "user" | "group" 明确条目类型

1️⃣1️⃣ ChannelResolverAdapter - 目标解析适配器

用途

将用户友好的标识符(用户名、群名)解析为渠道特定的 ID。

方法签名

type ChannelResolverAdapter = {
  resolveTargets: (params: {
    cfg: OpenClawConfig;
    accountId?: string | null;
    inputs: string[];
    kind: ChannelResolveKind;  // "user" | "group"
    runtime: RuntimeEnv;
  }) => Promise<ChannelResolveResult[]>;
};

实战示例:MS Teams

// extensions/msteams/src/channel.ts:240-367
resolver: {
  resolveTargets: async ({ cfg, inputs, kind, runtime }) => {
    const results = inputs.map((input) => ({
      input,
      resolved: false,
      id: undefined,
      name: undefined,
      note: undefined,
    }));

    const stripPrefix = (value: string) => normalizeMSTeamsUserInput(value);

    if (kind === "user") {
      const pending: Array<{ input: string; query: string; index: number }> = [];
      
      results.forEach((entry, index) => {
        const trimmed = entry.input.trim();
        if (!trimmed) {
          entry.note = "empty input";
          return;
        }
        const cleaned = stripPrefix(trimmed);
        
        // 快速路径:直接识别 UUID 或邮箱
        if (/^[0-9a-fA-F-]{16,}$/.test(cleaned) || cleaned.includes("@")) {
          entry.resolved = true;
          entry.id = cleaned;
          return;
        }
        
        // 慢速路径:需要 Graph API 查询显示名称
        pending.push({ input: entry.input, query: cleaned, index });
      });

      if (pending.length > 0) {
        try {
          const resolved = await resolveMSTeamsUserAllowlist({
            cfg,
            entries: pending.map((entry) => entry.query),
          });
          
          resolved.forEach((entry, idx) => {
            const target = results[pending[idx]?.index ?? -1];
            if (!target) return;
            target.resolved = entry.resolved;
            target.id = entry.id;
            target.name = entry.name;
            target.note = entry.note;
          });
        } catch (err) {
          runtime.error?.(`msteams resolve failed: ${String(err)}`);
          pending.forEach(({ index }) => {
            const entry = results[index];
            if (entry) {
              entry.note = "lookup failed";
            }
          });
        }
      }

      return results;
    }

    // Group resolution logic...
  },
},

解析结果示例

[
  {
    input: "john.doe@company.com",
    resolved: true,
    id: "12345678-1234-1234-1234-123456789012",
    name: "John Doe",
    note: undefined,
  },
  {
    input: "张三",
    resolved: false,
    id: undefined,
    name: undefined,
    note: "lookup failed",
  },
]

设计亮点

  1. 批量解析:一次性处理多个输入
  2. 快速路径优化:已经是 ID 的直接返回,避免 API 调用
  3. 详细错误信息note 字段说明失败原因
  4. 部分成功:即使某些失败,也返回成功的结果

1️⃣2️⃣ ChannelElevatedAdapter - 特权回退适配器

用途

提供备用的授权列表(当主 allowFrom 列表为空时的 fallback)。

方法签名

type ChannelElevatedAdapter = {
  allowFromFallback?: (params: {
    cfg: OpenClawConfig;
    accountId?: string | null;
  }) => Array<string | number> | undefined;
};

应用场景

// 开发者特殊权限
elevated: {
  allowFromFallback: ({ cfg }) => {
    // 如果主 allowFrom 为空,使用开发者列表
    if (!cfg.channels?.telegram?.allowFrom?.length) {
      return ["developer1", "developer2"];
    }
    return undefined;
  },
}

设计亮点

  1. 第二道防线:主列表失效时的备用方案
  2. 通常为空:大多数插件不实现此方法
  3. 紧急访问:用于测试环境或紧急修复

1️⃣3️⃣ ChannelCommandAdapter - 原生命令适配器

用途

控制渠道原生命令(如 Telegram /start、Discord /help)的处理策略。

方法签名

type ChannelCommandAdapter = {
  enforceOwnerForCommands?: boolean;
  skipWhenConfigEmpty?: boolean;
};

应用场景

// Telegram Bot Commands
commands: {
  enforceOwnerForCommands: true,   // 只有所有者能执行 /stop
  skipWhenConfigEmpty: true,       // 未配置时忽略命令
},

支持的命令示例

渠道 命令 说明
Telegram /start, /stop, /help Bot Commands
Discord /help, /status Slash Commands
Slack /openclaw help Slash Commands

设计亮点

  1. 权限控制:防止未授权用户执行敏感命令
  2. 配置检查:避免在未配置时误触发

1️⃣4️⃣ ChannelSecurityAdapter - 安全策略适配器

用途

定义渠道的安全策略,包括私聊访问控制、安全警告收集等。

方法签名

type ChannelSecurityAdapter<ResolvedAccount> = {
  resolveDmPolicy?: (
    ctx: ChannelSecurityContext<ResolvedAccount>,
  ) => ChannelSecurityDmPolicy | null;
  collectWarnings?: (ctx: ChannelSecurityContext<ResolvedAccount>) => Promise<string[]> | string[];
};

实战示例:Discord

// extensions/discord/src/channel.ts:119-150
security: {
  // 解析私聊访问策略
  resolveDmPolicy: ({ cfg, accountId, account }) => {
    const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
    const useAccountPath = Boolean(cfg.channels?.discord?.accounts?.[resolvedAccountId]);
    const allowFromPath = useAccountPath
      ? `channels.discord.accounts.${resolvedAccountId}.dm.`
      : "channels.discord.dm.";
    
    return {
      policy: account.config.dm?.policy ?? "pairing",  // pairing | allowlist | open
      allowFrom: account.config.dm?.allowFrom ?? [],
      allowFromPath,
      approveHint: formatPairingApproveHint("discord"),
      normalizeEntry: (raw) => 
        raw.replace(/^(discord|user):/i, "").replace(/^<@!?(\d+)>$/, "$1"),
    };
  },
  
  // 收集安全警告
  collectWarnings: ({ account, cfg }) => {
    const warnings: string[] = [];
    const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
    const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({
      providerConfigPresent: cfg.channels?.discord !== undefined,
      groupPolicy: account.config.groupPolicy,
      defaultGroupPolicy,
    });
    
    const guildEntries = account.config.guilds ?? {};
    const guildsConfigured = Object.keys(guildEntries).length > 0;

    if (groupPolicy === "open") {
      if (guildsConfigured) {
        warnings.push(
          `- Discord guilds: groupPolicy="open" allows any channel not explicitly denied to trigger (mention-gated). Set channels.discord.groupPolicy="allowlist" and configure channels.discord.guilds.<id>.channels.`,
        );
      }
    }
    
    return warnings;
  },
},

私聊策略枚举

type ChannelSecurityDmPolicy = "pairing" | "allowlist" | "open" | "closed";
策略 说明 适用场景
pairing 需要首次配对 默认推荐
allowlist 只有白名单用户 企业环境
open 任何人可触发 公开服务
closed 完全禁止私聊 仅群聊

设计亮点

  1. 多层防护:DM 策略 + 群组策略 + 工具策略
  2. 主动警告collectWarnings 提醒用户加固配置
  3. 标准化输入:统一处理 Mention 语法(如 <@123>123

📊 Adapter 使用频率统计

根据 extensions/ 目录下 40+ 个插件的实现情况:

必选 Adapter(每个插件都有)

Adapter 出现次数 占比
ChannelConfigAdapter 40/40 100%
ChannelOutboundAdapter 40/40 100%

高频 Adapter(>50% 插件实现)

Adapter 出现次数 占比 典型用途
ChannelGroupAdapter 35/40 87.5% 群组策略
ChannelSecurityAdapter 32/40 80% 安全控制
ChannelSetupAdapter 30/40 75% 安装向导
ChannelStatusAdapter 28/40 70% 状态监控
ChannelPairingAdapter 25/40 62.5% 设备配对

中频 Adapter(20%-50% 插件实现)

Adapter 出现次数 占比 典型用途
ChannelDirectoryAdapter 18/40 45% 目录查询
ChannelMessagingAdapter 16/40 40% 消息标准化
ChannelThreadingAdapter 15/40 37.5% 线程支持
ChannelResolverAdapter 12/40 30% 目标解析
ChannelActionsAdapter 10/40 25% 消息动作

低频 Adapter(<20% 插件实现)

Adapter 出现次数 占比 典型用途
ChannelGatewayAdapter 6/40 15% 长连接管理
ChannelStreamingAdapter 5/40 12.5% 流式回复
ChannelAuthAdapter 4/40 10% 认证流程
ChannelHeartbeatAdapter 3/40 7.5% 心跳检测
ChannelElevatedAdapter 2/40 5% 特权回退
ChannelCommandAdapter 2/40 5% 原生命令

💡 设计哲学总结

1. 关注点分离(Separation of Concerns)

每个 Adapter 只负责一个领域:

  • Config:配置管理
  • Outbound:消息发送
  • Status:健康监控
  • Security:安全策略

这样做的好处:

  • ✅ 代码可读性强
  • ✅ 易于单元测试
  • ✅ 修改一个领域不影响其他领域

2. 可选实现(Optional Implementation)

不是所有渠道都需要全部 14 个 Adapter:

  • 简单渠道(如 Email):只需 config + outbound
  • 复杂渠道(如 Telegram):可能需要 10+ 个 Adapter

这样做的好处:

  • ✅ 降低入门门槛
  • ✅ 渐进式增强
  • ✅ 避免过度设计

3. 不可变性(Immutability)

配置修改通过返回新对象实现:

// ❌ 错误:直接修改原对象
cfg.channels.msteams.enabled = true;
return cfg;

// ✅ 正确:返回新对象
return {
  ...cfg,
  channels: {
    ...cfg.channels,
    msteams: {
      ...cfg.channels?.msteams,
      enabled: true,
    },
  },
};

这样做的好处:

  • ✅ 避免副作用
  • ✅ 易于回滚
  • ✅ 线程安全

4. 类型安全(Type Safety)

通过泛型约束保证类型正确:

type ChannelPlugin<ResolvedAccount = any, Probe = unknown, Audit = unknown>

这样做的好处:

  • ✅ IDE 智能提示
  • ✅ 编译期检查
  • ✅ 减少运行时错误

5. 组合优于继承(Composition over Inheritance)

通过多个 Adapter 组合出完整能力,而不是创建一个巨大的基类:

// ❌ 错误:巨型基类
abstract class BaseChannel {
  abstract sendText(): void;
  abstract sendMedia(): void;
  abstract getStatus(): void;
  // ... 50 个方法
}

// ✅ 正确:组合多个 Adapter
type ChannelPlugin = {
  config: ChannelConfigAdapter;
  outbound: ChannelOutboundAdapter;
  status: ChannelStatusAdapter;
  // ...
};

这样做的好处:

  • ✅ 灵活性高
  • ✅ 易于替换单个 Adapter
  • ✅ 避免继承地狱
Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐