从入口到回复:OpenClaw 架构与核心流程一览
本文以大纲形式介绍 OpenClaw 的整体架构、核心模块和数据流。目标是让读者快速理解系统如何把多个消息渠道(Telegram、Discord、WhatsApp 等)统一接入 AI Agent,并保证会话隔离、回复原路返回。
OpenClaw 架构设计概览
本文以大纲形式介绍 OpenClaw 的整体架构、核心模块和数据流。目标是让读者快速理解系统如何把多个消息渠道(Telegram、Discord、WhatsApp 等)统一接入 AI Agent,并保证会话隔离、回复原路返回。
〇、前置知识
阅读本文前,建议了解以下概念:
核心概念
| 概念 | 说明 |
|---|---|
| Pi | OpenClaw 内置的 AI Agent 运行时,负责与 LLM(如 Claude)交互、管理会话上下文、执行工具调用。Pi 是 OpenClaw 的"大脑",所有消息最终都会交给 Pi 处理 |
| Workspace | 工作空间,一个隔离的配置单元。每个 workspace 有独立的 agent 配置、知识库、工具权限等。适合多租户或多人共享场景 |
| Agent | AI 助手实例,绑定特定的模型(如 claude-3-5-sonnet)、系统提示词、工具集。一个 workspace 可配置多个 agent |
| Channel | 消息通道,如 Telegram、Discord、WhatsApp、Signal 等。每个通道有 inbound(接收消息)和 outbound(发送消息)两个方向 |
| Hook | 钩子函数,在消息处理的特定时机触发自定义逻辑。如 message_received hook 可在消息进入系统时执行预处理 |
平台术语对照
不同消息平台对"群组"的概念有不同叫法:
| OpenClaw 统一术语 | Telegram | Discord | Slack | 说明 |
|---|---|---|---|---|
| guild | - | Server(服务器) | Workspace | 平台的顶级组织单元 |
| peer | Chat(对话/群组) | Channel(频道) | Channel | 消息的具体来源位置 |
| team | - | - | Team | Slack 特有的组织概念 |
技术背景
| 概念 | 说明 |
|---|---|
| Commander | Node.js 生态中流行的命令行参数解析库,用于构建 CLI 工具。OpenClaw 使用它来注册 openclaw gateway、openclaw agent 等子命令 |
| interface(接口) | TypeScript 中的类型定义结构,用于描述对象的"形状"(有哪些字段、什么类型),不包含具体实现代码 |
.impl. 后缀 |
OpenClaw 代码中的命名约定,表示"实现文件"。如 server.impl.ts 是 server.ts 接口的具体实现 |
一、架构分层总览
OpenClaw 采用五层架构,自上而下依次为:接入层 → 网关层 → 编排层 → 执行层 → 投递层。
层次职责速览
| 层次 | 核心职责 | 关键数据结构 |
|---|---|---|
| 接入层 | 接收各渠道原始消息,归一化为统一上下文 | FinalizedMsgContext(详见下节) |
| 网关层 | 常驻进程,管理通道生命周期、处理连接与请求 | OpenClawConfig、会话 store |
| 编排层 | 决定谁处理、怎么拿回复、往哪发 | ResolvedAgentRoute、MsgContext |
| 执行层 | 按 session 并发控制,运行 Agent | sessionKey(详见下节)、ReplyPayload |
| 投递层 | 解析目标通道,调用 outbound 发送消息 | AgentDeliveryPlan |
sessionKey 格式说明
sessionKey 是会话的唯一标识符,采用冒号分隔的字符串,规范形式为:
agent : <agentId> : <rest>
- agent:固定前缀,表示该 key 属于 agent 作用域,用于路由和会话存储。
- agentId:代理 ID(如
main、work),对应不同的 workspace 和会话存储。 - rest:会话类型与上下文,随场景变化,常见形式如下:
| 场景 | rest 示例 | 说明 |
|---|---|---|
| 主会话 | main |
默认主会话,多设备/多渠道可共用 |
| 群组 | <channel>:group:<id> |
如 telegram:group:-1001234567890 |
| 频道/房间 | <channel>:channel:<id> |
如 discord:channel:123456 |
| 私信(按渠道+对端) | <channel>:direct:<peerId> |
如 telegram:direct:123456 |
| 子代理 | subagent:<uuid> |
子任务会话 |
| 线程 | 在 base 后追加 :thread:<id> 或 :topic:<id> |
Slack/Discord 线程、Telegram 论坛话题 |
示例:
agent:main:main
agent:main:telegram:group:-1001234567890
agent:main:discord:channel:123456:thread:987654
agent:main:telegram:direct:123456
作用:同一 sessionKey 的消息会串行处理(通过 session lane 入队),避免同一会话内回复乱序。队列的 lane 名通常为 session: + sessionKey,与 sessionKey 本身不要混淆。
FinalizedMsgContext 核心字段
FinalizedMsgContext 是归一化后的消息上下文,包含:
| 字段 | 类型 | 说明 |
|---|---|---|
channel |
string | 通道标识,如 telegram、discord |
accountId |
string | 账户 ID,支持同通道多账号 |
peerId |
string | 对话 ID,标识群组或私聊 |
userId |
string | 发送者用户 ID |
text |
string | 消息正文 |
attachments |
array | 附件列表(图片、文件等) |
replyTo |
object | 引用回复的原消息信息 |
timestamp |
number | 消息时间戳 |
二、各层模块详解
2.1 接入层:入口与通道
接入层负责接收外部消息,并将其转换为系统内部统一的数据结构。
模块一览
| 模块 | 是什么 | 负责什么 |
|---|---|---|
| entry | 进程入口点 | 解析环境变量、profile 配置,校验实验性功能开关,然后转交 CLI 主控 |
| run-main | CLI 主控模块 | 加载环境、规范化命令行参数、构建命令树,决定走 CLI 逻辑还是启动 Gateway |
| program | 命令树(基于 Commander 库) | 注册所有子命令(gateway、channels、agent、message 等),解析并派发到具体 handler。Commander 是 Node.js 生态流行的 CLI 框架 |
| 通道 Monitor | 各消息渠道的监听器 | 接收原始消息事件,解析发送者、正文、媒体等,调用路由得到 sessionKey,组装成 FinalizedMsgContext |
| ChannelPlugin | 通道插件契约(接口定义) | 定义 channel 的 id、meta、inbound/outbound、config 等接口,内置通道和扩展都实现此接口。接口是 TypeScript 的类型约束,确保所有通道插件有一致的 API |
核心数据流:
渠道原始事件 → Monitor 归一化 → resolveAgentRoute 算 sessionKey → FinalizedMsgContext → dispatchReplyFromConfig
2.2 网关层:常驻进程
网关层是系统的核心常驻进程,负责管理所有连接、通道和请求处理。
模块一览
| 模块 | 是什么 | 负责什么 |
|---|---|---|
| server.impl | 网关主进程(实现文件) | 启动 HTTP/WebSocket 服务器,加载配置,注册 RPC 方法(agent/send/sessions 等),维护心跳与健康检查。.impl. 后缀表示这是接口的具体实现代码 |
| WebSocket + HTTP | 传输协议层 | WS 处理连接握手、请求-响应帧、服务端推送;HTTP 提供静态资源和可选 API |
| 辅助服务(Sidecars) | 网关启动时一并初始化的辅助模块 | 按顺序启动:清理过期锁 → browser 控制 → Gmail watcher → 加载 hooks → 启动通道 → 启动插件服务。Sidecar 意为"边车",比喻依附主服务运行的辅助组件 |
| ChannelManager | 通道生命周期管理 | 提供 startChannels / stopChannel 等接口,按配置启动各通道的 monitor |
启动流程:
startGatewayServer
→ 读配置、TLS
→ startGatewaySidecars(启动所有辅助服务)
→ startChannels(启动各通道 monitor)
→ 挂载 WS/HTTP 路由
2.3 编排层:路由与派发
编排层是消息处理的核心决策层,决定"谁来处理"和"结果往哪发"。
三大模块职责
| 模块 | 是什么 | 负责什么 |
|---|---|---|
| resolveAgentRoute | 路由决策器 | 根据消息位置(channel/accountId/peer/guildId)和配置 bindings(静态路由表,如 { "agentId": "main", "match": { "channel": "telegram" } }),确定 agentId 和 sessionKey |
| dispatchReplyFromConfig | 派发控制器 | 接收带 sessionKey 的消息上下文,做去重、hook 触发,调用 getReply,然后按回复类型投递 |
| getReplyFromConfig | 回复生成器 | 解析 agent 配置、模型、workspace,做上下文增强、命令鉴权,最终调用 Agent 获取回复 |
路由优先级(从高到低):
peer(指定对话)→ guild+roles(群组+角色)→ team → account → channel → default
优先级术语解释:
- peer:最精细粒度,精确匹配某个对话(如 Discord 的某个频道、Telegram 的某个群组)
- guild+roles:匹配群组级别,可进一步按用户角色细分(如 Discord 服务器中只让管理员触发 agent)
- team:组织级别匹配(主要用于 Slack)
- account:账户级别,匹配某个渠道下的特定账号
- channel:通道级别,如"所有 Telegram 消息"
- default:兜底配置,当以上都不匹配时使用
数据流:
FinalizedMsgContext
→ dispatchReplyFromConfig
→ 去重 / message_received hook / TTS 前置
→ getReplyFromConfig
→ 解析 agentId、模型、workspace
→ 命令分支 或 runPreparedReply(调 Agent)
→ 按 payload 类型投递(routeReply / dispatcher.send*)
bindings 配置说明与示例:bindings 是顶层的静态路由表,把「某通道、某账号、某群/私聊/频道/身份」固定绑定到某个 agent;未匹配任何 binding 时走 default agent。
- 配置位置:顶层
bindings(与agents、channels并列;历史曾用routing.bindings,已迁移)。 - 单条结构:
{ agentId: string, match: { channel, accountId?, peer?, guildId?, teamId?, roles? } }。channel必填;其余可选,用于收窄匹配范围。 - match 各字段含义:
| 字段 | 必填 | 含义 | 典型取值 |
|---|---|---|---|
channel |
是 | 通道 ID,该 binding 只对该通道生效 | telegram、discord、whatsapp、slack 等 |
accountId |
否 | 账号范围:不写或空 = 仅匹配默认账号 "default";"*" = 该通道下任意账号;写具体 ID = 仅该账号 |
"default"、"*"、"personal"、"T0AAAA" |
peer |
否 | 精确到某对话:私聊/群/频道,由 kind+id 决定 |
{ "kind": "direct"|"group"|"channel", "id": "<对话ID>" } |
guildId |
否 | 群组/服务器 ID(如 Discord Server),与 channel 一起限定「哪个服务器」 | Discord 的 guild ID |
teamId |
否 | 工作区/团队 ID(如 Slack Workspace) | Slack 的 team ID |
roles |
否 | 角色 ID 列表(如 Discord 角色),消息发送者须至少具备其一才匹配;常与 guildId 同用 |
["1111111111111111111"] |
- 路由优先级(从高到低,先匹配到的 binding 生效):
- binding.peer:当前消息的对话(peer)与某条 binding 的
peer完全一致。 - binding.peer.parent:当前是子对话(如 Discord 线程)时,用父对话的 peer 再匹配一次。
- binding.guild+roles:同一 guild 且发送者带有 binding 中某角色(如 Discord 管理员)。
- binding.guild:同一 guild,且该 binding 未写
roles。 - binding.team:同一 team(如 Slack 工作区)。
- binding.account:同一通道下该账号,且 binding 写了具体
accountId(非*)。 - binding.channel:同一通道,binding 为
accountId: "*"的通道级兜底。 - default:以上都不中则用配置的默认 agent。
- binding.peer:当前消息的对话(peer)与某条 binding 的
下面按匹配粒度给出详细示例,并逐条说明含义(ID 均为占位,需按实际替换)。
1)仅 channel(通道级兜底)
{ "agentId": "main", "match": { "channel": "telegram", "accountId": "*" } }
含义:所有来自 Telegram、任意账号的消息,只要没有更具体的 binding 命中,都由 main 处理。accountId: "*" 表示不限定账号,属于通道级规则。
2)channel + accountId(账号级)
{ "agentId": "home", "match": { "channel": "whatsapp", "accountId": "personal" } },
{ "agentId": "work", "match": { "channel": "whatsapp", "accountId": "biz" } }
含义:同一 WhatsApp 通道下,personal 账号的对话走 home,biz 账号的对话走 work。多账号时用 accountId 区分不同 bot/身份。
3)channel + accountId + peer(对话级,最细)
{ "agentId": "support", "match": {
"channel": "telegram",
"accountId": "default",
"peer": { "kind": "group", "id": "-1001234567890" }
} }
含义:仅当消息来自 Telegram、默认账号、且对话是群组 -1001234567890 时,由 support 处理。其他群或私聊不匹配本条,会继续匹配更粗的规则或 default。
4)Discord:某服务器的某频道(peer + guildId)
{ "agentId": "dev-bot", "match": {
"channel": "discord",
"accountId": "default",
"peer": { "kind": "channel", "id": "1234567890123456789" },
"guildId": "9876543210987654321"
} }
含义:仅当消息来自 Discord、默认账号、且来自 guild 9876543210987654321 下的频道 1234567890123456789 时,由 dev-bot 处理。同一 bot 在不同服务器里可指向不同 agent。
5)Discord:某服务器 + 角色(guildId + roles)
{ "agentId": "ops", "match": {
"channel": "discord",
"accountId": "default",
"guildId": "9876543210987654321",
"roles": ["1111111111111111111"]
} }
含义:同一 Discord 服务器下,只有发送者拥有角色 ID 1111111111111111111 的消息由 ops 处理;其他用户仍走更粗的 binding 或 default。常用于「仅管理员/某角色用专用 agent」。
6)Discord:仅 guild(整服一个 agent)
{ "agentId": "sonnet", "match": {
"channel": "discord",
"accountId": "default",
"guildId": "123456789012345678"
} }
含义:该 Discord 服务器下所有频道、所有用户的消息都由 sonnet 处理(未再限定 peer 或 roles)。
7)Slack:某工作区下某私聊(teamId + peer)
{ "agentId": "main", "match": {
"channel": "slack",
"accountId": "T0AAAA",
"teamId": "T0AAAA",
"peer": { "kind": "direct", "id": "U1BBBB" }
} }
含义:仅当消息来自 Slack 工作区 T0AAAA、账号 T0AAAA、且是与用户 U1BBBB 的私聊时,由 main 处理。
8)组合示例:先细后粗(推荐顺序)
同通道下建议先写 peer/角色等细规则,再写 account/channel 粗规则,这样先匹配到的更具体 binding 会生效:
"bindings": [
{ "agentId": "support", "match": { "channel": "telegram", "accountId": "default", "peer": { "kind": "group", "id": "-1001234567890" } } },
{ "agentId": "main", "match": { "channel": "telegram", "accountId": "default" } }
]
含义:Telegram 默认账号下,群 -1001234567890 → support;其余对话(其他群、私聊)→ main。若两条顺序颠倒,则「仅 channel+account」那条会先命中,群消息也会被误判给 main。
-
注意:
agentId必须在agents.list中存在;match.channel需与通道 ID 一致(小写、无空格)。路由时先按通道+账号筛出候选 binding(getEvaluatedBindingsForChannelAccount),再按上述优先级选第一条匹配。 -
完整配置文件示例(可放入
~/.openclaw/openclaw.json;ID 为占位):
{
"agents": {
"list": [
{ "id": "main", "model": "claude-sonnet-4-20250514" },
{ "id": "support", "model": "claude-sonnet-4-20250514" }
]
},
"bindings": [
{ "agentId": "main", "match": { "channel": "telegram", "accountId": "default" } },
{
"agentId": "support",
"match": {
"channel": "telegram",
"accountId": "default",
"peer": { "kind": "group", "id": "-1001234567890" }
}
}
],
"channels": {
"telegram": {
"accounts": {
"default": { "botToken": "YOUR_BOT_TOKEN" }
}
}
}
}
含义:默认 Telegram 消息由 main 处理;仅当消息来自群组 -1001234567890 时由 support 处理(peer 匹配优先于 account/channel 级规则)。
2.4 执行层:队列与 Agent
执行层负责 Agent 的实际运行,并通过队列机制保证并发安全。
模块一览
| 模块 | 是什么 | 负责什么 |
|---|---|---|
| command-queue / lanes | 并发控制队列 | 维护多条 lane(main/cron/subagent/nested + per-session),保证同一 session 串行,不同 session 可并行 |
| runEmbeddedPiAgent | Pi Agent 运行器 | 管理重试、上下文窗口压缩,订阅 Pi 会话流,汇总输出为 payload |
| subscribeEmbeddedPiSession | 会话订阅器 | 把 Pi 的流式事件转换为 onPartialReply/onBlockReply/onToolResult 等回调 |
| runMessageAction | 消息动作执行器 | 根据 payload 中的 send action,解析目标并调用通道 outbound 发送 |
Lane 并发模型
双层入队机制:
入队的都是任务(一个返回 Promise 的异步函数 task)。一次入站消息会先后占两个队:先占 session lane,再占 main lane,然后才执行真正的 Agent 逻辑(runEmbeddedAttempt)。
-
第一层:Session lane(
enqueueSession)- 入队内容:针对该会话的「整次处理」任务,即「先去 main 排队,再执行本次
runEmbeddedAttempt」的包装任务。代码形态为enqueueSession(() => enqueueGlobal(async () => { ... runEmbeddedAttempt(...) }))中传给enqueueSession的那个函数。 - 设计目的:同一 sessionKey 下多条消息按到达顺序排队,同一会话串行,避免同一对话里多轮回复交错、会话状态错乱。
- 粒度:每个 sessionKey 对应一条 session lane,lane 名为
session:+ sessionKey(例如session:agent:main:telegram:direct:123),不同会话的 lane 相互独立,可并行。
- 入队内容:针对该会话的「整次处理」任务,即「先去 main 排队,再执行本次
-
第二层:Main lane(
enqueueGlobal("main"))- 入队内容:单次 Pi Agent 执行任务,即上述包装任务内部传给
enqueueGlobal的async () => { ... runEmbeddedAttempt(...) ... },包含模型解析、会话加载、调用 LLM、工具调用、产出回复等整段逻辑。 - 设计目的:限制全局并发数(main 默认
maxConcurrent=4),避免同时跑过多 Pi/LLM 请求导致资源打满或限流。 - 粒度:全局共用一个 main lane,所有会话的「已通过 session 队」的请求在这里再排一次队。
- 入队内容:单次 Pi Agent 执行任务,即上述包装任务内部传给
设计思路小结:
| 层级 | 入队的「东西」 | 作用 |
|---|---|---|
| Session lane | 该会话的「整次处理」任务(内部会再入 main 并执行 runEmbeddedAttempt) |
同一会话串行,避免同一对话内多轮交错 |
| Main lane | 单次 runEmbeddedAttempt 执行任务 |
控制全局并发,避免同时跑太多 Agent |
因此:先入 session lane 保证会话内顺序,再入 main lane 保证全局并发上限;两者结合既避免单会话乱序,又避免多会话同时把系统撑爆。
2.5 投递层:回发
投递层负责把 Agent 产出的内容发送到正确的目标。
模块一览
| 模块 | 是什么 | 负责什么 |
|---|---|---|
| routeReply | 回复路由器 | 根据 payload、channel、to 等参数,找到对应通道的 outbound 并发送 |
| resolveAgentDeliveryPlan | 投递目标解析器 | 当目标未明确时,根据 session 的 lastChannel/lastTo 等算出 resolvedChannel 和 resolvedTo |
| Channel Outbound | 通道发送实现 | 调用各渠道 API(Telegram Bot API、Discord REST 等)真正发送消息 |
投递流程:
ReplyPayload
→ runMessageAction / routeReply
→ resolveAgentDeliveryPlan(解析目标)
→ Channel Outbound(实际发送)
三、核心流程:一条消息的完整旅程
流程要点
- 通道只关心:把原始事件变成
FinalizedMsgContext,以及收到 outbound 调用时真正发送 - 中间层关心:谁处理(路由)、谁执行(队列 + Agent)、发到哪里(delivery plan + routeReply)
- 会话隔离:通过 sessionKey + lane 保证同一会话的消息串行处理
四、关键设计亮点
4.1 通道插件化
内置通道(Telegram、Discord、Web 等)和扩展通道(extensions/*)实现同一套 ChannelPlugin 接口。Gateway 只认"channelId + outbound",新增通道无需修改核心代码。
4.2 Lane 并发控制
- 全局 lane(main/cron/subagent)按用途隔离
- Session lane 保证同一会话串行
- 双层入队机制防止并发冲突
4.3 会话键与路由
sessionKey 把 agent + channel + account + peer 绑定在一起,配合 bindings 配置实现:
- 多 agent 场景:不同群/私聊可绑定不同 agent
- 多账户场景:同一渠道多账号独立会话
4.4 Delivery Plan
resolveAgentDeliveryPlan 统一计算回复目标,避免:
- 回复错通道
- 回复错会话
- implicit/explicit 目标混淆
4.5 Streaming 与 Final-only
- Agent 流式输出可推送给 Control UI(OpenClaw 的管理界面,用于配置 agent、查看会话、监控状态)
- 外发通道(Telegram/Discord 等)只发最终结果,避免把思考过程暴露给用户
五、源码阅读入口
推荐从以下文件入手,按顺序阅读:
- 入口:
src/entry.ts→src/cli/run-main.ts - Gateway 启动:
src/gateway/server.impl.ts(关注startGatewaySidecars) - 消息处理主线:
src/auto-reply/reply/dispatch-from-config.ts→src/auto-reply/reply/get-reply.ts - 路由:
src/routing/resolve-route.ts - 并发控制:
src/process/command-queue.ts - 投递:
src/auto-reply/reply/route-reply.ts
六、术语速查
| 术语 | 含义 |
|---|---|
| Pi | OpenClaw 内置的 AI Agent 运行时,负责与 LLM 交互、管理会话上下文、执行工具调用 |
| Workspace | 工作空间,隔离的配置单元,包含 agent 配置、知识库、工具权限等 |
| Agent | AI 助手实例,绑定模型、系统提示词、工具集 |
| sessionKey | 会话唯一标识,规范格式为 agent:<agentId>:<rest>,其中 rest 随会话类型变化(如 main、telegram:group:id、discord:channel:id、channel:direct:peerId 等)。同一 sessionKey 串行处理,lane 名为 session: + sessionKey。 |
| lane | 并发队列的划分单位,用于隔离不同类型的任务 |
| FinalizedMsgContext | 归一化后的消息上下文,包含 channel、accountId、peerId、userId、text、attachments 等字段 |
| ResolvedAgentRoute | 路由结果,包含 agentId、sessionKey、mainSessionKey |
| ReplyPayload | Agent 产出的回复内容,核心字段包括:type(回复类型,如 text/image/markdown)、text(文本内容)、to(目标地址,可选)、channel(目标通道,可选)、attachments(附件列表,可选)。 |
| outbound | 通道的发送接口实现 |
| bindings | 配置项,定义哪个位置(群/私聊/账号)使用哪个 agent |
| Hook | 钩子函数,在消息处理的特定时机触发自定义逻辑 |
| Control UI | OpenClaw 的管理界面,用于配置 agent、查看会话、监控状态。Agent 的流式输出可推送到 Control UI |
| guild | 平台的顶级组织单元(Discord Server、Slack Workspace 等) |
| peer | 消息的具体来源位置(Discord 频道、Telegram 群组等) |
| team | Slack 特有的组织概念 |
| 辅助服务(Sidecars) | 与网关主进程同时启动的辅助模块,包括 ChannelManager、Hooks、插件服务、定时任务等 |
| Commander | Node.js 命令行参数解析库,OpenClaw 用它构建 CLI 命令树 |
更多推荐




所有评论(0)