乐于分享
好东西不私藏

OpenClaw源码解析(六):Channels渠道插件是如何设计的

OpenClaw源码解析(六):Channels渠道插件是如何设计的

核心文件:– src/channels/plugins/types.plugin.ts– src/channels/plugins/types.core.ts– src/channels/plugins/types.adapters.ts


这一篇怎么读

正确顺序是:

  1. 先看 types.core.ts
  2. 再看 types.adapters.ts
  3. 最后回到 types.plugin.ts
  • types.core.ts 定义“渠道里有哪些基本对象”
  • types.adapters.ts 定义“这些对象有哪些行为钩子”
  • types.plugin.ts 把前两层装配成一个总接口

这篇要回答的问题是:

OpenClaw 是如何在不写满 if-else 的前提下,统一接入 20+ 消息渠道的?


写作最前面

OpenClaw 的渠道系统不是“一个大接口 + 每个平台全量实现”。

它更接近下面这个结构:

ChannelPlugin├─ identity/meta/capabilities├─ config├─ gateway├─ outbound├─ status├─ security├─ threading├─ messaging└─ ... 一系列按需实现的 adapter

这意味着:

  • 只有少数字段是必填
  • 大部分能力都是可选 adapter
  • 每个平台只需要实现自己真正支持的那部分

这就是 OpenClaw 能同时支持很多平台、又不把核心层写爆的关键。


第 1 层:types.core.ts 先定义“渠道领域对象”

先看 src/channels/plugins/types.core.ts

这里的类型是在定义:

  • 渠道是什么
  • 账户状态长什么样
  • 能力如何声明
  • 消息动作、线程、目录这些对象怎么表达

第 1 段:ChannelId

export type ChannelId = ChatChannelId | (string & {});

它说明:

  • core 里存在一组已知 ChatChannelId
  • 但系统仍允许扩展渠道使用新的字符串 id

所以渠道系统从类型层面就不是封闭枚举,而是开放扩展的。

这和后面的插件体系是完全一致的。


第 2 段:ChannelMeta

export type ChannelMeta = {  idChannelId;  labelstring;  selectionLabelstring;  docsPathstring;  docsLabel?: string;  blurbstring;  order?: number;  aliases?: string[];  ...};

它承担了几个职责:

  • UI 展示
  • CLI/选择器显示
  • 文档跳转
  • 别名和排序
  • 某些平台偏好表达

所以一个渠道插件首先要回答的不是“怎么连 API”,而是:

我是谁?在系统里怎么被展示和选择?


第 3 段:ChannelAccountSnapshot

export type ChannelAccountSnapshot = {  accountIdstring;  name?: string;  enabled?: boolean;  configured?: boolean;  linked?: boolean;  running?: boolean;  connected?: boolean;  ...};

这个类型非常重要,因为它揭示了 OpenClaw 的状态模型不是布尔类型。

一个渠道账户可能同时处于不同维度的状态:

  • 是否配置完成
  • 是否启用
  • 是否已连接
  • 是否正在运行
  • 最近错误是什么
  • 最近事件是什么时候

这会直接影响:

  • status 命令
  • 健康检查
  • 渠道运维体验

第 4 段:ChannelCapabilities

export type ChannelCapabilities = {  chatTypesArray<ChatType | "thread">;  polls?: boolean;  reactions?: boolean;  edit?: boolean;  unsend?: boolean;  reply?: boolean;  effects?: boolean;  groupManagement?: boolean;  threads?: boolean;  media?: boolean;  nativeCommands?: boolean;  blockStreaming?: boolean;};

这是渠道抽象里值得学习的设计。

它不是写:

  • if channel === telegram
  • if channel === slack

而是让渠道自己声明能力,再由上层按能力做决策。

这类设计的好处非常大:

  • 新平台加入时不必修改所有上层分支判断
  • 业务逻辑围绕 capability,而不是平台名字
  • 平台差异被压缩成统一的能力表述

所以 ChannelCapabilities 本质上是“平台差异的声明式接口”。


第 5 段:线程、消息动作、目录这些“次级领域对象”

types.core.ts 后半段还定义了很多次级对象,例如:

  • ChannelMentionAdapter 对应的数据上下文
  • ChannelThreadingContext
  • ChannelThreadingToolContext
  • ChannelDirectoryEntry
  • ChannelMessageActionContext
  • ChannelAgentTool

这里值得注意的是:

OpenClaw 并没有把“渠道能力”只理解成收发文本消息。

它还统一抽象了:

  • 线程回复
  • 联系人/群组目录
  • 动作按钮
  • agent 可发现工具

这说明渠道层不是一个 webhook wrapper,而是一个较完整的交互能力层。


