Skip to main content

网关内部机制

消息网关是一个长期运行的进程,通过统一的架构连接 Hermes 与超过 14 个外部消息平台。

核心文件

文件用途
gateway/run.pyGatewayRunner — 主循环、斜杠命令处理、消息分发(约 7,500 行)
gateway/session.pySessionStore — 对话持久化与会话密钥构建
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) │
└─────────────────────────────────────────────────┘

消息流转流程

当来自任意平台的消息到达时:

  1. 平台适配器 接收原始事件,并将其标准化为一个 MessageEvent
  2. 基础适配器 检查活跃会话保护:
    • 如果该会话正在运行代理 → 将消息加入队列,并设置中断事件
    • 若为 /approve/deny/stop → 绕过保护(直接分发)
  3. GatewayRunner._handle_message() 接收事件:
    • 通过 _session_key_for_source() 解析会话密钥(格式:agent:main:{platform}:{chat_type}:{chat_id}
    • 检查授权状态(详见授权部分)
    • 判断是否为斜杠命令 → 分发至命令处理器
    • 检查代理是否已在运行 → 拦截如 /stop/status 等命令
    • 否则 → 创建 AIAgent 实例并启动对话
  4. 响应 通过平台适配器返回

会话密钥格式

会话密钥编码了完整的路由上下文:

agent:main:{platform}:{chat_type}:{chat_id}

例如:agent:main:telegram:private:123456789

支持线程感知的平台(如 Telegram 论坛主题、Discord 线程、Slack 线程)可能在 chat_id 部分包含线程 ID。切勿手动构造会话密钥 —— 必须始终使用 build_session_key() 来自 gateway/session.py

两级消息保护机制

当代理正在运行时,传入的消息需经过两道顺序检查:

  1. 一级 — 基础适配器 (gateway/platforms/base.py):检查 _active_sessions。若会话处于活动状态,则将消息排队至 _pending_messages 并设置中断事件。此步骤在消息抵达网关运行器之前拦截。

  2. 二级 — 网关运行器 (gateway/run.py):检查 _running_agents。拦截特定命令(/stop/new/queue/status/approve/deny),并按需路由。其余所有内容触发 running_agent.interrupt()

必须在代理被阻塞期间仍能抵达运行器的命令(如 /approve)通过 await self._message_handler(event) 内联分发 —— 它们绕过后台任务系统以避免竞态条件。

授权机制

网关采用多层授权检查,按顺序执行:

  1. 平台级全允许标志(如 TELEGRAM_ALLOW_ALL_USERS)—— 若启用,则该平台上所有用户均被授权
  2. 平台白名单(如 TELEGRAM_ALLOWED_USERS)—— 逗号分隔的用户 ID 列表
  3. 私聊配对—— 已认证用户可通过配对码绑定新用户
  4. 全局全允许GATEWAY_ALLOW_ALL_USERS)—— 若启用,则跨所有平台的所有用户均被授权
  5. 默认:拒绝—— 未授权用户将被拒绝

私聊配对流程

Admin: /pair
Gateway: "Pairing code: ABC123. Share with the user."
New user: ABC123
Gateway: "Paired! You're now authorized."

配对状态保存在 gateway/pairing.py 中,重启后仍可保留。

斜杠命令分发

网关中所有斜杠命令均通过相同的解析管道:

  1. hermes_cli/commands.py 提取的 resolve_command() 将输入映射为标准名称(支持别名、前缀匹配)
  2. 标准名称与 GATEWAY_KNOWN_COMMANDS 进行比对
  3. _handle_message() 中的处理器根据标准名称进行分发
  4. 部分命令受配置限制(gateway_config_gateCommandDef 上)

运行中代理保护机制

禁止在代理处理过程中执行的命令会提前被拒绝:

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/.envAPI 密钥、机器人令牌、平台凭证
~/.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
├── 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

令牌锁机制

使用唯一凭据连接的适配器会在 acquire_scoped_lock() 中调用 connect(),并在 release_scoped_lock() 中调用 disconnect()。这可防止两个配置文件同时使用同一机器人令牌。

投递路径

出站投递(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)时:

  1. 网关为每条消息创建一个 AIAgent,附带会话 ID
  2. MemoryManager 使用会话上下文初始化提供者
  3. 提供者的工具(如 honcho_profileviking_search)通过以下路径路由:
AIAgent._invoke_tool()
→ self._memory_manager.handle_tool_call(name, args)
→ provider.handle_tool_call(name, args)
  1. 会话结束/重置时,on_session_end() 触发清理和最终数据刷新

内存刷新生命周期

当会话被重置、恢复或过期时:

  1. 内置记忆体被刷新至磁盘
  2. 内存提供者的 on_session_end() 钩子被触发
  3. 临时 AIAgent 执行一次仅含记忆体的对话轮次
  4. 随后上下文被丢弃或归档

后台维护

网关在处理消息的同时运行周期性维护任务:

  • 定时任务触发 —— 检查任务调度并触发到期任务
  • 会话过期清理 —— 在超时后清除废弃会话
  • 内存主动刷新 —— 在会话过期前主动刷新内存
  • 缓存刷新 —— 刷新模型列表和提供者状态

进程管理网关以长期运行的进程形式运行,可通过以下方式管理:

  • 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 扫描来终止所有网关进程(用于更新时)。

相关文档