Gateway 网关:连接 20+ 平台的统一消息路由
如何用 session key 隔离多用户、用双层守卫防止并发、用 delivery 系统实现任意渠道回复
系列:通过 Hermes 探秘 Agent 工程 | 第 8 篇 上一篇:安全防护体系:当 Agent 拥有终端时如何防止「做出格」的事
问题:一个 Agent,20+ 个入口
当你安装 Hermes,你可能通过以下方式跟它聊天:
- Telegram:Ping bot
- Discord:服务器里召唤 Agent
- Slack:工作群里 /hermes
- 微信:公众号或企业微信机器人
- 命令行:终端里直接 hermes
- REST API:自己写的前端调用
Hermes 需要在所有这些渠道里统一行为——安全配置一样、工具权限一样、输出体验一样。
Hermes 的解决方案是 Gateway:一个长连接进程,作为所有消息渠道的统一入口。
Gateway 是什么
Gateway 是一个独立的后台服务(systemd / launchd),管理所有平台适配器和会话状态。它不执行 LLM 推理本身——它只做消息接收、路由、响应下发。
┌─────────────────────────────────────────────────┐
│ GatewayRunner │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Telegram │ │ Discord │ │ Slack │ ... │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └─────────────┼─────────────┘ │
│ ▼ │
│ _handle_message() │
│ │ │
│ ┌──────────┼──────────┐ │
│ ▼ ▼ ▼ │
│ Slash cmd AIAgent Queue/BG │
│ dispatch creation sessions │
│ │ │
│ ▼ │
│ SessionStore │
│ (SQLite persistence) │
└─────────────────────────────────────────────────┘
Session 模型
Session Key 结构
Gateway 的每个对话都用一个 session key 唯一标识:
agent:main:{platform}:{chat_type}:{chat_id}
例如:
agent:main:telegram:private:123456789(Telegram 私聊)agent:main:discord:channel:987654321(Discord 频道)agent:main:slack:group:ABC123(Slack 群组)
加了话题的平台的 chat_id 里还包含线程 ID(如 Telegram 论坛主题、Discord Thread)。
不要手动构造 session key——始终使用 gateway/session.py 里的 build_session_key() 函数。session key 是数据库查询、缓存映射、路由匹配的唯一 key,拼写错误意味着创建一个孤儿会话或消息投递失败。
SessionStore 持久化
所有会话数据存储在 SQLite 数据库里:
- 消息历史
- 会话元数据(创建时间、来源渠道、启用配置等)
- 工具守卫状态
- 上下文压缩摘要
SessionStore 提供了一套原子 API,让 Gateway 在重启后可以从数据库恢复会话。
消息流:从平台到 AIAgent
一条消息到达 Gateway 后的完整路径:
1. 平台适配器 → MessageEvent
每个平台适配器(Telegram Adapter / Discord Adapter 等)接收各自平台的原始事件,规范化成 Hermes 内部的 MessageEvent。
规范化包含:
- 提取发送者 ID、频道 ID、消息内容
- 分离文本正文与附件
- 识别是否为命令(以
/开头)
2. 基础适配器 → 两层守卫
BasePlatformAdapter 定义了两层消息守卫机制:
L1 — 适配器层守卫:检查 _active_sessions。如果当前会话里已经有 AIAgent 在运行,消息被放入 _pending_messages 队列,并设置 interrupt 事件。这一步确保在消息进入 Gateway 主逻辑之前就被拦截——多个平台的适配器并行运行,不需要每个平台自己实现检查逻辑。
L2 — GatewayRunner 层守卫:检查 _running_agents。对在 AIAgent 运行期间到达的消息,做分流:
/stop、/new、/status、/approve、/deny等管理员命令 → 内联调度(直接调用_message_handler()异步执行)- 普通用户消息 → 触发
running_agent.interrupt(),让 AIAgent 自己决定是打断当前工具执行还是排队处理
这种内联调度避免了背景竞争条件:如果 /approve 排队等待当前任务执行完,而当前任务其实在等用户去按 approve……就死锁了。
3. GatewayRunner → AIAgent
消息通过守卫后,GatewayResolver 决定如何处理:
- Slash 命令:发到
gateway/run.py的命令分发器 - 模型切换中:拦截
/model命令,返回"请等当前任务完成或 /stop" - 普通消息:创建
AIAgent实例,传入当前 session 的配置(模型、工具集、安全设置)
AIAgent 开始执行 run_conversation() 工具调度循环。中间产生的所有输出通过 delivery.py 实时轨递到源平台。
4. 响应下发
gateway/delivery.py 负责消息的出站投递:
- 直接回复:发回到来源平台
- Home Channel 投递:cron job 的输出路由到指定 channel
- 跨平投递:可以指定把消息发到不同的平台(如 Discord 用户通过 Telegram 收回复)
- 显式目标投递:send 命令可以通过
telegram:-100123456:17585这样的 URL 指定任意目标
Cron job 的投递被刻意设计为不在 gateway session 历史里——避免消息交替违规(某些平台限制机器人必须在用户消息后才能回复)。
多层授权
Gateway 按顺序执行以下检查,任一通过即授权:
- 平台级 allow-all(如
TELEGRAM_ALLOW_ALL_USERS):该平台的全部用户被允许 - 平台级 allowlist(如
TELEGRAM_ALLOWED_USERS):逗号分隔的用户 ID - DM pairing:已授权的管理员可以生成配对码,新用户输入配对码完成授权
- 全局 allow-all(
GATEWAY_ALLOW_ALL_USERS):所有平台的所有用户被允许 - 默认:拒绝
这是纵深授权——按粒度从粗到细排列。配对这个设计特别实用——管理员不需要编辑配置文件,直接通过聊天就能给用户解锁。配对状态持久化在 gateway/pairing.py,重启后仍然有效。
平台适配器:插件体系
绝大多数平台适配器写在 plugins/platforms/<name>/adapter.py,少部分旧版直接写在 gateway/platforms/。
统一的适配器接口:
| 方法 | 作用 |
|---|---|
connect() | 连接到该平台 API |
disconnect() | 从容断开连接 |
send_message() | 发送消息到该平台 |
on_message() | 接收平台事件 → 生成 MessageEvent |
几个有趣的细节:
Token Locks:用 acquire_scoped_lock() 防止两个 profile 同时用同一个 bot token。这在部署多 profile(如"公司号"和"测试号")时特别关键——同时重连会踢掉对方的 session。
适配器隔离:每个适配器独立运行在自己的 asyncio task 里。一个适配器崩溃(比如 Discord.py 异常)不会拖垮整个 Gateway——其他平台继续正常工作。
中继适配器(relay adapter):当配置了 GATEWAY_RELAY_URL,Gateway 会通过出站 WebSocket WebSocket 连接到一个"连接器"服务,然后接收 descriptor、inbound 帧。不知道——这样可以对接到没有适配器实现的平台(如某个小众即时通讯协议)。
配置来源
Gateway 的配置来自三层:
| 来源 | 示例 |
|---|---|
~/.hermes/.env | API keys、bot tokens、平台凭证 |
~/.hermes/config.yaml | 模型设置、工具配置、显示选项 |
| 环境变量 | 覆盖上面任何值 |
CLI 和 Gateway 的配置行为不同:CLI 用 load_cli_config() 内置了默认值字典,而 Gateway 直接读 YAML 配置。这意味着同一个配置字段,如果用户没写进 config.yaml,CLI 可能会给默认行为,但 Gateway 可能表现为不同行为。
Gateway 的运营运维
服务安装
# 安装为系统服务
hermes gateway install
# 管理服务
hermes gateway start
hermes gateway stop
hermes gateway restart
hermes gateway status
# systemd 日志
journalctl --user -u hermes-gateway -f
# macOS 日志
tail -f ~/.hermes/logs/gateway.log
安装过程会自动生成 systemd .service 文件(Linux)或 launchd .plist(macOS),设置重启策略(Restart=on-failure / KeepAlive),确保 Gateway 崩溃后自动重启。
优雅重启
Hermes Gateway 支持两种重启方式:
systemd / launchd 重启:SIGTERM → 超时 SIGKILL → 重启。这种重启会中断正在执行的 Agent 任务。
SIGUSR1 重启(Hermes 独有):发送 SIGUSR1 信号 → GatewayRunner 捕获信号 →
request_restart(via_service=True)→ 等待当前正在运行的 AIAgent 完成任务(最多agent.restart_drain_timeout秒)→ 正常退出 → 服务管理器自动重启。
SIGUSR1 重启是无损升级的——新代码加载后,当前任务被完成,后续请求自动发到新进程版本。
Profile 支持
Hermes Gateway 支持多 profile——相同机器上运行多个独立实例:
hermes gateway run --profile prod # 生产实例(绑定 127.0.0.1:8080)
hermes gateway run --profile test # 测试实例(绑定 127.0.0.1:8081)
Profile 通过环境变量 HERMES_PROFILE 隔离:不同的 ~/.hermes/profiles/<profile>/ 目录存储独立配置、独立的数据库文件、独立的 profile Gateway 进程独立的 profile Gateway 实例。
总结
Gateway 是 Hermes 的消息路由器——不思考,只管连接、转发、持久化。它的核心设计:
- Session Key:
agent:main:{platform}:{chat_type}:{chat_id}格式,唯一标识一个对话工程模型 - 双层守卫:适配器层 + GatewayRunner 层,既防止并发又支持 admin 命令打断
- 纵深授权:从粗到细的授权检查 + DM pairing 这个很实用的自助授权模式
- 插件适配器:每个平台一个插件目录,共享
BasePlatformAdapter接口 - 无损升级:SIGUSR1 + drain timeout + 进程服务管理器自动重启
下一篇,我们深入 Hermes 的 Provider 抽象层——如何让 Hermes 同时与 OpenAI、Anthropic、Gemini、本地 Ollama 等多种 LLM 提供商协作。