剖析 OpenClaw 与 Hermes:Agent 如何通过统一网关层接入 IM 插件
- Agent 不应直接对接每个即时通讯(IM)平台:OpenClaw 和 Hermes 都建议通过一个统一的网关层将不同 IM 的消息转换为标准事件,再由 Agent 执行。这避免了 Agent 核心被特定 IM 协议污染,并简化了会话管理和权限控制。
- OpenClaw 采用插件化 Channel Gateway:它允许以插件形式添加新的 IM 支持,每个插件需提供
openclaw.plugin.json文件用于身份识别、配置校验等。这种方式非常适合构建多 IM 插件生态系统。 - Hermes 使用内置 Platform Adapter:其扩展方式更倾向于在仓库内实现适配器,而不是外部插件。Hermes 的架构设计使得同一个 AIAgent 可以服务多种入口点,如 CLI、API Server 等,而平台差异则在接入层处理。
- 统一的消息标准化与回复分发机制:无论是 OpenClaw 还是 Hermes,都采用了将原始 IM 消息转化为统一格式(如 MessageEvent 或 ctxPayload),并通过专门的分发组件(如 reply dispatcher 或 adapter.send_message())来发送回复,确保 Agent 逻辑与具体的 IM 实现解耦。
- 推荐的自建 Agent IM 网关架构:结合 OpenClaw 的插件式扩展性和 Hermes 的统一消息处理及权限控制策略,开发者可以创建既灵活又安全的 IM 网关,支持多样化的通信渠道同时保持核心业务逻辑的一致性。
剖析 OpenClaw 与 Hermes:Agent 如何通过统一网关层接入 IM 插件
现在很多 Agent 项目已经不满足于“在网页里聊天”了。真正有用的 Agent,往往需要出现在用户原本工作的地方:Slack、Telegram、飞书、钉钉、企微、Discord、WhatsApp、OpenIM、邮件、短信,甚至 issue tracker 里。
这就带来一个工程问题:Agent 要不要直接对接每一个 IM?
答案通常是否定的。OpenClaw 和 Hermes 给出的思路都很明确:Agent 核心不直接理解各个 IM 协议,而是通过一个统一的 Gateway,把不同 IM 的消息转换成标准事件,再交给 Agent 执行。
一、为什么 Agent 需要统一 IM 网关?
如果没有网关层,一个 Agent 要接入多个 IM,就会变成这样:
Agent
├── Telegram SDK
├── Slack SDK
├── 飞书事件回调
├── 钉钉 WebSocket
├── 企微回调
├── Discord Bot
└── WhatsApp Business API
这会导致几个问题:
第一,Agent 核心被 IM 协议污染。模型调用、工具调用、记忆、RAG、任务执行,本来应该是平台无关的,但一旦把 Telegram、Slack、飞书的字段写进 Agent,后面维护会很痛苦。
第二,会话管理会混乱。私聊、群聊、线程、频道、机器人多账号、群里 @ 触发,这些都需要稳定的 session key。如果每个 IM 自己处理,很容易出现上下文串线。
第三,权限控制难统一。谁能唤醒 Agent?群里是否必须 @?哪些人能调用高危工具?这些都应该在网关层统一处理,而不是散落在每个 Bot 里。
所以更合理的架构是:
各类 IM / 消息平台
↓
IM Adapter / Channel Plugin
↓
统一 Gateway
↓
Agent Runtime
↓
工具调用 / RAG / 数据库 / 业务系统
OpenClaw 和 Hermes 都是这个方向,但两者的扩展方式不同:OpenClaw 更像插件化 Channel Gateway,Hermes 更像内置 Platform Adapter Gateway。
二、OpenClaw:以 Channel Plugin 为核心的统一网关
OpenClaw 官方文档对自己的定位很清楚:它是一个 self-hosted gateway,可以把 Discord、Google Chat、iMessage、Matrix、Teams、Signal、Slack、Telegram、WhatsApp、Zalo 等聊天入口连接到 AI Agent;同时它强调一个 Gateway 可以同时服务内置 channel、bundled channel plugin、外部 channel plugin。OpenClaw 文档还明确说:Gateway 是 sessions、routing、channel connections 的 single source of truth。(GitHub)
也就是说,OpenClaw 的核心不是“某一个 IM Bot”,而是:
OpenClaw Gateway
├── built-in channels
├── bundled channel plugins
├── external channel plugins
├── session manager
├── routing manager
├── reply dispatcher
└── agent runtime bridge
它的设计重点是:IM 能力以 channel plugin 的形式挂到 Gateway 上。
三、OpenClaw 的插件是怎么被发现和加载的?
OpenClaw 的 native plugin 需要提供一个 openclaw.plugin.json 文件。官方 manifest 文档说明,OpenClaw 会在加载插件代码之前读取这个 manifest,用它做插件身份识别、配置校验、UI 提示、auth/setup 元数据、channel config 元数据等;缺失或无效的 manifest 会被视为插件错误,并阻止配置校验。(GitHub)
这点很关键:Gateway 在执行插件代码之前,就已经知道这个插件是谁、提供什么 channel、需要什么配置。
一个典型插件会有两层元信息:
openclaw.plugin.json
负责:插件身份、配置 schema、channel config schema
package.json
负责:npm 包信息、入口文件、openclaw.extensions
OpenClaw 文档也区分了两者:openclaw.plugin.json 用于 discovery、config validation、UI hints 等运行前信息;package.json 里的 openclaw 字段用于 entrypoints、安装门控、setup、catalog 元数据等。(GitHub)
四、以 OpenIM 插件为例:OpenClaw 的 IM 插件结构
OpenIM 的 OpenClaw channel 插件是一个比较好的样本。它的 manifest 中声明了插件 id、name、version、description,并且声明自己提供 openim channel,同时提供 configSchema。(GitHub)
简化后类似这样:
{
"id": "openclaw-channel",
"name": "OpenIM Channel",
"version": "0.3.2",
"description": "OpenIM protocol channel for OpenClaw",
"channels": ["openim"],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": true
}
}
}
}
OpenClaw 的 manifest 文档里还区分了 configSchema 和 channelConfigs:前者校验 plugins.entries.<plugin-id>.config,后者校验 channels.<channel-id>。这说明 OpenClaw 把“插件自身配置”和“channel 实例配置”分得很清楚。(GitHub)
这类结构对做企业 IM 接入很有价值。比如你做飞书插件,可以这样拆:
plugins.entries.feishu.config
app_id
app_secret
encrypt_key
verification_token
channels.feishu
allowUsers
allowGroups
requireMention
defaultAgent
sessionScope
也就是说,插件负责能力注册,channel 配置负责运行策略。
五、OpenClaw 插件运行时:register(api)
OpenIM 插件的入口是一个 register(api) 函数。代码里可以看到它会调用:
api.registerChannel({ plugin: OpenIMChannelPlugin })
api.registerCli(...)
registerOpenIMTools(api)
api.registerService(...)
也就是说,一个 OpenClaw IM 插件不只是“发消息接口”,它会同时注册 channel、CLI setup、工具、后台 service。(GitHub)
抽象出来就是:
OpenClaw Plugin
├── registerChannel()
│ └── 告诉 Gateway:我提供一个 openim channel
├── registerCli()
│ └── 提供 openim setup 等命令
├── registerTools()
│ └── 让 Agent 可以主动调用 OpenIM 工具
└── registerService()
└── 启动 OpenIM SDK、登录账号、监听消息
这套机制很适合三方 IM,因为 IM 插件通常需要长期连接,例如 WebSocket、长轮询、事件回调服务、SDK 登录状态维护等。单纯写一个 sendMessage() 是不够的。
六、OpenClaw 的 Channel Plugin 如何发消息?
OpenIM 插件的 channel 实现中有 sendText 逻辑:先解析目标 to,要求是 user:<id> 或 group:<id>;然后获取已连接的 OpenIM client;最后调用 OpenIM 的发送逻辑,把文本发到目标用户或群组。(GitHub)
简化后可以理解成:
sendText({ to, text, accountId }) {
const target = parseTarget(to)
if (!target) {
return { ok: false, error: "invalid target" }
}
const client = getConnectedClient(accountId)
if (!client) {
return { ok: false, error: "OpenIM not connected" }
}
await sendTextToTarget(client, target, text)
return { ok: true, provider: "openim" }
}
这就是典型的 outbound adapter 设计:
Gateway 标准发送请求
↓
Channel Plugin 解析目标
↓
调用具体 IM SDK/API
↓
返回统一 SendResult
Agent 不需要知道 OpenIM 的用户 ID、群 ID、SDK 方法名,它只需要通过 Gateway 说:“往这个 channel 的这个 target 发一段文本。”
七、OpenClaw 的入站消息流:从 IM 消息到 Agent 上下文
OpenIM 插件的 processInboundMessage 很能体现 OpenClaw 的网关思想。
它会先做几类处理:
1. 判断 runtime.channel.reply 是否可用
2. 解析消息文本、媒体、发送人、群信息
3. 做白名单校验
4. 群聊判断是否 @ 机器人
5. 生成 baseSessionKey
6. 通过 routing.resolveAgentRoute 做 Agent 路由
7. 构造统一 ctxPayload
8. recordInboundSession 记录会话
9. dispatchReplyWithBufferedBlockDispatcher 调 Agent 并分发回复
代码中可以看到,OpenIM 插件会在有 inbound whitelist 时校验发送者;如果是群聊并且没有 mention,就直接 return;如果没有 whitelist,但群聊配置了 requireMention 且没有被 @,也会 return。(GitHub)
这说明 OpenClaw 把群聊触发策略放在 channel 入站层处理:
私聊:可以默认响应
群聊:通常需要 @bot
白名单:只有允许的用户/群可以触发
然后它会生成类似这样的 session key:
const baseSessionKey = group
? `openim:group:${msg.groupID}`.toLowerCase()
: `openim:${msg.sendID}`.toLowerCase()
再调用 Gateway runtime 的 routing 能力解析最终 agent route。代码里可以看到它会拿到 route.sessionKey,并构造 ctxPayload,里面包含 Body、RawBody、From、To、SessionKey、AccountId、ChatType、SenderId、Provider、Surface、OriginatingChannel 等字段。(GitHub)
这个 ctxPayload 就是 OpenClaw 的关键抽象:不同 IM 的原始事件,最终都要变成 Agent 能读懂的统一上下文。
八、OpenClaw 的回复分发:Agent 输出如何回到原 IM?
OpenIM 插件最后会调用:
runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
ctx: ctxPayload,
cfg,
dispatcherOptions: {
deliver: async (payload) => {
await sendReplyFromInbound(client, msg, payload.text)
}
}
})
代码中可以看到,它把 ctxPayload 交给 Gateway 的 reply dispatcher;dispatcher 负责处理 Agent 回复,然后通过插件提供的 deliver 回调把文本发回 OpenIM 原会话。(GitHub)
这背后的分层很漂亮:
Agent
只负责生成回复
Gateway Reply Dispatcher
负责流式/分块/缓冲/错误处理
Channel Plugin
负责把回复发回具体 IM
这避免了一个常见错误:让 Agent 自己记住消息来自哪个 IM,然后自己调用对应 SDK 发回去。
正确做法是 Gateway 记录 OriginatingChannel、SessionKey、From、To,再通过 channel plugin 回发。
九、Hermes:以内置 Platform Adapter 为核心的统一网关
Hermes 的网关设计和 OpenClaw 类似,也是把消息平台接入和 Agent 核心解耦。但 Hermes 的扩展方式更偏“内置 adapter”,而不是 OpenClaw 那种外部 channel plugin。
Hermes 的架构文档里说明,Messaging Gateway 是一个 long-running process,包含 18 个 platform adapters、统一 session routing、用户授权、slash command dispatch、hook system、cron ticking 和后台维护。(GitHub)
Hermes 的整体 Agent 架构是:
Entry Points
├── CLI
├── Gateway
├── ACP
├── Batch Runner
├── API Server
└── Python Library
↓
AIAgent
↓
Provider Resolution / Tool Dispatch / Session Storage
官方文档中也明确写到:同一个 AIAgent class 服务 CLI、gateway、ACP、batch、API server;平台差异存在于 entry point,而不是 agent 本身。(GitHub)
这句话非常关键。它说明 Hermes 的原则是:
平台接入层可以不同,
但 Agent 核心必须保持平台无关。
十、Hermes 的消息流:Adapter → MessageEvent → GatewayRunner → AIAgent
Hermes 官方 Gateway Internals 文档把消息流写得很清楚:
1. Platform adapter 接收原始事件,并标准化成 MessageEvent
2. Base adapter 检查 active session guard
3. GatewayRunner._handle_message() 接收事件
4. 解析 session key
5. 做授权检查
6. 判断是否是 slash command
7. 如果没有正在运行的 agent,则创建 AIAgent 并运行对话
8. 回复通过 platform adapter 发回
这个流程说明 Hermes 也采用了标准事件模型:不同平台进来的原始事件,会先被 adapter 统一成 MessageEvent,再进入 GatewayRunner。(Hermes Agent)
对应架构可以画成:
Telegram / Slack / Discord / 飞书 / 钉钉 / 企微
↓
Platform Adapter
↓
MessageEvent
↓
GatewayRunner._handle_message()
↓
Authorization / Slash Command / Session Routing
↓
AIAgent.run_conversation()
↓
Adapter.send_message()
Hermes 的 session key 格式也很明确:
agent:main:{platform}:{chat_type}:{chat_id}
例如:
agent:main:telegram:private:123456789
官方文档还提醒,Telegram forum topics、Discord threads、Slack threads 这类支持 thread 的平台,可能会把 thread id 放进 chat_id 部分,并且不要手工拼 session key,而应该使用 build_session_key()。(Hermes Agent)
这说明 Hermes 对“跨平台会话隔离”很重视。它不是简单用用户 id 做上下文,而是把 agent、platform、chat_type、chat_id 都编码进 session key。
十一、Hermes 的 Platform Adapter 抽象
Hermes 文档列出了 gateway/platforms/ 下的各种 adapter,包括 Telegram、Discord、Slack、WhatsApp、Signal、Matrix、Mattermost、Email、SMS、DingTalk、Feishu/Lark、WeCom、Weixin、BlueBubbles、QQ Bot、Webhook、API Server、Home Assistant 等。每个平台 adapter 实现共同接口,包括生命周期管理、出站消息发送、入站消息标准化为 MessageEvent。(Hermes Agent)
官方添加平台 adapter 的文档也给出了标准结构:
User ↔ Messaging Platform ↔ Platform Adapter ↔ Gateway Runner ↔ AIAgent
并说明每个 adapter 都要继承 BasePlatformAdapter,实现 connect()、disconnect()、send()、send_typing()、get_chat_info() 等方法;入站消息则构造 MessageEvent,调用 self.handle_message(event) 交给基础类转发到 Gateway Runner。(Hermes Agent)
简化成代码就是:
class NewPlatAdapter(BasePlatformAdapter):
async def connect(self) -> bool:
# 建立 WebSocket / webhook / long polling
return True
async def disconnect(self) -> None:
# 清理连接
pass
async def send(self, chat_id, content, reply_to=None, metadata=None):
# 调用平台 API 发消息
return SendResult(success=True)
async def on_message(self, raw_event):
source = self.build_source(
chat_id=chat_id,
chat_type="dm",
user_id=user_id,
user_name=user_name,
)
event = MessageEvent(
text=content,
message_type=MessageType.TEXT,
source=source,
message_id=msg_id,
)
await self.handle_message(event)
所以 Hermes 的 adapter 责任是:
1. 连接平台
2. 接收平台事件
3. 把事件转成 MessageEvent
4. 把 Gateway 的回复发回平台
而 GatewayRunner 负责:
1. session key
2. authorization
3. slash command
4. running-agent guard
5. AIAgent 创建和执行
十二、Hermes 的权限与运行中断设计
Hermes 的 Gateway 不只是转发消息,它还做了较完整的运行控制。
官方文档描述了两级 message guard:
Level 1:Base adapter 检查 active session
Level 2:Gateway runner 检查 running agents
如果某个 session 里的 Agent 正在运行,新来的消息会被排队,并设置 interrupt event;而 /approve、/deny、/stop 这类命令可以绕过普通后台任务系统,直接进入处理流程,避免竞态。(Hermes Agent)
这非常适合“能执行任务”的 Agent。因为 Agent 可能正在跑一个长任务,例如查资料、写代码、执行 shell、调用工具。如果用户这时发了 /stop,网关必须能打断;如果用户发了 /approve,网关必须能让审批命令直达。
Hermes 的授权也分多层:平台级 allow-all、平台 allowlist、DM pairing、全局 allow-all,最后默认拒绝未授权用户。(Hermes Agent)
这对企业 IM 接入尤其重要。你不能让任何群成员都能让 Agent 执行工具,尤其是涉及文件、代码、数据库、内部系统时。
十三、Hermes 的平台接入为什么不像 OpenClaw 插件?
这是 OpenClaw 和 Hermes 最大的差异之一。
OpenClaw 的 channel 能以外部插件方式接入,插件通过 manifest 和 register(api) 挂载到 Gateway。
Hermes 虽然也有 plugin system,但其文档描述的插件主要用于 tools、hooks、CLI commands,以及 memory providers、context engines 等;platform adapter 则是 gateway/platforms/ 里的内置 adapter 体系。Hermes 的架构文档中,plugin system 的发现来源包括用户插件目录、项目插件目录和 pip entry points,但描述的是工具、hooks、CLI 命令、memory provider、context engine 等。(GitHub)
Hermes 添加 platform adapter 的官方指南也提醒:新增一个 messaging platform adapter 会涉及 20 多个文件,adapter 文件本身通常只占 40% 工作量。(Hermes Agent)
更直接的是,一个关于 Linear platform adapter 的 issue 提到:Hermes 有工具插件系统,但没有 platform adapter 插件系统;目前 established approach 是 built-in adapter pattern。(GitHub)
所以可以这样总结:
OpenClaw:
平台接入 = 外部 channel plugin 也可以做
Hermes:
平台接入 = 主要走仓库内 built-in adapter
这不是优劣问题,而是取舍不同。
OpenClaw 更适合做“多 IM 插件生态”。
Hermes 更适合做“统一 Agent 产品内置多平台入口”。
十四、两者架构对比
| 维度 | OpenClaw | Hermes |
|---|---|---|
| 核心定位 | self-hosted multi-channel gateway | self-improving agent with messaging gateway |
| IM 接入方式 | built-in channel + bundled/external channel plugin | gateway/platforms/ 内置 adapter |
| 插件发现 | openclaw.plugin.json + package.json#openclaw |
Python 插件系统主要用于 tools/hooks/memory/context,不是平台 adapter |
| 入站标准化 | channel plugin 构造 ctxPayload | adapter 构造 MessageEvent |
| 会话路由 | Gateway 是 sessions/routing/channel connections 的 single source of truth | session key: agent:main:{platform}:{chat_type}:{chat_id} |
| 群聊触发 | channel 层处理 mention、whitelist | adapter/Gateway 层结合授权与命令处理 |
| 出站回复 | reply dispatcher + plugin deliver | adapter send / send_message |
| 扩展第三方 IM | 更像 npm 插件式扩展 | 更像修改核心仓库新增 adapter |
十五、如果我们自己做 Agent IM 网关,应该学什么?
如果你要做自己的 Agent 产品,比如“AI 标书助手接入飞书、钉钉、企微、OpenIM”,可以同时借鉴两者。
我建议的架构是:
im-gateway
├── core
│ ├── message-envelope.ts
│ ├── session-router.ts
│ ├── auth-policy.ts
│ ├── reply-dispatcher.ts
│ ├── channel-directory.ts
│ └── attachment-store.ts
│
├── adapters
│ ├── feishu
│ ├── dingtalk
│ ├── wecom
│ ├── openim
│ └── slack
│
├── agent-clients
│ ├── dify
│ ├── coze
│ ├── langchain
│ └── custom-agent
│
└── plugins
├── manifest.json
└── register.ts
核心接口可以这样设计:
interface IMAdapter {
id: string
start(ctx: GatewayContext): Promise<void>
stop(): Promise<void>
sendText(target: SendTarget, text: string): Promise<SendResult>
sendImage?(target: SendTarget, imageUrl: string): Promise<SendResult>
sendFile?(target: SendTarget, fileUrl: string): Promise<SendResult>
}
统一入站事件可以这样设计:
interface MessageEnvelope {
provider: string
accountId?: string
chatType: "direct" | "group" | "channel" | "thread"
chatId: string
threadId?: string
senderId: string
senderName?: string
messageId: string
timestamp: number
text: string
rawEvent?: unknown
attachments?: Attachment[]
sessionKey: string
originatingChannel: string
}
Agent 侧只需要看这个:
{
"user_id": "feishu:ou_xxx",
"session_id": "agent:main:feishu:group:oc_xxx",
"text": "帮我总结一下这个投标文件",
"attachments": []
}
而不是看这个:
{
"open_id": "...",
"union_id": "...",
"chat_id": "...",
"tenant_key": "...",
"event_id": "...",
"message": {
"message_type": "text"
}
}
这就是统一网关层的价值。
十六、最值得复用的 6 个设计点
1. Agent 核心必须平台无关
Hermes 的设计原则很清楚:同一个 AIAgent 服务 CLI、Gateway、ACP、Batch、API Server,平台差异留在入口层。(GitHub)
你自己的 Agent 也应该如此:
Agent 不知道消息来自飞书还是钉钉
Agent 只知道 user、session、text、attachments、tools
2. SessionKey 要稳定且可解释
可以参考 Hermes:
agent:{agent_id}:{platform}:{chat_type}:{chat_id}
比如:
agent:bid-assistant:feishu:group:oc_abc
agent:bid-assistant:dingtalk:direct:user_123
agent:contract-review:wecom:group:wr_xxx
3. 群聊必须有触发规则
OpenClaw 的 OpenIM 插件已经体现了这个逻辑:白名单、群聊 mention、requireMention 都在入站层做拦截。(GitHub)
建议你也做成配置:
{
"channels": {
"feishu": {
"groups": {
"*": {
"requireMention": true
}
}
}
}
}
4. Reply Dispatcher 要独立出来
不要让 Agent 自己发 IM 消息。
应该是:
Agent 输出
↓
Gateway Reply Dispatcher
↓
Adapter.sendText()
OpenClaw 的 dispatchReplyWithBufferedBlockDispatcher 就是这个思路。(GitHub)
5. 权限控制放在 Gateway 层
Hermes 的授权链路包括平台 allow-all、平台 allowlist、DM pairing、全局 allow-all,最后默认拒绝。(Hermes Agent)
企业场景下可以再细分:
普通用户:只能问答
项目成员:可以读知识库
管理员:可以创建任务
超级管理员:可以调用外部系统写操作
6. Channel Directory 很重要
Agent 不能只知道一堆 ID,它需要知道:
{
"feishu": {
"groups": [
{
"id": "oc_xxx",
"name": "投标项目A群"
}
],
"users": [
{
"id": "ou_xxx",
"name": "张三"
}
]
}
}
这样用户才能自然地说:
把这份总结发到投标项目A群
而不是:
把这份总结发到 feishu:oc_xxx
结语
OpenClaw 和 Hermes 的共同点是:它们都没有让 Agent 直接绑定 IM 协议,而是通过 Gateway 做消息标准化、会话隔离、权限控制和回复分发。
它们的差异在于:
OpenClaw 更像插件平台:
第三方 IM 可以通过 channel plugin 接入 Gateway。
Hermes 更像完整 Agent 产品:
多平台 adapter 内置在 gateway/platforms/ 里,统一服务 AIAgent。
如果你要做自己的 Agent IM 接入层,最推荐的组合是:
OpenClaw 式插件机制
+
Hermes 式统一 MessageEvent / SessionKey / Authorization / Running-Agent Guard
最终目标是让 Agent 只关心一件事:
我收到了一条来自某个用户、某个会话的标准消息;
我处理它;
然后把标准回复交给 Gateway。
至于这条消息来自飞书、钉钉、企微、OpenIM、Slack 还是 Telegram,都应该由统一网关层和 IM 插件去解决。