第 2 层:types.adapters.ts 定义“行为插槽”

如果说 types.core.ts 定义的是名词,那 types.adapters.ts 定义的就是动词。

这里每个 adapter 都代表一块独立行为面,例如:

  • 配置
  • setup
  • outbound
  • status
  • gateway
  • auth
  • heartbeat
  • directory
  • resolver
  • security

理解这一层时,最重要的原则是:

一个渠道不是实现一个大而全类,而是按需提供多个小行为模块。


第 6 段:ChannelConfigAdapter 是真正的必经之路

export type ChannelConfigAdapter<ResolvedAccount> = {  listAccountIds(cfgOpenClawConfig) => string[];  resolveAccount(cfgOpenClawConfigaccountId?: string | null) => ResolvedAccount;  ...};

这层为什么重要?

因为系统无论做什么,几乎都得先知道:

  • 这个渠道有哪些账户
  • 当前账户配置如何解析成运行时对象

所以在 ChannelPlugin 顶层里,config 是少数必填字段之一。

这也解释了为什么 ResolvedAccount 这个泛型存在:

  • 各平台配置长得完全不同
  • 但核心层只要求“你能把配置解析成一个可用账户对象”

这是一种非常实用的抽象:

  • 保留平台自由度
  • 统一核心调用入口

第 7 段:ChannelSetupAdapter

export type ChannelSetupAdapter = {  resolveAccountId?: ...  resolveBindingAccountId?: ...  applyAccountName?: ...  applyAccountConfig: ...  validateInput?: ...};

这一层处理的是“初始化配置写入”。

也就是说,渠道插件不只是运行时适配器,还参与:

  • 初始账户建立
  • 配置写入
  • 输入校验

这意味着 onboarding / configure 等 CLI 流程并不是写死平台细节,而是可以调用渠道自己的 setup 能力。


第 8 段:ChannelOutboundAdapter

export type ChannelOutboundAdapter = {  deliveryMode"direct" | "gateway" | "hybrid";  chunker?: ...  chunkerMode?: "text" | "markdown";  textChunkLimit?: number;  resolveTarget?: ...  sendPayload?: ...  sendText?: ...  sendMedia?: ...  sendPoll?: ...};

这一层是“发消息”抽象,不只是一个 sendText()

里面已经体现出很多真实平台差异:

  • 发送路径可能不同:direct/gateway/hybrid
  • 文本分块规则不同
  • 目标解析规则不同
  • 平台对 poll/media/payload 的支持不同

所以 OpenClaw 的 outbound 层本质上是在做:

统一发送接口 + 保留平台发送策略差异


第 9 段:ChannelStatusAdapter

export type ChannelStatusAdapter<ResolvedAccountProbe = unknownAudit = unknown> = {  defaultRuntime?: ChannelAccountSnapshot;  buildChannelSummary?: ...  probeAccount?: ...  auditAccount?: ...  buildAccountSnapshot?: ...  ...};

这说明“状态检查”不是核心层自己定义,而是交由渠道补充。

为什么需要这样?

因为每个平台判断健康的方法都不一样:

  • 有的平台靠 token 探测
  • 有的平台靠 websocket / long poll 状态
  • 有的平台要做额外 audit

所以 core 只定义状态框架,具体 probe/audit 由渠道给出。


第 10 段:ChannelGatewayAdapter 是运行时入口

export type ChannelGatewayAdapter<ResolvedAccount = unknown> = {  startAccount?: (ctxChannelGatewayContext<ResolvedAccount>) => Promise<unknown>;  stopAccount?: (ctxChannelGatewayContext<ResolvedAccount>) => Promise<void>;  loginWithQrStart?: ...  loginWithQrWait?: ...  logoutAccount?: ...};

这是整个渠道系统最核心的 adapter 之一。

它控制的是:

  • gateway 启动时如何拉起该渠道账户
  • gateway 停止时如何清理
  • 某些平台如何完成二维码登录
  • 如何注销账户

第 11 段:ChannelGatewayContext 的真正意义

export type ChannelGatewayContext<ResolvedAccount = unknown> = {  cfgOpenClawConfig;  accountIdstring;  accountResolvedAccount;  runtimeRuntimeEnv;  abortSignalAbortSignal;  log?: ChannelLogSink;  getStatus() => ChannelAccountSnapshot;  setStatus(nextChannelAccountSnapshot) => void;  channelRuntime?: PluginRuntime["channel"];};

这里至少暴露了 3 个设计意图:

1. 渠道运行时有自己的状态面

通过 getStatus / setStatus,渠道可以维护自身运行态,而不是把状态塞到全局黑盒里。

2. 渠道生命周期受统一 runtime 管理

