网关内部机制
消息网关是将 Hermes 连接到 20 个以上外部消息平台的长运行进程,采用统一架构实现。
关键文件
| 文件 | 用途 |
|---|---|
gateway/run.py | GatewayRunner — 主循环、斜杠命令、消息分发(大文件;查看 git 获取当前行数) |
gateway/session.py | SessionStore — 对话持久化和会话键构建 |
gateway/delivery.py | 向目标平台/频道的出站消息投递 |
gateway/pairing.py | 用于用户授权的 DM 配对流程 |
gateway/channel_directory.py | 将聊天 ID 映射到人类可读名称,用于定时任务投递 |
gateway/hooks.py | 钩子发现、加载和生命周期事件分发 |
gateway/mirror.py | 用于 send_message 的跨会话消息镜像 |
gateway/status.py | 配置文件作用域网关实例的 token 锁管理 |
gateway/builtin_hooks/ | 始终注册的钩子扩展点(未预装) |
gateway/platforms/ | 平台适配器(每个消息平台一个) |
架构概览
┌─────────────────────────────────────────────────┐
│ GatewayRunner │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Telegram │ │ Discord │ │ Slack │ │
│ │ Adapter │ │ Adapter │ │ Adapter │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └─────────────┼─────────────┘ │
│ ▼ │
│ _handle_message() │
│ │ │
│ ┌───────────┼───────────┐ │
│ ▼ ▼ ▼ │
│ Slash command AIAgent Queue/BG │
│ dispatch creation sessions │
│ │ │
│ ▼ │
│ SessionStore │
│ (SQLite persistence) │
└───────┴─────────────┴─────────────┴─────────────┘
消息流
当任意平台有消息到达时:
- 平台适配器接收原始事件,将其规范化为
MessageEvent - 基础适配器检查活跃会话守卫:
- 如果该会话的 Agent 正在运行 → 将消息入队,设置中断事件
- 如果是
/approve、/deny、/stop→ 绕过守卫(直接分发)
- GatewayRunner._handle_message() 接收事件:
- 通过
_session_key_for_source()解析会话键(格式:agent:main:{platform}:{chat_type}:{chat_id}) - 检查授权(见下方授权部分)
- 检查是否为斜杠命令 → 分发给命令处理器
- 检查 Agent 是否正在运行 → 拦截
/stop、/status等命令 - 否则 → 创建
AIAgent实例并运行对话
- 通过
- 响应通过平台适配器发回
会话键格式
会话键编码了完整的路由上下文:
agent:main:{platform}:{chat_type}:{chat_id}
例如:agent:main:telegram:private:123456789
支持线程的平台(Telegram 论坛话题、Discord 线程、Slack 线程)可能在 chat_id 部分包含线程 ID。永远不要手动构建会话键——始终使用 gateway/session.py 中的 build_session_key()。
双层消息守卫
当 Agent 正在运行时,传入消息会经过两个连续的守卫:
-
第一层 — 基础适配器(
gateway/platforms/base.py):检查_active_sessions。如果会话处于激活状态,将消息加入_pending_messages队列并设置中断事件。这在消息到达网关运行器之前就进行拦截。 -
第二层 — 网关运行器(
gateway/run.py):检查_running_agents。拦截特定命令(/stop、/new、/queue、/status、/approve、/deny)并进行相应路由。其他所有命令触发running_agent.interrupt()。
在 Agent 阻塞时必须到达运行器的命令(如 /approve)通过 await self._message_handler(event) 直接分发——它们绕过后台任务系统以避免竞态条件。
授权
网关按顺序评估多层授权检查:
- 每平台允许所有用户标志(如
TELEGRAM_ALLOW_ALL_USERS)——若设置,该平台所有用户均已授权 - 平台允许列表(如
TELEGRAM_ALLOWED_USERS)——逗号分隔的用户 ID - DM 配对 ——已认证用户可通过配对码为新用户授权
- 全局允许所有用户(
GATEWAY_ALLOW_ALL_USERS)——若设置,所有平台的所有用户均已授权 - 默认:拒绝 — 未授权用户被拒绝
DM 配对流程
管理员: /pair
网关: "配对码:ABC123。请与用户分享。"
新用户: ABC123
网关: "配对成功!您现已获得授权。"
配对状态持久化在 gateway/pairing.py 中,重启后仍然有效。
斜杠命令分发
网关中的所有斜杠命令都流经相同的解析流程:
hermes_cli/commands.py中的resolve_command()将输入映射到规范名称(处理别名、前缀匹配)- 规范名称与
GATEWAY_KNOWN_COMMANDS进行比对 _handle_message()中的处理器根据规范名称进行分发- 部分命令受配置约束(
CommandDef上的gateway_config_gate)
运行中 Agent 守卫
当 Agent 正在处理时,不得执行的命令会被提前拒绝:
if _quick_key in self._running_agents:
if canonical == "model":
return "⏳ Agent is running — wait for it to finish or /stop first."
绕过命令(/stop、/new、/approve、/deny、/queue、/status)有特殊处理。
配置来源
网关从多个来源读取配置:
| 来源 | 提供内容 |
|---|---|
~/.hermes/.env | API 密钥、机器人令牌、平台凭证 |
~/.hermes/config.yaml | 模型设置、工具配置、显示选项 |
| 环境变量 | 覆盖以上任何配置 |
与 CLI(使用带硬编码默认值的 load_cli_config())不同,网关直接通过 YAML 加载器读取 config.yaml。这意味着存在于 CLI 默认值字典但不在用户配置文件中的配置键,在 CLI 和网关之间的行为可能有所不同。
平台适配器
每个消息平台在 gateway/platforms/ 中都有对应的适配器:
gateway/platforms/
├── base.py # BaseAdapter — shared logic for all platforms
├── telegram.py # Telegram Bot API (long polling or webhook)
├── discord.py # Discord bot via discord.py
├── slack.py # Slack Socket Mode
├── whatsapp.py # WhatsApp Business Cloud API
├── signal.py # Signal via signal-cli REST API
├── matrix.py # Matrix via mautrix (optional E2EE)
├── mattermost.py # Mattermost WebSocket API
├── email.py # Email via IMAP/SMTP
├── sms.py # SMS via Twilio
├── dingtalk.py # DingTalk WebSocket
├── feishu.py # Feishu/Lark WebSocket or webhook
├── wecom.py # WeCom (WeChat Work) callback
├── weixin.py # Weixin (personal WeChat) via iLink Bot API
├── bluebubbles.py # Apple iMessage via BlueBubbles macOS server
├── qqbot/ # QQ 机器人(腾讯 QQ)通过官方 API v2(子包:adapter.py, crypto.py, keyboards.py, …)
├── yuanbao.py # 元宝(腾讯)DM/群组适配器
├── feishu_comment.py # 飞书文档/云盘评论回复处理器
├── msgraph_webhook.py # Microsoft Graph 变更通知 webhook(Teams、Outlook 等)
├── webhook.py # Inbound/outbound webhook adapter
├── api_server.py # REST API server adapter
└── homeassistant.py # Home Assistant conversation integration
适配器实现统一接口:
connect()/disconnect()— 生命周期管理send_message()— 出站消息投递on_message()— 入站消息规范化 →MessageEvent
Token 锁
使用唯一凭证连接的适配器在 connect() 中调用 acquire_scoped_lock(),在 disconnect() 中调用 release_scoped_lock()。这防止两个配置文件同时使用同一机器人令牌。
投递路径
出站投递(gateway/delivery.py)处理:
- 直接回复 — 将响应发回到发起聊天
- 主频道投递 — 将定时任务输出和后台结果路由到配置的主频道
- 显式目标投递 —
send_message工具指定telegram:-1001234567890 - 跨平台投递 — 投递到与发起消息不同的平台
定时任务投递不会镜像到网关会话历史中——它们只存在于各自的定时任务会话中。这是刻意的设计选择,以避免消息交替违规。
钩子
网关钩子是响应生命周期事件的 Python 模块:
网关钩子事件
| 事件 | 触发时机 |
|---|---|
gateway:startup | 网关进程启动 |
session:start | 新对话会话开始 |
session:end | 会话完成或超时 |
session:reset | 用户通过 /new 重置会话 |
agent:start | Agent 开始处理消息 |
agent:step | Agent 完成一次工具调用迭代 |
agent:end | Agent 完成并返回响应 |
command:* | 执行任意斜杠命令 |
钩子从 gateway/builtin_hooks/(扩展点 — 当前在发布的发行版中为空;_register_builtin_hooks() 是无操作存根)和 ~/.hermes/hooks/(用户安装)发现。每个钩子是包含 HOOK.yaml 清单文件和 handler.py 的目录。
记忆提供商集成
当记忆提供商插件(如 Honcho)启用时:
- 网关为每条消息创建带会话 ID 的
AIAgent MemoryManager用会话上下文初始化提供商- 提供商工具(如
honcho_profile、viking_search)通过以下路径路由:
AIAgent._invoke_tool()
→ self._memory_manager.handle_tool_call(name, args)
→ provider.handle_tool_call(name, args)
- 会话结束/重置时,触发
on_session_end()进行清理和最终数据刷新
记忆刷新生命周期
当会话重置、恢复或过期时:
- 内置记忆刷新到磁盘
- 记忆提供商的
on_session_end()钩子触发 - 临时
AIAgent运行仅记忆的对话轮次 - 上下文随后被丢弃或归档
后台维护
网关在处理消息的同时执行周期性维护:
- 定时任务轮询 — 检查任务调度并触发到期任务
- 会话过期 — 超时后清理废弃会话
- 记忆刷新 — 在会话过期前主动刷新记忆
- 缓存刷新 — 刷新模型列表和提供商状态
进程管理
网关作为长期运行进程管理,通过以下方式控制:
hermes gateway start/hermes gateway stop— 手动控制systemctl(Linux)或launchctl(macOS)— 服务管理- PID 文件位于
~/.hermes/gateway.pid— 配置文件作用域的进程追踪
配置文件作用域与全局:start_gateway() 使用配置文件作用域的 PID 文件。hermes gateway stop 仅停止当前配置文件的网关。hermes gateway stop --all 使用全局 ps aux 扫描来终止所有网关进程(用于更新时)。