Hermes Agent 源码解析
第 7 讲:Gateway 与平台适配器——20+ 平台如何共享同一个 Agent 内核
基于 Hermes Agent v0.16.0 源码 · 2026-06-20
一、Gateway:Agent 的"通信中枢"
前六讲我们完成了全局架构地图、对话循环、工具系统、插件系统、Provider 抽象层、记忆与技能系统的深度解析。这一讲聚焦 Hermes 最庞大的子系统——Gateway,它让同一个 Agent 内核同时服务 Telegram、Discord、Slack、微信、飞书、钉钉、WhatsApp、Signal、Email、SMS 等 20+ 平台。
Gateway 不是一个简单的"消息转发器"——它是一个完整的通信运行时:管理多平台连接生命周期、会话隔离、流式响应分发、媒体处理、安全沙箱、跨平台镜像投递……每一个环节都有精密的设计。
📦 源码仓库
https://github.com/NousResearch/hermes-agent
本地源码:~/.hermes/hermes-agent/
本讲核心文件:
gateway/run.py(16292 行)— GatewayRunner 主循环
gateway/platforms/base.py(4884 行)— BasePlatformAdapter ABC
gateway/platform_registry.py(260 行)— 平台注册中心
gateway/config.py(2139 行)— 配置管理 + Platform 枚举
gateway/session.py(1444 行)— 会话管理
gateway/stream_events.py(171 行)— 结构化流事件
gateway/stream_dispatch.py(132 行)— 事件分发器
gateway/delivery.py(433 行)— 投递路由
gateway/mirror.py(168 行)— 跨平台镜像
gateway/platforms/telegram.py, discord.py, ...(20+ 平台适配器)
二、Platform 枚举:动态扩展的平台注册表
Platform 枚举是 Gateway 的"平台字典"。它有一个精妙的设计:内置平台硬编码 + 插件平台动态注册,通过 Python 枚举的 _missing_() 钩子实现无缝扩展。
📄 gateway/config.py (第 136-231 行)
class Platform(Enum):
"""Supported messaging platforms."""
LOCAL = "local"
TELEGRAM = "telegram"
DISCORD = "discord"
WHATSAPP = "whatsapp"
SLACK = "slack"
SIGNAL = "signal"
MATRIX = "matrix"
FEISHU = "feishu"
WECOM = "wecom"
WEIXIN = "weixin"
QQBOT = "qqbot"
DINGTALK = "dingtalk"
# ... 共 20+ 内置平台
@classmethod
def _missing_(cls, value):
"""Accept unknown platform names only for known plugin adapters.
Creates a pseudo-member cached in _value2member_map_ so that
Platform("irc") is Platform("irc") holds True (identity-stable).
"""
# 1. 检查缓存 (已创建的伪成员)
if value in cls._value2member_map_:
return cls._value2member_map_[value]
# 2. 扫描内置插件目录 plugins/platforms/
if value in _Platform__bundled_plugin_names:
pseudo = object.__new__(cls)
pseudo._value_ = value
cls._value2member_map_[value] = pseudo
return pseudo
# 3. 运行时插件注册 (platform_registry)
from gateway.platform_registry import platform_registry
if platform_registry.is_registered(value):
# 同上, 创建伪成员
...
return None # 拒绝任意字符串, 防止枚举污染
设计亮点:通过 _missing_() 钩子,Platform("irc") 可以正常工作——只要 IRC 适配器已注册。这避免了每次添加新平台都要修改核心枚举。同时 _scan_bundled_plugin_platforms() 扫描 plugins/platforms/ 目录,自动发现插件平台。
三、BasePlatformAdapter ABC:平台适配器的契约
所有平台适配器继承 BasePlatformAdapter。这个 ABC 定义了最小必要接口,同时提供了大量可选的默认实现——适配器只需覆盖它需要定制的部分。
1. 核心抽象方法(必须实现)
📄 gateway/platforms/base.py (第 1793-2277 行)
class BasePlatformAdapter(ABC):
"""Base class for platform adapters."""
# ── 必须实现的抽象方法 ──
@abstractmethod
async def connect(self) -> bool:
"""连接到平台并开始接收消息。"""
@abstractmethod
async def disconnect(self) -> None:
"""断开连接。"""
@abstractmethod
async def send(
self,
chat_id: str,
content: str,
reply_to: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None
) -> SendResult:
"""发送消息到聊天。返回 SendResult(success, message_id, error)。"""
# ── 可选覆盖的方法(有默认实现) ──
async def edit_message(self, ...) -> SendResult:
"""编辑已发送消息。默认返回 success=False。"""
return SendResult(success=False, error="Not supported")
async def delete_message(self, ...) -> bool:
"""删除消息。默认返回 False。"""
return False
async def send_typing(self, chat_id, metadata=None) -> None:
"""发送输入指示器。默认 no-op。"""
pass
async def send_draft(self, ...) -> SendResult:
"""流式草稿预览 (Telegram Bot API 9.5+)。默认 raise。"""
raise NotImplementedError(...)
# ── 能力标志 ──
supports_code_blocks: bool = False
# 平台是否渲染 Markdown 代码块
typed_command_prefix: str = "/"
# 用户可输入的命令前缀 (Slack/Matrix 用 "!")
REQUIRES_EDIT_FINALIZE: bool = False
# 是否需要显式 finalize (钉钉 AI Cards 等)
2. 会话隔离与中断控制
BasePlatformAdapter 内置了会话级并发控制——每个会话(session)在同一时间只能处理一个消息,防止并发冲突:
BasePlatformAdapter 会话控制结构:
_active_sessions: Dict[str, asyncio.Event]
└─ 每个 session_key → 一个 Event 对象
└─ 消息到达时: 如果 Event.is_set() → 会话忙
└─ 处理完成后: Event.clear() → 会话空闲
_session_tasks: Dict[str, asyncio.Task]
└─ session → 正在处理该会话的 Task
└─ /stop, /new, /reset 可取消正确的 Task
└─ 避免旧 Task 的 finally 块删除新 Task 的锁
_pending_messages: Dict[str, MessageEvent]
└─ 照片连拍/相册合并: 多条 PHOTO 事件
合并为一条, 避免截断用户意图
_busy_text_mode: str ("interrupt" / "queue" / "ignore")
└─ 会话忙时的策略:
interrupt → 中断当前, 处理新的
queue → 排队等待
ignore → 丢弃并回复 "busy"
输入指示器 (_typing_paused):
└─ 审批等待等场景暂停 "正在输入..."
└─ 避免审批弹窗和输入指示器同时出现3. 流式渲染钩子:适配器决定呈现方式
这是 Gateway 架构中最精妙的设计之一——Agent 发出结构化事件,适配器决定如何呈现:
📄 gateway/platforms/base.py (第 1990-2051 行)
class BasePlatformAdapter(ABC):
# ── 结构化流事件渲染 ──
# 适配器决定 *如何* 呈现每个流事件
# 默认实现 1:1 复现历史行为, 适配器覆盖以实现原生渲染
def render_message_event(self, event, sink) -> None:
"""渲染 MessageChunk / MessageStop / Commentary。
默认: 映射到 stream consumer 的已有原语。
Telegram 覆盖: 用 MarkdownV2 渲染 ```bash``` 代码块。
iMessage 覆盖: 丢弃工具 chrome (纯文本平台无法格式化)。
"""
if isinstance(event, MessageChunk):
sink.on_delta(event.text)
elif isinstance(event, MessageStop):
if not event.final:
sink.on_segment_break()
elif isinstance(event, Commentary):
sink.on_commentary(event.text)
def format_tool_event(self, event, *, mode="all",
preview_max_len=40) -> Optional[str]:
"""返回工具调用的渲染 chrome, 或 None 表示"吃掉"该事件。
默认: emoji + tool_name + 短预览 (40 字符限制)。
纯文本平台: 返回 None (不显示工具进度)。
verbose 模式: 显示完整 args dict。
"""
emoji = get_tool_emoji(event.tool_name, default="⚙️")
if mode == "verbose":
return f"{emoji} {event.tool_name}({keys})\n{args}"
preview = event.preview or ""
if len(preview) > 40:
preview = preview[:37] + "..."
return f"{emoji} {event.tool_name}: \"{preview}\""
设计哲学:Agent 不关心 Telegram 用什么 markdown、Discord 用什么 embed、钉钉用什么 AI Card——Agent 只发出结构化事件,Gateway 负责翻译。这实现了真正的关注点分离。
四、PlatformRegistry:插件化平台发现
PlatformRegistry 是平台适配器的注册中心。插件可以通过它注册新平台,无需修改核心代码:
📄 gateway/platform_registry.py (第 38-159 行)
@dataclass
class PlatformEntry:
"""单个平台适配器的元数据和工厂。"""
name: str # 配置标识: "irc", "viber"
label: str # 人类可读: "IRC", "Viber"
adapter_factory: Callable # 工厂函数, 接收 PlatformConfig
check_fn: Callable # 依赖是否可用?
validate_config: Callable # 配置是否正确?
is_connected: Callable # 是否已连接?
setup_fn: Callable # 交互式配置向导
required_env: list # hermes setup 显示的变量
install_hint: str # 缺失时的安装提示
# ── 高级特性 ──
platform_hint: str # 注入系统提示词的平台指引
emoji: str # CLI/Gateway 显示的 emoji
pii_safe: bool # 是否脱敏 PII
max_message_length: int # 智能分块的消息上限
cron_deliver_env_var: str # Cron 投递目标
standalone_sender_fn: Callable # 独立进程发送 (Cron 专用)
env_enablement_fn: Callable # 环境变量自动配置
apply_yaml_config_fn: Callable # YAML→环境变量桥接
class PlatformRegistry:
def register(self, entry: PlatformEntry) -> None:
"""注册平台适配器。同名覆盖 (last-writer wins)。"""
def create_adapter(self, name: str, config: PlatformConfig):
"""查找并实例化适配器。"""
entry = self.get(name)
if entry:
return entry.adapter_factory(config)
# 回退到内置 if/elif 链 (向后兼容)
插件使用示例:
插件侧注册:
from gateway.platform_registry import platform_registry, PlatformEntry
platform_registry.register(PlatformEntry(
name="irc",
label="IRC",
adapter_factory=lambda cfg: IRCAdapter(cfg),
check_fn=lambda: importlib.util.find_spec("irc"),
validate_config=lambda cfg: bool(cfg.extra.get("server")),
required_env=["IRC_SERVER"],
install_hint="pip install irc",
platform_hint="You are on IRC. Do not use markdown.",
emoji="💬",
))
Gateway 侧使用:
adapter = platform_registry.create_adapter("irc", platform_config)
# 插件优先, 未找到则回退到内置适配器五、GatewayRunner:16K 行的通信运行时
gateway/run.py 是 Hermes 最大的单一文件(16292 行)。它包含 GatewayRunner 类——Gateway 的大脑,管理所有平台的连接生命周期、消息路由、流式响应、安全策略。
1. 启动管线:从配置到在线
GatewayRunner 启动管线:
start_gateway()
│
├─ 1. Windows 特殊处理 (_ensure_windows_gateway_venv_imports)
│ └─ 修复 venv 包可见性 (MCP SDK 等)
│
├─ 2. 安装事件循环异常处理器
│ └─ _gateway_loop_exception_handler
│ └─ 捕获瞬态网络错误, 防止网关崩溃
│ └─ 白名单: TimedOut, NetworkError, ConnectError...
│
├─ 3. 加载配置 (GatewayConfig)
│ └─ config.yaml + 环境变量桥接
│ └─ 平台发现: 内置 + 插件
│
├─ 4. 创建 GatewayRunner 实例
│ └─ Agent 缓存: LRU + TTL 驱逐
│ _AGENT_CACHE_MAX_SIZE = 128
│ _AGENT_CACHE_IDLE_TTL_SECS = 3600
│
├─ 5. 为每个平台创建适配器
│ └─ platform_registry.create_adapter()
│ └─ 设置消息处理器: adapter.set_message_handler()
│ └─ 设置会话存储: adapter.set_session_store()
│
├─ 6. 并行连接所有平台
│ └─ asyncio.gather(*[adapter.connect() for ...])
│ └─ 连接超时: _PLATFORM_CONNECT_TIMEOUT_SECS_DEFAULT = 30s
│
└─ 7. 启动后台任务
└─ Agent 缓存驱逐 (_session_expiry_watcher)
└─ 图片缓存清理 (cleanup_image_cache)
└─ 音频缓存清理 (cleanup_audio_cache)2. 消息处理管线:从用户输入到 Agent 响应
消息处理管线 (GatewayRunner._process_message_background):
用户消息到达
│
├─ Phase 0: 预处理
│ ├─ coerce_plaintext_gateway_command()
│ │ └─ DM 中 "restart gateway" → "/restart"
│ │ └─ 防止自然语言触发自我重启 (#16007)
│ │
│ ├─ 主题恢复 (_apply_topic_recovery)
│ │ └─ Telegram DM Topic 模式: 修正 thread_id
│ │
│ ├─ 会话忙检查 (_active_sessions)
│ │ └─ interrupt / queue / ignore 策略
│ │
│ └─ 用户授权检查 (_is_user_authorized)
│ └─ 环境白名单: TELEGRAM_ALLOWED_USERS
│ └─ 特殊: 适配器自有策略 (WeCom/Weixin) 跳过
│
├─ Phase 1: 会话恢复/创建
│ ├─ 查找或创建 SessionContext
│ ├─ 加载历史 (_build_gateway_agent_history)
│ │ └─ 保留 assistant 推理字段 (reasoning, reasoning_content,
│ │ codex_reasoning_items, codex_message_items, finish_reason)
│ │ └─ Telegram 观察上下文分离 (observed group chatter)
│ │
│ ├─ 自动继续检查 (_is_fresh_gateway_interruption)
│ │ └─ 新鲜窗口: 1 小时 (可配置)
│ │ └─ 旧中断标记不自动恢复 (#31066)
│ │
│ └─ 创建/复用 AIAgent 实例
│ └─ LRU 缓存, 128 上限, 1h TTL
│
├─ Phase 2: Agent 执行
│ ├─ 流式回调 → GatewayEventDispatcher
│ │ └─ 结构化事件: MessageChunk, ToolCallChunk, Commentary...
│ │ └─ 适配器渲染: Telegram draft / Discord edit / 钉钉 AI Card
│ │
│ ├─ 工具进度队列 (send_progress_messages)
│ │ └─ 独立 drain 循环, 不与流式响应竞争
│ │
│ └─ 响应截断与分块
│ └─ UTF-16 长度限制 (Telegram: 4096 code units)
│ └─ 二进制搜索安全截断点 (不劈半 surrogate pair)
│
├─ Phase 3: 后处理
│ ├─ 持久化到会话存储 (SQLite)
│ ├─ 记忆同步 (sync_all → 后台线程)
│ ├─ 媒体提取 (extract_media / extract_local_files)
│ │ └─ MEDIA:/path/to/file.png → 原生附件投递
│ │
│ └─ 清理: 释放会话锁, 清除回调
│
└─ 错误处理
├─ Provider 错误 → 用户友好分类 (认证/限流/策略)
├─ 密钥脱敏 (_redact_gateway_user_facing_secrets)
└─ Telegram 特殊: 噪声状态过滤3. 安全防线:多层防护
Gateway 是面向网络的,安全设计极其重要。源码中有五层安全防线:
📄 gateway/run.py (第 71-136 行)
# 第一层: 瞬态网络错误白名单
# 防止 TimedOut/NetworkError 杀死整个网关进程
_transient_class_names = {
"TimedOut", "NetworkError", "ReadError", "WriteError",
"ConnectError", "ConnectTimeout", "ReadTimeout", ...
}
# 第二层: Provider 错误分类与脱敏
# 用户看到分类摘要, 不看到原始 HTTP body
_GATEWAY_PROVIDER_ERROR_RE = re.compile(
r"api\s+(?:call\s+)?failed|provider\s+authentication\s+failed|...")
# 第三层: 密钥脱敏
_GATEWAY_SECRET_PATTERNS = (
re.compile(r"\bsk-[A-Za-z0-9][A-Za-z0-9_\-]{12,}\b"), # OpenAI
re.compile(r"\bgh[pousr]_[A-Za-z0-9_]{20,}\b"), # GitHub
re.compile(r"\bxox[baprs]-[A-Za-z0-9\-]{20,}\b"), # Slack
re.compile(r"\bhf_[A-Za-z0-9]{20,}\b"), # HuggingFace
re.compile(r"\bglpat-[A-Za-z0-9_\-]{20,}\b"), # GitLab
re.compile(r"(?i)\b(Bearer\s+)[A-Za-z0-9._\-]{20,}\b"), # Generic
)
# 第四层: Telegram 噪声过滤
# 压缩摘要/重试/降级等内部状态不推送到聊天
_TELEGRAM_NOISY_STATUS_RE = re.compile(
r"auxiliary\s+.+?\s+failed|compression\s+summary\s+failed|...")
# 第五层: 媒体投递路径验证
# validate_media_delivery_path() 在 base.py 中
# 白名单 (缓存目录) + 黑名单 (凭证目录) + 时间窗口
六、流式事件系统:结构化交付契约
历史上海报通过一堆松散类型的回调函数驱动 Gateway 投递(stream_delta_callback(text), tool_progress_callback(event_type, tool_name, ...)……),导致 Telegram 上工具进度气泡和流式草稿互相竞争。新架构用结构化事件彻底解耦:
1. 事件词汇表
📄 gateway/stream_events.py (第 43-145 行)
# ── 消息事件 ──
@dataclass(frozen=True)
class MessageChunk:
"""流式助手文本增量。"""
text: str
@dataclass(frozen=True)
class MessageStop:
"""当前助手消息段完成。"""
final: bool = False # True = 整个 turn 结束
@dataclass(frozen=True)
class Commentary:
"""工具迭代间的完整中间消息。"""
text: str
# ── 工具调用事件 ──
@dataclass(frozen=True)
class ToolCallChunk:
"""工具调用开始。"""
tool_name: str
preview: Optional[str] = None
args: Optional[Dict[str, Any]] = None
index: int = 0 # 单调索引, 关联 start/finish
@dataclass(frozen=True)
class ToolCallFinished:
"""工具调用完成。"""
tool_name: str
duration: float = 0.0
ok: bool = True
index: int = 0
# ── 网关控制事件 ──
@dataclass(frozen=True)
class LongToolHint:
"""工具运行超时的新手引导。"""
tool_name: str = ""
duration: float = 0.0
@dataclass(frozen=True)
class GatewayNotice:
"""网关级控制消息 (restart, online, long_run)。"""
kind: str
text: str = ""
extra: Dict[str, Any] = field(default_factory=dict)
StreamEvent = Union[
MessageChunk, MessageStop, Commentary,
ToolCallChunk, ToolCallFinished,
LongToolHint, GatewayNotice,
] # 显式联合, 穷尽 match 时缺失 case = 类型错误
关键约束:事件描述传输,不描述上下文。它们不持久化到对话历史——历史由 Agent 拥有。Gateway 选择"吃掉"某些事件(如纯文本平台丢弃工具 chrome)不会改变 Agent 存储的字节。
2. GatewayEventDispatcher:同步路由器
Dispatcher 是 Agent 和 Gateway 之间的同步路由器——无 asyncio、无平台知识,可以从 Agent 的工作线程安全调用:
📄 gateway/stream_dispatch.py (第 40-132 行)
class GatewayEventDispatcher:
"""将类型化流事件通过适配器路由到投递 sink。"""
def __init__(self, adapter, sink=None, *,
enqueue_tool_line=None,
tool_mode="all",
preview_max_len=40,
on_long_tool=None,
on_notice=None):
...
def dispatch(self, event: StreamEvent) -> None:
"""路由单个事件。绝不向 Agent 工作线程抛出异常。"""
try:
self._dispatch(event)
except Exception: # 呈现层绝不能破坏 Agent 循环
logger.debug("stream-event dispatch error", exc_info=True)
def _dispatch(self, event: StreamEvent) -> None:
if isinstance(event, (MessageChunk, MessageStop, Commentary)):
# 消息事件 → 适配器.render_message_event → sink
self.adapter.render_message_event(event, self.sink)
elif isinstance(event, ToolCallChunk):
# 工具事件 → 适配器.format_tool_event → 进度队列
line = self.adapter.format_tool_event(
event, mode=self.tool_mode, ...)
if line: # None = 适配器选择"吃掉"
self._enqueue_tool_line(line)
设计要点:Dispatcher 是纯同步的,因为它在 Agent 的工作线程上运行。任何阻塞或异常都会被捕获——呈现层的问题绝不能中断 Agent 的思考。
七、会话管理与跨平台投递
1. SessionSource 与 SessionContext
📄 gateway/session.py (第 70-193 行)
@dataclass
class SessionSource:
"""描述消息来源。"""
platform: Platform
chat_id: str
chat_name: Optional[str] = None
chat_type: str = "dm" # "dm", "group", "channel", "thread"
user_id: Optional[str] = None
user_name: Optional[str] = None
thread_id: Optional[str] = None # Forum topics, Discord threads
guild_id: Optional[str] = None # Discord guild / Slack workspace
parent_chat_id: Optional[str] = None
message_id: Optional[str] = None
role_authorized: bool = False
@property
def description(self) -> str:
"""人类可读描述, 注入系统提示词。"""
# "DM with Alice" / "group: #dev-channel" / "channel: general"
@dataclass
class SessionContext:
"""完整会话上下文, 用于动态系统提示词注入。"""
source: SessionSource
connected_platforms: List[Platform]
home_channels: Dict[Platform, HomeChannel]
shared_multi_user_session: bool = False
2. 会话重置策略
📄 gateway/config.py (第 274-299 行)
@dataclass
class SessionResetPolicy:
"""控制会话何时重置 (丢失上下文)。"""
mode: str = "both" # "daily", "idle", "both", "none"
at_hour: int = 4 # 每日重置时间 (0-23, 本地时区)
idle_minutes: int = 1440 # 空闲超时 (24 小时)
notify: bool = True # 自动重置时通知用户
notify_exclude_platforms: tuple = ("api_server", "webhook")
3. 跨平台镜像投递
当 Agent 通过 send_message 工具向某个平台发送消息时,mirror.py 会将该消息写入目标会话的对话记录——这样目标平台的 Agent 也能看到这条消息:
📄 gateway/mirror.py (第 25-81 行)
def mirror_to_session(
platform: str, chat_id: str, message_text: str,
source_label: str = "cli", thread_id: Optional[str] = None,
user_id: Optional[str] = None,
) -> bool:
"""将投递镜像写入目标会话的对话记录。"""
session_id = _find_session_id(platform, chat_id, ...)
if not session_id:
return False
mirror_msg = {
"role": "assistant",
"content": message_text,
"timestamp": datetime.now().isoformat(),
"mirror": True,
"mirror_source": source_label,
}
_append_to_sqlite(session_id, mirror_msg)
return True
# 回放时, mirror=True 的消息被标记:
# "[Delivered from cli] <content>"
# 目标 Agent 知道这是从另一个会话投递的
4. DeliveryRouter:Cron 投递路由
DeliveryRouter 处理定时任务的输出投递。支持多种目标格式:
DeliveryTarget 解析:
"origin" → 回到任务创建源
"local" → 保存到本地文件
"telegram" → Telegram home channel
"telegram:123" → 特定 Telegram 聊天
"telegram:123:thread" → 特定线程
投递管线:
DeliveryRouter.deliver()
├─ 解析 DeliveryTarget
├─ 查找适配器 (self.adapters[platform])
├─ 发送消息 (adapter.send / adapter.send_image ...)
├─ 镜像到目标会话 (mirror_to_session)
└─ 失败回退: 保存到 cron/output/
静默检测:
_SILENCE_NARRATION 正则匹配:
"silent", "silence", "no response", 🔇, "."
→ 不投递, 避免空消息轰炸用户八、媒体处理管线
Gateway 处理来自平台的图片、音频、视频、文档——全部经过统一的缓存管线:
媒体处理管线 (base.py):
平台收到附件
│
├─ 1. 下载 (cache_image_from_url / cache_audio_from_url)
│ ├─ SSRF 防护: is_safe_url() 检查
│ ├─ 重定向守卫: _ssrf_redirect_guard()
│ │ 每次重定向重新验证目标地址
│ └─ 指数退避重试 (默认 2 次)
│
├─ 2. 分类 (cache_media_bytes)
│ ├─ 按扩展名 + MIME 类型分类
│ ├─ 图片 → image cache (~/.hermes/cache/images/)
│ ├─ 音频 → audio cache (~/.hermes/cache/audio/)
│ ├─ 视频 → video cache (~/.hermes/cache/videos/)
│ └─ 文档 → document cache (~/.hermes/cache/documents/)
│
├─ 3. 安全验证
│ ├─ 图片: magic bytes 检查 (PNG/JPEG/GIF/BMP/WEBP)
│ ├─ 音频: 扩展名白名单
│ └─ 文档: 扩展名白名单 (40+ 类型)
│
├─ 4. 沙箱路径转换 (to_agent_visible_cache_path)
│ └─ Agent 看到的安全路径, 非真实文件系统路径
│
└─ 5. 返回 CachedMedia 对象
└─ path, media_type, kind, display_name
└─ context_note(): "[image 'photo.jpg' saved at: ...]"
媒体投递 (Agent → 平台):
validate_media_delivery_path(path)
├─ 白名单: 缓存目录无条件信任
├─ 黑名单: /etc, /proc, ~/.ssh, ~/.aws, ~/.hermes/.env
├─ 严格模式: 还需在时间窗口内 (默认 1s)
└─ 非严格模式 (默认): 不在黑名单即可九、设计总结
Gateway 核心设计
- 动态平台枚举:内置 + 插件平台通过
_missing_()无缝扩展 - BasePlatformAdapter ABC:最小接口 + 大量默认实现 + 能力标志
- PlatformRegistry:插件化发现,last-writer wins 覆盖策略
- 会话隔离:per-session Event + Task 映射,精确中断控制
- Agent 缓存:LRU + TTL 驱逐,128 上限防内存泄漏
流式架构关键设计
- 结构化事件:8 种冻结 dataclass,Agent 发出,Gateway 渲染
- 关注点分离:Agent 不关心 Telegram markdown vs Discord embed
- 同步路由器:GatewayEventDispatcher 无 asyncio,Agent 线程安全
- 适配器渲染钩子:render_message_event + format_tool_event
- 历史隔离:事件不持久化,历史由 Agent 拥有
安全设计
- 瞬态错误白名单:网络错误不杀进程,事件循环级捕获
- Provider 错误分类:用户看到摘要,不看到原始 HTTP body
- 密钥脱敏:6 种 API 密钥模式 + Bearer token
- Telegram 噪声过滤:内部状态不推送到聊天
- SSRF 防护:URL 安全检查 + 重定向守卫
- 媒体投递沙箱:白名单 + 黑名单 + 时间窗口
跨平台协同
- 镜像投递:send_message 写入目标会话记录
- DeliveryRouter:Cron 输出路由,支持 origin/local/platform:chat_id
- 静默检测:避免空消息轰炸用户
- 会话重置策略:daily/idle/both/none 四种模式
- Home Channel:每平台默认目的地,/sethome 配置
下一讲预告:Cron 调度器与定时任务——Hermes 如何管理后台任务、会话恢复、以及跨会话的长期工作流
夜雨聆风