深入解析 OpenClaw Channel Plugin 架构:14 大核心 Adapter 设计之道
在构建支持 40+ 通信渠道的 AI 网关系统时,如何平衡统一抽象与渠道特性?OpenClaw 通过一套精妙的 Channel Plugin 架构给出了答案。本文将深入剖析构成 OpenClaw 插件系统的 14 个核心 Adapter 接口——从配置管理(ChannelConfigAdapter)到消息投递(ChannelOutboundAdapter),从设备安装(ChannelSetupAd
📖 引言
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),让每个渠道实现自己的版本。
本文你将学到什么
- 14 个 Adapter 的完整职责和方法签名
- 每个 Adapter 在生产环境中的真实实现示例
- 哪些 Adapter 是必选的,哪些是可选的
- 如何借鉴这套设计思想到自己的项目中
🏗️ 架构总览
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 }
: {}),
},
},
};
}
// 命名账户配置...
},
}
设计亮点
- 验证与应用的分离:
validateInput先检查,applyAccountConfig再写入 - 环境变量支持:允许通过
--use-env使用TELEGRAM_BOT_TOKEN环境变量 - 不可变更新:每次返回新的配置对象,避免副作用
- 默认账户特殊处理:
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,
},
设计亮点
- 多账户支持:通过
listAccountIds+resolveAccount支持配置多个账户 - 配置状态检查:
isConfigured验证凭证是否完整 - 启用/禁用机制:可以临时禁用某个账户而不删除配置
- 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:工具权限控制
- 在公开群组:禁用文件上传工具(防止滥用)
- 在私密群组:允许所有工具
设计亮点
- 提及门控:群聊中必须 @机器人 才响应,避免刷屏
- 策略可配置:通过
groupPolicy灵活控制开放程度 - 工具沙箱:不同群组可以使用不同的工具集
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 |
设计亮点
- 自动分块:长消息自动切割,防止超出平台限制
- 格式转换:Markdown → HTML(Telegram)、纯文本(Signal)
- 特殊功能支持:如 MS Teams 的投票功能
- 身份切换:支持通过 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 是否过期 |
设计亮点
- 分层检查:
probe(快速)vsaudit(深度) - 问题聚合:
collectStatusIssues汇总所有健康问题 - 运行时追踪:记录最后一次启动/停止时间
- 可观测性:为 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:后续交互
- 该用户的所有消息都被信任(无需再次配对)
设计亮点
- ID 标准化:统一去除
telegram:、user:等前缀 - 确认反馈:让用户知道配对成功
- 安全边界:只有配对用户才能触发敏感操作
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;
};
设计亮点
- 优雅关闭:通过
abortSignal实现资源清理 - 状态追踪:实时更新
running、lastStartAt等状态 - QR 码支持:适配 WhatsApp、微信等需要扫码登录的渠道
- 日志注入:每个账户独立的日志上下文
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 密钥文件 |
设计亮点
- 交互式认证:支持 CLI 提示、浏览器跳转等
- 凭证安全存储:保存到
~/.openclaw/credentials/ - 与 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",
})
设计亮点
- 前置检查:
checkReady确认是否满足发送条件 - 接收者列表:支持动态计算心跳目标
- 可配置频率:通过 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 | 大型组织 |
设计亮点
- 双重来源:支持静态配置 + 实时查询
- 搜索过滤:
query参数支持模糊匹配 - 分页支持:
limit参数控制返回数量 - 类型区分:
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",
},
]
设计亮点
- 批量解析:一次性处理多个输入
- 快速路径优化:已经是 ID 的直接返回,避免 API 调用
- 详细错误信息:
note字段说明失败原因 - 部分成功:即使某些失败,也返回成功的结果
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️⃣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️⃣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 | 完全禁止私聊 | 仅群聊 |
设计亮点
- 多层防护:DM 策略 + 群组策略 + 工具策略
- 主动警告:
collectWarnings提醒用户加固配置 - 标准化输入:统一处理 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
- ✅ 避免继承地狱
更多推荐


所有评论(0)