通过 abortSignal,系统可以优雅关闭渠道,而不是靠每个平台自己处理退出。

3. 外部插件和内置渠道的能力边界不同

channelRuntime?: PluginRuntime[“channel”] 这点很关键。

源码注释中写得很清楚:

  • 外部插件可以通过这个字段拿到高级 runtime helpers
  • 内置渠道通常直接 import 内部模块

这说明 OpenClaw 很明确地区分了:

  • core 内置实现
  • 插件 SDK 可见实现

所以插件系统不是简单源码复用,而是有明确 API 边界。


第 12 段:ChannelSecurityAdapter

export type ChannelSecurityAdapter<ResolvedAccount = unknown> = {  resolveDmPolicy?: ...  collectWarnings?: ...};

这一层很能体现 OpenClaw 的安全观。

系统没有把安全策略写死成“所有平台都一样”,而是允许渠道决定:

  • DM policy 如何解析
  • 是否有特定平台警告

这很合理,因为不同平台的身份模型和消息模型并不相同。

所以安全不是单独外挂的一层,而是渠道契约的一部分。


第 13 段:其他 adapter 的意义

除了上面几块主干,types.adapters.ts 还定义了很多补充行为面:

  • ChannelGroupAdapter
  • ChannelPairingAdapter
  • ChannelAuthAdapter
  • ChannelHeartbeatAdapter
  • ChannelDirectoryAdapter
  • ChannelResolverAdapter
  • ChannelElevatedAdapter
  • ChannelCommandAdapter

这些 adapter 合起来说明一件事:

渠道插件不只负责“消息收发”,还负责消息边界、安全边界、查找边界、登录边界和运维边界。

这也是为什么 OpenClaw 的渠道抽象看起来会比一般 bot 框架重很多。


第 3 层:最后回到 types.plugin.ts

看完 core types 和 adapters 之后,再回头看 ChannelPlugin 就清楚多了:

export type ChannelPlugin<ResolvedAccount = any,Probe = unknown,Audit = unknown> = {  id: ChannelId;  meta: ChannelMeta;  capabilities: ChannelCapabilities;  defaults?: ...  reload?: ...  onboarding?: ChannelOnboardingAdapter;  config: ChannelConfigAdapter<ResolvedAccount>;  configSchema?: ChannelConfigSchema;  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[];};

现在你就能明白这不是“大接口”,而是“装配表”。

ChannelPlugin 的职责其实很简单:

  • 声明身份
  • 声明能力
  • 提供配置入口
  • 按需挂接若干行为 adapter

这就是 OpenClaw 渠道扩展机制最核心的设计。


哪些字段最关键

如果只抓主干,最关键的是这些:

  • id
  • meta
  • capabilities
  • config
  • gateway
  • outbound
  • status

其中:

  • id/meta/capabilities 决定“这个渠道是什么”
  • config 决定“系统怎么理解它的账户”
  • gateway 决定“系统怎么运行它”
  • outbound 决定“系统怎么通过它发消息”
  • status 决定“系统怎么观测它”

如何增加一个最小可用渠道

应该先问 4 个问题:

  1. 我的账户配置怎么解析?config
  2. 我的运行监听怎么启动和停止?gateway
  3. 我怎么把文本发出去?outbound
  4. 我想向 core 声明哪些能力?capabilities

也就是说,一个最小可用渠道,通常只需要:

  • id
  • meta
  • capabilities
  • config
  • gateway
  • outbound

其余能力按需补充。


为什么这种设计能撑住 20+ 平台

核心原因有 4 个:

  1. 平台差异被拆成多个小 adapter,而不是一个巨型实现接口。
  2. 上层逻辑尽量依赖 capability 和 adapter,而不是平台名字。
  3. 内置渠道和外部插件共享同一套主契约。
  4. 运行时、安全、状态、目录、线程这些复杂能力都被提前纳入抽象边界。

这让 OpenClaw 的渠道系统既能扩展,又不至于把核心层写成平台特例地狱。


把三层文件压缩成一句话

你可以这样记:

types.core.ts 定义渠道对象types.adapters.ts 定义对象可实现的行为types.plugin.ts 把对象和行为装配成一个 ChannelPlugin

读完这一篇后你应该能回答

  1. ChannelConfigAdapter 为什么是少数必填能力之一?
  2. ChannelGatewayAdapter 和 ChannelOutboundAdapter 各自负责什么?
  3. 外部插件为什么需要 channelRuntime 这种 SDK 边界?

下一篇预告

开始沿真实消息执行链进入agent大脑。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » OpenClaw源码解析(六):Channels渠道插件是如何设计的

猜你喜欢

  • 暂无文章