网关内部机制
消息网关是长期运行的进程,通过统一架构连接 Hermes 与 14+ 个外部消息平台。
核心文件
| 文件 | 用途 |
|---|---|
gateway/run.py | GatewayRunner — 主循环、斜杠命令处理、消息分发(约 9,000 行) |
gateway/session.py | SessionStore — 对话持久化与会话密钥构建 |
gateway/delivery.py | 向目标平台/渠道发送出站消息 |
gateway/pairing.py | 用户授权的私聊配对流程 |
gateway/channel_directory.py | 将聊天 ID 映射为可读名称,用于定时任务投递 |
gateway/hooks.py | 钩子发现、加载及生命周期事件分发 |
gateway/mirror.py | 跨会话消息镜像功能,支持 send_message |
gateway/status.py | 用于配置范围内的网关实例的令牌锁管理 |
gateway/builtin_hooks/ | 始终注册的钩子(例如 BOOT.md 系统提示钩子) |
gateway/platforms/ | 平台适配器(每个消息平台一个) |
架构概览
┌─────────────────────────────────────────────────┐
│ GatewayRunner │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Telegram │ │ Discord │ │ Slack │ ... │
│ │ Adapter │ │ Adapter │ │ Adapter │ │
│ └─────┬─────┘ └─────┬────┘ └─────┬────┘ │
│ │ │ │ │
│ └──────────────┼──────────────┘ │
│ ▼ │
│ _handle_message() │
│ │ │
│ ┌────────────┼────────────┐ │
│ ▼ ▼ ▼ │
│ Slash command AIAgent Queue/BG │
│ dispatch creation sessions │
│ │ │
│ ▼ │
│ SessionStore │
│ (SQLite persistence) │
└─────────────────────────────────────────────────┘
消息流转流程
当来自任意平台的消息到达时:
- 平台适配器 接收原始事件,将其标准化为
MessageEvent - 基础适配器 检查活跃会话保护:
- 若该会话正在运行代理 → 将消息加入队列,并设置中断事件
- 若为
/approve、/deny、/stop→ 绕过保护(直接执行)
- GatewayRunner._handle_message() 接收事件:
- 通过
_session_key_for_source()解析会话密钥(格式:agent:main:{platform}:{chat_type}:{chat_id}) - 检查授权(见下文授权部分)
- 判断是否为斜杠命令 → 分发至命令处理器
- 检查代理是否已在运行 → 拦截如
/stop、/status等命令 - 否则 → 创建
AIAgent实例并启动对话
- 通过
- 响应 通过平台适配器返回
会话密钥格式
会话密钥编码了完整的路由上下文:
agent:main:{platform}:{chat_type}:{chat_id}
例如:agent:main:telegram:private:123456789
支持线程感知的平台(如 Telegram 论坛主题、Discord 线程、Slack 线程)可能在 chat_id 部分包含线程 ID。切勿手动构造会话密钥 —— 必须始终使用 build_session_key() 从 gateway/session.py 获取。
两级消息保护机制
当代理正在运行时,进入的消息需经过两层顺序保护:
-
第一级 — 基础适配器(
gateway/platforms/base.py):检查_active_sessions。若会话处于活动状态,则将消息排队至_pending_messages并设置中断事件。此步骤在消息到达网关运行器前拦截。 -
第二级 — 网关运行器(
gateway/run.py):检查_running_agents。拦截特定命令(/stop、/new、/queue、/status、/approve、/deny),并按需路由。其余消息触发running_agent.interrupt()。
必须在代理被阻塞期间仍能抵达运行器的命令(如 /approve)通过 await self._message_handler(event) 内联分发 —— 它们绕过后台任务系统以避免竞态条件。
授权机制
网关采用多层级授权检查,按顺序评估:
- 平台级全允许标志(如
TELEGRAM_ALLOW_ALL_USERS)—— 若启用,则该平台上所有用户均被授权 - 平台白名单(如
TELEGRAM_ALLOWED_USERS)—— 逗号分隔的用户 ID 列表 - 私聊配对—— 已认证用户可通过配对码绑定新用户
- 全局全允许(
GATEWAY_ALLOW_ALL_USERS)—— 若启用,则所有平台上的所有用户均被授权 - 默认:拒绝—— 未授权用户将被拒绝
私聊配对流程
Admin: /pair
Gateway: "Pairing code: ABC123. Share with the user."
New user: ABC123
Gateway: "Paired! You're now authorized."
配对状态保存在 gateway/pairing.py 中,重启后仍保持有效。
斜杠命令分发
网关中所有斜杠命令均通过相同的解析管道:
resolve_command()从hermes_cli/commands.py提取输入,映射为标准名称(处理别名、前缀匹配)- 标准名称与
GATEWAY_KNOWN_COMMANDS进行比对 - 处理器在
_handle_message()中根据标准名称进行分发 - 部分命令受配置限制(
gateway_config_gate在CommandDef上生效)
运行中代理保护机制
禁止在代理处理过程中执行的命令会提前被拒绝:
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.py # QQ Bot (Tencent QQ) via Official API v2
├── 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
令牌锁机制
使用唯一凭证连接的适配器会在 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:step | 代理完成一次工具调用迭代时 |
agent:end | 代理完成并返回响应时 |
command:* | 执行任何斜杠命令时 |
钩子从 gateway/builtin_hooks/(始终激活)和 ~/.hermes/hooks/(用户安装)中发现。每个钩子是一个包含 HOOK.yaml 清单和 handler.py 的目录。
内存提供者集成
当启用内存提供者插件(如 Honcho)时:
- 网关为每条消息创建一个
AIAgent,附带会话 ID 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,执行仅含记忆体的对话回合 - 然后丢弃或归档上下文
后台维护任务
网关在处理消息的同时运行定期维护任务:- Cron ticking — 检查任务调度并触发已到期的任务
- 会话超时清理 — 清理超时后未使用的会话
- 内存主动刷新 — 在会话过期前主动释放内存
- 缓存刷新 — 刷新模型列表及服务提供商状态
进程管理
网关以长期运行的进程形式运行,通过以下方式管理:
hermes gateway start/hermes gateway stop— 手动控制systemctl(Linux)或launchctl(macOS) — 服务管理- PID 文件位于
~/.hermes/gateway.pid— 针对特定配置文件的进程追踪
配置文件作用域 vs 全局:start_gateway() 使用配置文件作用域的 PID 文件。hermes gateway stop 仅停止当前配置文件对应的网关实例。hermes gateway stop --all 采用全局 ps aux 扫描机制终止所有网关进程(用于更新时的强制关闭)。