Hermes Agent 源码专题【左扬精讲】—— 24 个平台适配器:从 TG 长轮询到 Weixin AES-CDN
Hermes Agent 源码专题【左扬精讲】—— 24 个平台适配器:从 TG 长轮询到 Weixin AES-CDN
这是 Hermes Agent 源码专题【左扬精讲】系列 第 10 篇。
本篇是 Layer 5 平台适配器层 —— 前一篇讲了 BasePlatformAdapter ABC 的契约定义,这一篇展开它背后 24 个内置 adapter + 11 个插件 adapter 的差异化实现:每个 adapter 怎么接入自己的平台协议(TG Bot API 长轮询 / Slack Socket Mode / 飞书 WebSocket / 微信 AES-CDN / Email IMAP 轮询),怎么把平台原生事件翻译成 MessageEvent,怎么发送回平台。
本篇不是科普各平台 API 文档,而是顺着源码讲清楚:同一个 ABC 契约下,22 套完全不同的接入方案,为什么 Hermes 能用同一套 handle_message + send 承载它们。
完整调用链(源码锚点标注在源码引用块里):
-
-
- Platform enum ─ 平台标识符注册表(gateway/config.py)
- BasePlatformAdapter ABC ─ 统一抽象层(gateway/platforms/base.py)
- TGAdapter ─ PTB 长轮询 + media group + forum topic(gateway/platforms/TG.py)
- FeishuAdapter ─ WebSocket 长连接 + 飞书加密回调(gateway/platforms/feishu.py)
- WeixinAdapter ─ iLink 长轮询 + AES-128-ECB CDN(gateway/platforms/weixin.py)
- EmailAdapter ─ IMAP 轮询 + SMTP 发送(gateway/platforms/email.py)
- WebhookAdapter ─ aiohttp HTTP 服务 + HMAC 验签(gateway/platforms/webhook.py)
-
Layer 5 ─ 平台适配器层(gateway/platforms/)
════════════════════════════════════════════════════════════
Layer 5 ─ Platform enum(gateway/config.py 第 136 行)
Platform enum ← 24 个内置平台 + 动态插件成员
_missing_() ← 插件平台按需创建 pseudo-member
_BUILTIN_PLATFORM_VALUES ← 内置平台快照,防止枚举污染
Layer 5 ─ 内置 adapter 22 个(gateway/platforms/*.py)
┌─ 长轮询 ─────────────────────────────────────────
│ TGAdapter PTB Application.run() ← python-TG-bot 长轮询
│ WeixinAdapter iLink getUpdates ← 微信 iLink 协议
│ EmailAdapter IMAP IDLE + SELECT ← email 拉模式
│
├─ WebSocket ───────────────────────────────────
│ FeishuAdapter lark-oapi WebSocket ← 飞书长连接
│ SlackAdapter slack-bolt Socket Mode ← WebSocket 替代 HTTP
│ WeComAdapter WeCom WebSocket 协议 ← 企业微信 WebSocket
│ DingTalkAdapter dingtalk-stream SDK ← Stream Mode
│ MatrixAdapter matrix-nio HTTP + WS ← Matrix homeserver
│
├─ Webhook 被动接收 ────────────────────────────
│ WebhookAdapter aiohttp web server ← 通用 HTTP webhook
│ WecomCallbackAdapter HTTP + XML 加解密 ← 企业微信回调模式
│ SmsAdapter Twilio webhook ← SMS inbound
│ MSGraphWebhookAdapter MS Graph change notify ← Outlook/Teams 事件
│
├─ 特殊桥接 ────────────────────────────────────
│ WhatsAppAdapter Node.js web client bridge ← 微信桌面协议桥接
│ WhatsAppCloudAdapter Meta Graph API + aiohttp ← WhatsApp Business
│ QQAdapter QQ Bot WebSocket Gateway ← 官方 QQ Bot 协议
│ YuanbaoAdapter 腾讯混元 WebSocket 协议 ← 腾讯内部 AI 平台
│ BlueBubblesAdapter BlueBubbles REST API ← iMessage 桥接
│ SignalAdapter signal-cli HTTP daemon ← signal-cli REST
│ APIServerAdapter aiohttp JSON-RPC ← 内部 API 入口
│
└─ 插件 adapter(plugins/platforms/)
DiscordAdapter discord.py WebSocket ← 插件目录
MattermostAdapter Mattermost HTTP API ← 插件目录
GoogleChatAdapter Google Chat API ← 插件目录
... (共 11 个)
Layer 5 ─ 关键差异化维度
接入协议:长轮询 / WebSocket / Webhook / 特殊桥接
身份标识:bot token / OAuth / 邮箱地址 / 手机号 / Webhook URL
消息路由:chat_id / room_id / thread_id / session_id
媒体处理:CDN 上传 / AES 加密 / multipart/form-data
安全验签:HMAC / RSA / AES 加解密 / 飞书签名
Layer 5 TGAdapter FeishuAdapter WeixinAdapter EmailAdapter WebhookAdapter SlackAdapter WeComAdapter DingTalkAdapter Platform enum
本篇学习重点
必须掌握
- 理解 gateway/config.py 第 136 行 Platform enum 的三层成员体系:内置成员 + 动态 pseudo-member + 运行时注册
- 理解 4 种接入协议的差异:TGAdapter 长轮询(PTB Application)/ FeishuAdapter WebSocket(lark-oapi)/ WebhookAdapter 被动接收(aiohttp)/ WeixinAdapter 特殊桥接(iLink + AES-CDN)
- 理解 Platform._missing_()(第 169 行)怎么让插件 adapter 不改 enum 就能加入 Hermes
- 理解各 adapter 的 connect 方法怎么分别初始化自己的网络栈:PTB Application / aiohttp web server / IMAP IDLE / WebSocket client
- 理解 lazy import 模式:gateway/platforms/__init__.py 第 34 行 __getattr__ 让 QQ / Yuanbao 等重型 adapter 按需加载
需要了解
- 各 adapter 的 supports_code_blocks 类属性差异(飞书 / 微信 / Matrix 支持代码块渲染,TG 不支持)
- check_*_requirements() lazy install 模式(TG / Slack / 钉钉等),避免启动时缺少依赖就整体失败
- 各 adapter 的 MAX_MESSAGE_LENGTH 差异(飞书 8000 / 微信 2000 / QQ 4000 / 企业微信自定)
- MessageDeduplicator 在多个 adapter 中的复用模式(防止重放攻击)
- 插件 adapter 的 plugin.yaml 发现机制(gateway/config.py 第 214 行 _scan_bundled_plugin_platforms)
目录
- 一、Platform enum ─ 24 个内置 + 插件成员动态扩展
- 二、TGAdapter ─ PTB 长轮询 + forum topic + media group
- 三、FeishuAdapter ─ WebSocket 长连接 + 三层身份标识
- 四、WeixinAdapter ─ iLink 长轮询 + AES-128-ECB CDN
- 五、Webhook / Email / SMS ─ 三种被动接入模式
- 六、Slack / DingTalk / WeCom / Matrix ─ 其他 WebSocket 协议
- 七、WhatsApp / QQ / Yuanbao / Signal ─ 特殊协议桥接
- 八、插件 adapter 发现机制 ─ Discord / Mattermost / Teams
- 九、FAQ 20 问
一、Platform enum ─ 24 个内置 + 插件成员动态扩展
Layer 视角 ─ Platform enum 这一层解决什么?
第 9 篇讲过 BasePlatformAdapter ABC 是 "所有 adapter 共享的窄腰",但 Hermes 需要一个统一标识符来表示"这个 adapter 对应哪个平台"。如果用字符串硬编码,到处是 "TG" == platform 的脆弱比较。Platform enum 就是这个问题的解:所有内置平台用显式成员,插件平台通过 _missing_() 按需创建 pseudo-member,同一个 Platform.TG 在任何地方都是唯一引用。
三层成员体系:
- 内置成员(第 144-167 行)── TG / DISCORD / SLACK / FEISHU / WECOM / WEIXIN / EMAIL / SMS / WEBHOOK ... 共 24 个
- 动态 pseudo-member(第 169 行 _missing_)── 插件平台首次访问时创建,缓存在 _value2member_map_,保证 Platform("irc") is Platform("irc") 成立
- 运行时注册(第 199 行)── 用户安装的 pip 包插件通过 platform_registry 注册,与内置成员同等待遇
1.1 24 个内置成员一览
契约:
gateway/config.py 第 136 行 class Platform(Enum) 的 24 个内置成员(按协议类型分组):
- 长轮询 / 主动拉取:TG(第 145 行)/ WEIXIN(第 163 行)/ EMAIL(第 154 行)/ SMS(第 155 行)
- WebSocket 主动连接:FEISHU(第 160 行)/ WECOM(第 161 行)/ DINGTALK(第 156 行)/ MATRIX(第 152 行)
- Webhook 被动接收:WEBHOOK(第 158 行)/ WECOM_CALLBACK(第 162 行)/ MSGRAPH_WEBHOOK(第 159 行)/ API_SERVER(第 157 行)
- 特殊协议桥接:WHATSAPP(第 147 行)/ WHATSAPP_CLOUD(第 148 行)/ QQBOT(第 165 行)/ YUANBAO(第 166 行)/ SIGNAL(第 150 行)/ BLUEBUBBLES(第 164 行)
- 其他:DISCORD(第 146 行,插件)/ SLACK(第 149 行)/ MATTERMOST(第 151 行,插件)/ HOMEASSISTANT(第 153 行)
- 特殊:LOCAL(第 144 行)/ RELAY(第 167 行,实验性)
1.2 _missing_() 动态 pseudo-member 机制
源码视角 ─ gateway/config.py 第 169-211 行:
@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).
Arbitrary strings are rejected to prevent enum pollution.
"""
if not isinstance(value, str) or not value.strip():
return None
# Normalise to lowercase to avoid case mismatches in config
value = value.strip().lower()
# Check cache first (another call may have created it already)
if value in cls._value2member_map_:
return cls._value2member_map_[value]
# Only create pseudo-members for bundled plugin platforms (discovered
# via filesystem scan) or runtime-registered plugin platforms.
global _Platform__bundled_plugin_names
if _Platform__bundled_plugin_names is None:
_Platform__bundled_plugin_names = cls._scan_bundled_plugin_platforms()
if value in _Platform__bundled_plugin_names:
pseudo = object.__new__(cls)
pseudo._value_ = value
pseudo._name_ = value.upper().replace("-", "_").replace(" ", "_")
cls._value2member_map_[value] = pseudo
cls._member_map_[pseudo._name_] = pseudo
return pseudo
# Runtime-registered plugins (e.g. user-installed, discovered after
# the enum was defined).
try:
from gateway.platform_registry import platform_registry
if platform_registry.is_registered(value):
pseudo = object.__new__(cls)
pseudo._value_ = value
pseudo._name_ = value.upper().replace("-", "_").replace(" ", "_")
cls._value2member_map_[value] = pseudo
cls._member_map_[pseudo._name_] = pseudo
return pseudo
except Exception:
pass
return None
关键设计:
- 三段检查 ── 先看 cache(同一进程内多次访问)、再看 bundled plugin 目录扫描、最后看 runtime registry
- identity 稳定性 ── pseudo-member 缓存在 _value2member_map_,保证 Platform("discord") is Platform("discord") 为 True(因为 is 比较的是对象 identity,不是 == 值比较)
- 白名单安全 ── 随意字符串(如
Platform("hacker"))返回 None,不会污染枚举 - _scan_bundled_plugin_platforms(第 214 行) ── 扫描 plugins/platforms/ 目录下的子目录,找到有 plugin.yaml 或 plugin.yml 的目录作为插件平台名
1.3 lazy import ─ __getattr__ 按需加载
源码视角 ─ gateway/platforms/__init__.py 第 34-41 行:
# QQAdapter and YuanbaoAdapter were previously imported eagerly here, but
# nothing in the codebase consumes ``from gateway.platforms import
# QQAdapter`` (every real call site uses the long-form path
# ``from gateway.platforms.qqbot import QQAdapter``). The eager imports
# pulled in qqbot's chunked-upload + keyboards + onboard machinery and
# yuanbao's websocket stack — about 48 ms wall and ~8 MB RSS on every
# CLI invocation, even ones that never touch a gateway adapter.
#
# Use PEP 562 module ``__getattr__`` to keep the public re-export working
# while deferring the actual import to first attribute access.
__all__ = ["BasePlatformAdapter", "MessageEvent", "SendResult", "QQAdapter", "YuanbaoAdapter"]
def __getattr__(name):
if name == "QQAdapter":
from .qqbot import QQAdapter # noqa: F401
return QQAdapter
if name == "YuanbaoAdapter":
from .yuanbao import YuanbaoAdapter # noqa: F401
return YuanbaoAdapter
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
关键设计:
- 历史背景(注释第 13-18 行)── 原来在 gateway/platforms/__init__.py 顶部 eager import QQAdapter 和 YuanbaoAdapter,分别拉入了 chunked-upload/keyboards/onboard(QQ)和 yuanbao websocket 栈;CLI 启动时(不管有没有开 gateway)都多花 48ms + 8MB RSS
- PEP 562 ── Python 3.7+ 的 module-level __getattr__ 允许在首次访问时才 import
- 向后兼容 ── 外部代码如果还写
from gateway.platforms import QQAdapter,仍然可以工作,只是变成 lazy 了
Lessonlazy import 是平台 adapter 的性能护城河 —— Hermes 的平台数量会持续增长(每新增一个协议栈都要 import 一整块新代码),但大多数用户只跑 1~2 个平台。PEP 562 __getattr__ 让每个 adapter 的启动成本从"全局 import"变成"按需 load",CLI 冷启动不受影响。这是 per-conversation prompt caching is sacred 之外的另一个"成本最小化"设计:不让任何人替不需要的 adapter 付账。
本节小结
- Platform enum(第 136 行)定义 24 个内置成员 + _missing_() 动态扩展
- _missing_()(第 169 行)三段检查:cache → bundled scan → runtime registry,白名单安全
- _scan_bundled_plugin_platforms(第 214 行)扫描 plugins/platforms/ 找 plugin.yaml
- __getattr__(gateway/platforms/__init__.py 第 34 行)PEP 562 lazy import,省 48ms + 8MB RSS
二、TGAdapter ─ PTB 长轮询 + forum topic + media group
Layer 视角 ─ TGAdapter 这一层解决什么?
TG 是 Hermes 内置 adapter 中功能最复杂的一个。它需要处理:PTB(python-TG-bot)长轮询接入、forum topic 模式(Bot API 9.4+)、DM topic 模式、inline keyboard 交互、media group(多图一次性发送)、fallback IP 穿透、企业防火墙代理等。相比其他 adapter,TGAdapter 的 connect 方法是最复杂的:它启动的不是单个 asyncio Task,而是一个完整的 PTB Application。
2.1 connect() ── PTB Application.run() 长轮询
源码视角 ─ gateway/platforms/TG.py 第 337 行 class TGAdapter 与第 112 行 check_TG_requirements():
class TGAdapter(BasePlatformAdapter):
"""
TG bot adapter.
Handles:
- Receiving messages from users and groups
- Sending responses back
- Handling media and commands
"""
def check_TG_requirements() -> bool:
"""Check if TG dependencies are available.
If python-TG-bot is missing, attempts to lazy-install it via
``tools.lazy_deps.ensure("platform.TG")``. After a successful
install, re-imports the SDK and flips ``TG_AVAILABLE`` to True
so the adapter's class-level type aliases get rebound.
"""
global TG_AVAILABLE, Update, Bot, Message, InlineKeyboardButton
# ...
if TG_AVAILABLE:
return True
try:
from tools.lazy_deps import ensure as _lazy_ensure
_lazy_ensure("platform.TG", prompt=False)
except Exception:
return False
# ... re-bind module-level globals on success
TG_AVAILABLE = True
from TG import Update as _Update, Bot as _Bot
Update = _Update; Bot = _Bot; ...
关键设计:
- lazy install ── check_TG_requirements() 先看 TG_AVAILABLE flag(全局),False 时尝试
tools.lazy_deps.ensure("platform.TG");成功后重新 import TG 库并 rebind 模块级全局变量 - 全局 rebind ── 第 120-170 行,import 成功后把 Update / Bot / Message / InlineKeyboardButton / Application / filters 等全部 rebind 到模块级全局变量,这样类的类型注解(Application)能正确解析
2.2 消息接收流水线
行为:
gateway/platforms/TG.py 中 TGAdapter 对 BasePlatformAdapter 的关键覆写:
- PTB Application.run() 长轮询 ── PTB Application 管理自己的 event loop + handler 体系,TGAdapter 的 connect 启动它,disconnect 停止它
- forum topic 支持 ── Bot API 9.4 引入的 forum topic 模式,每个 topic 当独立 chat,message_thread_id 字段传递 topic id
- DM topic mode ── 私聊场景下也支持 topic(TG 的"thread"概念),用于多会话隔离
- inline keyboard 交互 ── handler_callback_query 处理按钮点击事件,翻译成 COMMAND 类型 MessageEvent
- media group ── TG 允许一次发送多个媒体(album),PTB 的 filters.PHOTO handler 负责聚合后交给 Hermes
- fallback IP 穿透 ── 国内访问 TG API 需要代理;gateway/platforms/TG_network.py 的 TGFallbackTransport 通过预配置的 fallback IP 列表做故障切换
2.3 媒体处理 ── cache_image_from_bytes 等
契约:
gateway/platforms/TG.py 第 92-106 行定义了 TG 专属的媒体扩展名和 MIME 映射:
_TG_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".gif"}
_TG_IMAGE_MIME_TO_EXT = {
"image/png": ".png", "image/jpeg": ".jpg", "image/jpg": ".jpg",
"image/webp": ".webp", "image/gif": ".gif",
}
_TG_IMAGE_EXT_TO_MIME = {
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
".webp": "image/webp", ".gif": "image/gif",
}
调用 BasePlatformAdapter 提供的 cache_image_from_bytes / cache_audio_from_bytes / cache_video_from_bytes / cache_document_from_bytes(从 gateway/platforms/base.py 第 1312 行 CachedMedia 工具函数)将二进制下载到本地临时文件,填充 MessageEvent.media_urls。
本节小结
- TGAdapter(gateway/platforms/TG.py 第 337 行)是最复杂的内置 adapter
- check_TG_requirements()(第 112 行)lazy install PTB 库 + rebind 模块级类型
- PTB Application 长轮询 + forum topic + DM topic + inline keyboard + media group
- fallback IP 穿透解决国内访问 TG API 的网络问题
三、FeishuAdapter ─ WebSocket 长连接 + 三层身份标识
Layer 视角 ─ FeishuAdapter 这一层解决什么?
飞书(Lark)是最复杂的中国办公平台 adapter,原因有三:身份标识体系复杂(open_id / user_id / union_id 三层),连接模式多样(WebSocket 长连接 vs Webhook 被动接收),媒体加密和签名(encrypt_key AES 加密,HMAC 签名验证)。FeishuAdapter 处理 WebSocket 长连接模式,FeishuCommentAdapter(gateway/platforms/feishu_comment.py)处理飞书文档评论事件。
3.1 三层身份标识体系
契约:
gateway/platforms/feishu.py 第 18-45 行的文档注释完整描述了飞书身份模型:
- open_id(
ou_xxx)—— App 级标识,同一自然人在不同飞书应用下得到不同的 open_id。事件 payload 中无需额外权限即可获得。 - user_id(
u_xxx)—— 企业级标识,在公司内稳定,但需要contact:user.employee_id:readonly权限才能获取。 - union_id(
on_xxx)—— 开发者级标识,同一开发者的所有应用下共享,是跨 app 场景下的最佳稳定 ID。 - bot open_id —— 机器人在自身应用上下文内的 open_id,用于 @mention 鉴权。
session key 优先使用 union_id(通过 user_id_alt),而不是 open_id(通过 user_id),确保同一用户在不同应用间 session 稳定。
3.2 connect() ── lark-oapi WebSocket 长连接
源码视角 ─ gateway/platforms/feishu.py 第 1409 行 class FeishuAdapter:
class FeishuAdapter(BasePlatformAdapter):
"""Feishu/Lark bot adapter."""
supports_code_blocks = True # Feishu renders fenced code blocks
MAX_MESSAGE_LENGTH = 8000
# Feishu identity model (open_id / user_id / union_id 三层)
# Session-key participant isolation prefers union_id (via user_id_alt)
# over open_id (via user_id) so that sessions stay stable if the same
# user is seen through different apps in the future.
关键设计:
- supports_code_blocks = True —— 飞书原生支持 Markdown 代码块渲染,不需要像 TG 那样用 HTML
<code>标签包裹 - MAX_MESSAGE_LENGTH = 8000 —— 飞书单条消息上限是 8000 字符(比其他平台大),但 Hermes 自己的 FeishuAdapter 也截断到这里
- union_id 作为主标识 —— 注释明确说"优先 union_id",这是因为飞书的 union_id 在同一个开发者账号下的所有应用间稳定,open_id 只在单个应用内有效
3.3 加密链路 ── encrypt_key AES + HMAC 签名
行为:
gateway/platforms/feishu.py 的 Webhook 模式(通过 gateway/platforms/feishu.py 第 73-88 行 aiohttp 可选依赖)支持加密回调:
- encrypt_key(配置项)—— 飞书 Webhook 加密通信的 AES 密钥,接收时解密 payload
- HMAC 签名验证(hmac 标准库)—— 验证飞书回调请求的签名,防止伪造
- lark-oapi SDK —— 官方 SDK 处理 token 刷新、API 调用封装、错误重试等
- aiohttp(独立可选依赖)—— 第 75-85 行:在 lark_oapi 导入之前先 import aiohttp,这样 Webhook 模式在 lark_oapi 缺失时仍然能工作
本节小结
- FeishuAdapter(gateway/platforms/feishu.py 第 1409 行)处理飞书 WebSocket 长连接
- 三层身份体系:open_id(App 级)/ user_id(企业级)/ union_id(开发者级)
- session key 优先 union_id,确保跨应用 session 稳定
- supports_code_blocks = True,MAX_MESSAGE_LENGTH = 8000
- encrypt_key AES + HMAC 签名双保险
四、WeixinAdapter ─ iLink 长轮询 + AES-128-ECB CDN
Layer 视角 ─ WeixinAdapter 这一层解决什么?
微信个人账号接入 Hermes 是最特殊的 adapter —— 微信没有官方 Bot API,Hermes 通过腾讯官方的 iLink Bot API(一个中间层服务)桥接。WeixinAdapter 需要处理:iLink 长轮询(35s 超时)、AES-128-ECB 加密 CDN 传输(媒体文件)、QR 码登录(首次授权)、session token 回显(每条出站消息必须携带 context_token)。
4.1 iLink 协议概览
契约:
gateway/platforms/weixin.py 第 1-11 行的模块 docstring:
"""
Weixin platform adapter.
Connects Hermes Agent to WeChat personal accounts via Tencent's iLink Bot API.
Design notes:
- Long-poll ``getupdates`` drives inbound delivery.
- Every outbound reply must echo the latest ``context_token`` for the peer.
- Media files move through an AES-128-ECB encrypted CDN protocol.
- QR login is exposed as a helper for the gateway setup wizard.
"""
核心 API 端点(第 72-85 行):
- ilink/bot/getupdates —— 长轮询接收消息,超时 35s
- ilink/bot/sendmessage —— 发送消息,必须带 context_token
- ilink/bot/sendtyping —— 发送"正在输入"状态
- ilink/bot/get_bot_qrcode / get_qrcode_status —— QR 登录流程
- ilink/bot/getuploadurl —— 获取 CDN 上传地址(含 AES 加密)
4.2 connect() ── iLink 长轮询循环
源码视角 ─ gateway/platforms/weixin.py 第 72-96 行常量定义:
ILINK_BASE_URL = "https://ilinkai.weixin.qq.com"
WEIXIN_CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c"
ILINK_APP_ID = "bot"
CHANNEL_VERSION = "2.2.0"
ILINK_APP_CLIENT_VERSION = (2 << 16) | (2 << 8) | 0
LONG_POLL_TIMEOUT_MS = 35_000
API_TIMEOUT_MS = 15_000
CONFIG_TIMEOUT_MS = 10_000
QR_TIMEOUT_MS = 35_000
MAX_CONSECUTIVE_FAILURES = 3
RETRY_DELAY_SECONDS = 2
BACKOFF_DELAY_SECONDS = 30
SESSION_EXPIRED_ERRCODE = -14
RATE_LIMIT_ERRCODE = -2 # iLink frequency limit — backoff and retry
MESSAGE_DEDUP_TTL_SECONDS = 300
关键设计:
- 35s 长轮询 —— HTTP 请求在服务端等消息,最长等 35 秒再返回,减少空轮询
- session 过期处理 —— errcode = -14 表示 session 过期,adapter 需要重新授权
- 频率限制 —— errcode = -2 表示触发 iLink 频率限制,backoff 30 秒后重试
- MessageDeduplicator —— 300s TTL 去重,防止消息重复投递
4.3 AES-128-ECB CDN 媒体上传
行为:
gateway/platforms/weixin.py 第 38-56 行导入:
- cryptography.hazmat.primitives.ciphers —— AES-128-ECB 加密,由 CRYPTO_AVAILABLE flag 控制可用性
- aiohttp —— 异步 HTTP 客户端,用于 CDN 上传和 API 调用
媒体文件上传流程:先调 ilink/bot/getuploadurl 获取加密后的 CDN URL,再将文件 AES-128-ECB 加密后上传到该 URL。接收端(用户微信端)解密后展示。
Lesson微信的封闭生态倒逼出 Hermes 最复杂的 adapter —— 没有官方 Bot API,就只能用 iLink 中间层;iLink 没有 WebSocket,就得用 35s 长轮询;媒体走 CDN 就得自己做 AES 加密。相比之下,TG 只需要调 Bot API,Slack 只需要开 Socket Mode,微信却需要 adapter 自建整条传输链。这说明:接入封闭平台的总成本由平台方的开放程度决定,而不是 Hermes 的抽象层设计决定。
本节小结
- WeixinAdapter(gateway/platforms/weixin.py 第 1138 行)接入微信个人账号
- iLink 长轮询(35s)+ 错误码体系(-14 session 过期 / -2 频率限制)
- AES-128-ECB CDN 媒体传输(cryptography 库)
- QR 码登录首次授权 + context_token 每消息回显
- supports_code_blocks = True,MAX_MESSAGE_LENGTH = 2000
五、Webhook / Email / SMS ─ 三种被动接入模式
Layer 视角 ─ 被动接入这一组解决什么?
长轮询和 WebSocket 都是 adapter 主动建立连接。另一类场景是外部服务主动推送到 Hermes:GitHub 发 webhook / 邮箱收到邮件 / Twilio 推送 SMS。这些 adapter 不需要 connect() 建立长连接,而是启动一个 HTTP server 监听端口,等待外部推送。
5.1 WebhookAdapter ─ aiohttp HTTP 服务 + HMAC 验签
源码视角 ─ gateway/platforms/webhook.py 第 107 行 class WebhookAdapter:
class WebhookAdapter(BasePlatformAdapter):
"""Generic webhook receiver that triggers agent runs from HTTP POSTs."""
def __init__(self, config: PlatformConfig):
super().__init__(config, Platform.WEBHOOK)
self._host: str = config.extra.get("host", DEFAULT_HOST)
# ...
# Security invariants:
# - HMAC secret is required per route (validated at startup)
# - Rate limiting per route (fixed-window, configurable)
# - Idempotency cache prevents duplicate agent runs on webhook retries
# - Body size limits checked before reading payload
# - Set secret to "INSECURE_NO_AUTH" to skip validation (testing only)
关键设计:
- 路由配置(第 8-19 行 docstring)—— 每个 route 定义 events / secret / prompt template / skills / deliver 目标 / deliver_extra;支持
deliver_only: true跳过 agent 直接投递通知 - HMAC 验签(hmac.compare_digest,防时序攻击)—— 每条 route 独立 secret,启动时强制校验存在
- 幂等去重 —— webhook 重试是常见场景,adapter 用幂等 key 缓存防止重复 agent run
- deliver 目标(第 65-70 行 _BUILTIN_DELIVER_PLATFORMS)—— webhook 收到后可以路由到其他平台(TG / discord / slack ...)
5.2 EmailAdapter ─ IMAP IDLE + SMTP
源码视角 ─ gateway/platforms/email.py 第 303 行 class EmailAdapter:
class EmailAdapter(BasePlatformAdapter):
"""Email gateway adapter using IMAP (receive) and SMTP (send)."""
# Environment variables:
# EMAIL_IMAP_HOST / EMAIL_SMTP_HOST / EMAIL_ADDRESS / EMAIL_PASSWORD
# EMAIL_POLL_INTERVAL (default: 15s) / EMAIL_ALLOWED_USERS
# Automated sender patterns — emails from these are silently ignored
_NOREPLY_PATTERNS = (
"noreply", "no-reply", "no_reply", "donotreply", "do-not-reply",
"mailer-daemon", "postmaster", "bounce", "notifications@",
"automated@", "auto-confirm", "auto-reply", "automailer",
)
# RFC headers that indicate bulk/automated mail
_AUTOMATED_HEADERS = {
"Auto-Submitted": lambda v: v.lower() != "no",
"Precedence": lambda v: v.lower() in {"bulk", "list", "junk"},
"X-Auto-Response-Suppress": lambda v: bool(v),
"List-Unsubscribe": lambda v: bool(v),
}
关键设计:
- IMAP POLL 而非 IMAP IDLE —— Hermes 的 EmailAdapter 是轮询模式(每 15s 查一次),不是真正的 IMAP IDLE(服务器推送)。这在大多数邮箱(尤其是 Gmail)不支持 IDLE 的情况下是务实的选择。
- 自动化邮件过滤 —— noreply 模式列表 + RFC Auto-Submitted / Precedence 头检测,自动忽略系统邮件
- SMTP 发送 —— 用 smtplib.SMTP(同步库,在 executor thread 中运行);IPv4 only(socket.create_connection AF_INET 约束,避免多栈问题)
- MAX_MESSAGE_LENGTH = 50_000 —— Gmail-safe 上限
5.3 SmsAdapter ─ Twilio webhook
契约:
gateway/platforms/sms.py 第 56 行 class SmsAdapter:
class SmsAdapter(BasePlatformAdapter):
"""
Twilio SMS Hermes gateway adapter.
Each inbound phone number gets its own Hermes session (multi-tenant).
Replies are always sent from the configured TWILIO_PHONE_NUMBER.
"""
关键设计:
- 多租户 session —— 每个 inbound phone number(发送方)对应一个独立 session,支持多用户共用一个 Twilio 号码
- 被动接收 —— Twilio POSTs 到配置的 webhook URL;adapter 解析 From / Body / MediaUrls 字段
- 出站回复 —— 回复始终从配置的 TWILIO_PHONE_NUMBER 发出,由 Twilio 服务器路由
本节小结
- WebhookAdapter(gateway/platforms/webhook.py 第 107 行)启动 aiohttp server 监听 HTTP POST,HMAC 验签 + 幂等去重
- EmailAdapter(gateway/platforms/email.py 第 303 行)IMAP 轮询 + SMTP 发送,RFC 头过滤自动化邮件
- SmsAdapter(gateway/platforms/sms.py 第 56 行)Twilio webhook,多租户 phone → session 映射
六、Slack / DingTalk / WeCom / Matrix ─ 其他 WebSocket 协议
Layer 视角 ─ 其他 WebSocket adapter 这一组解决什么?
除 FeishuAdapter 之外,还有 4 个 WebSocket 协议 adapter:Slack Socket Mode、钉钉 Stream Mode、企业微信 WebSocket、Matrix HTTP+WS。它们都继承 BasePlatformAdapter,覆写 connect 启动各自的 WebSocket 客户端,send 用各自平台的 API。
6.1 SlackAdapter ─ Socket Mode + mrkdwn + thread
源码视角 ─ gateway/platforms/slack.py 第 305 行 class SlackAdapter:
class SlackAdapter(BasePlatformAdapter):
"""
Slack bot adapter using Socket Mode.
Requires two tokens:
- SLACK_BOT_TOKEN (xoxb-...) for API calls
- SLACK_APP_TOKEN (xapp-...) for Socket Mode WebSocket
"""
# ContextVar carrying the user_id of the slash-command invoker.
# Set in _handle_slash_command, read in send() to match the correct
# stashed response_url when multiple users issue commands on the same
# channel concurrently.
_slash_user_id: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
"_slash_user_id",
default=None,
)
关键设计:
- Socket Mode —— Slack 推荐的连接方式:slack-bolt 的 AsyncApp 维护 WebSocket,不需要公网 HTTPS webhook URL,比旧版 HTTP 方式更简单
- 双 token —— BOT_TOKEN(xoxb)用于 API 调用,APP_TOKEN(xapp)用于 WebSocket 鉴权
- _slash_user_id ContextVar(第 65 行)—— 解决同一 channel 内多个用户并发发 slash command 时 response_url 路由问题;ContextVar 在 asyncio Task 间传播
- mrkdwn 渲染 —— Slack 原生支持 mrkdwn(类 Markdown),supports_code_blocks 继承默认值 False(不用显式设置)
- lazy install —— check_slack_requirements()(第 81 行)lazy install slack-bolt/slack-sdk
6.2 DingTalkAdapter ─ Stream Mode + AI Card
源码视角 ─ gateway/platforms/dingtalk.py 第 146 行 class DingTalkAdapter:
class DingTalkAdapter(BasePlatformAdapter):
"""
DingTalk platform adapter using Stream Mode.
Uses dingtalk-stream SDK (>=0.20) for real-time message reception without webhooks.
Responses are sent via DingTalk's session webhook (markdown format).
Supports: text, images, audio, video, rich text, files, and group @mentions.
Requires:
pip install "dingtalk-stream>=0.20" httpx
DINGTALK_CLIENT_ID and DINGTALK_CLIENT_SECRET env vars
"""
关键设计:
- dingtalk-stream SDK —— Stream Mode(>0.20)是钉钉推荐的接入方式,不需要公网 webhook;阿里云官方 SDK
- AI Card 模式 —— 通过 alibabacloud_dingtalk.card_1_0 SDK 发送 AI 卡片消息(CARD_SDK_AVAILABLE flag),用于流式输出展示
- mention_patterns —— 支持自定义正则唤醒词(如中文别名
^小马),不只识别 @bot - group mention gating —— 群聊默认需要 @mention 才响应,require_mention: false 时打开自由对话
6.3 WeComAdapter vs WecomCallbackAdapter
行为:
企业微信有两个独立的 adapter:
- WeComAdapter(gateway/platforms/wecom.py 第 142 行)—— WebSocket 长连接模式:连接
wss://openws.work.weixin.qq.com,接收 aibot_msg_callback 事件,发送 aibot_send_msg;SUPPORTS_MESSAGE_EDITING = False - WecomCallbackAdapter(gateway/platforms/wecom_callback.py)—— 被动回调模式:企业微信 POST XML 加密消息到 HTTP 端点,adapter 解密后处理;用 defusedxml.ElementTree 防 XXE 攻击;defusedxml 是安全关键依赖(第 20-23 行注释明确说明)
两种模式的选择取决于企业微信应用的类型:自建应用用 WebSocket 多(实时性更好),第三方应用用回调模式。
6.4 MatrixAdapter ─ Homeserver 协议
契约:
gateway/platforms/matrix.py 第 774 行 class MatrixAdapter:
class MatrixAdapter(BasePlatformAdapter):
"""Gateway adapter for Matrix (any homeserver)."""
supports_code_blocks = True # Matrix renders fenced code blocks (HTML/markdown)
# Matrix clients commonly reserve typed "/" for client-local commands;
关键设计:
- supports_code_blocks = True —— Matrix 原生支持 HTML/markdown 代码块渲染
- matrix-nio 库 —— 异步 Matrix 客户端,支持 HTTP long poll + WebSocket(自动协商)
- 任意 Homeserver —— 不绑定特定 Matrix 提供商(Element.io / self-hosted /Synapse / Dendrite 均可)
- typed "/" 保留 —— Matrix 客户端本地用
/做快捷命令,adapter 需要处理冲突
本节小结
- SlackAdapter(gateway/platforms/slack.py 第 305 行)Socket Mode + 双 token + ContextVar _slash_user_id
- DingTalkAdapter(gateway/platforms/dingtalk.py 第 146 行)dingtalk-stream SDK Stream Mode + AI Card SDK
- WeComAdapter(gateway/platforms/wecom.py 第 142 行)WebSocket vs WecomCallbackAdapter 被动 XML 回调
- MatrixAdapter(gateway/platforms/matrix.py 第 774 行)matrix-nio,任意 Homeserver,supports_code_blocks = True
七、WhatsApp / QQ / Yuanbao / Signal ─ 特殊协议桥接
Layer 视角 ─ 特殊协议桥接这一组解决什么?
这一组 adapter 面对的平台没有开放、标准的 HTTP/WebSocket API,需要桥接到平台特定的协议或外部进程。它们是 Hermes 适配器层中最具创造性的实现:WhatsApp 用 Node.js 桥接桌面协议,QQ 用官方 Bot Gateway,Yuanbao 用腾讯混元内部协议,BlueBubbles 用 iMessage REST API。
7.1 WhatsAppAdapter vs WhatsAppCloudAdapter
行为:
gateway/platforms/whatsapp.py 第 233 行 class WhatsAppAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter):
- WhatsAppBehaviorMixin —— gateway/platforms/whatsapp_common.py 中的共享行为 mixin,WhatsAppAdapter 和 WhatsAppCloudAdapter 都继承它
- Node.js web client 桥接 —— WhatsApp 没有官方 Bot API,Hermes 通过一个本地 Node.js 进程连接 WhatsApp Web 协议,adapter 通过 HTTP 与该进程通信
gateway/platforms/whatsapp_cloud.py 第 178 行 class WhatsAppCloudAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter):
- Meta Graph API —— WhatsApp Business Cloud API 提供了官方 Bot 接口(不需要 Node.js 桥接);Inbound 通过 aiohttp server 接收 Meta webhook POST,Outbound 通过
graph.facebook.com/.../messagesAPI 发送 - verify token 验证 —— Meta webhook 接入时的 hub.verify_token 校验
7.2 QQAdapter ─ 官方 Bot WebSocket Gateway + REST API
源码视角 ─ gateway/platforms/qqbot/adapter.py 第 154 行 class QQAdapter:
class QQAdapter(BasePlatformAdapter):
"""QQ Bot adapter backed by the official QQ Bot WebSocket Gateway + REST API."""
# QQ Bot API does not support editing sent messages.
SUPPORTS_MESSAGE_EDITING = False
MAX_MESSAGE_LENGTH = MAX_MESSAGE_LENGTH
关键设计:
- SUPPORTS_MESSAGE_EDITING = False —— QQ Bot API 不支持编辑已发消息,与 BlueBubblesAdapter / SignalAdapter 一致
- WebSocket Gateway —— 官方 QQ Bot 协议,通过 WebSocket 长连接接收事件
- REST API 发送 —— 出站走 QQ 官方 REST API
- chunked upload —— 大文件通过 gateway/platforms/qqbot/chunked_upload.py 分片上传
- onboard / keyboards —— 首次使用引导流程和自定义键盘(gateway/platforms/qqbot/onboard.py / gateway/platforms/qqbot/keyboards.py)
7.3 YuanbaoAdapter ─ 腾讯混元 WebSocket 协议
契约:
gateway/platforms/yuanbao.py 第 4981 行 class YuanbaoAdapter(BasePlatformAdapter):
- 腾讯混元 —— 腾讯内部的 AI 平台,YuanbaoAdapter 是其与 Hermes 的桥接
- proto 协议(gateway/platforms/yuanbao_proto.py)—— 使用 Protobuf 定义消息格式,比 JSON 更紧凑
- media 处理(gateway/platforms/yuanbao_media.py)—— 混元平台的媒体上传下载
- sticker 支持(gateway/platforms/yuanbao_sticker.py)—— 表情包交互
7.4 SignalAdapter / BlueBubblesAdapter
契约:
- SignalAdapter(gateway/platforms/signal.py 第 174 行)—— 基于 signal-cli HTTP daemon:本地启动 signal-cli 的 REST 接口,adapter 通过 HTTP 与之通信;SUPPORTS_MESSAGE_EDITING = False(Signal 无编辑 API)
- BlueBubblesAdapter(gateway/platforms/bluebubbles.py 第 112 行)—— BlueBubbles 是一个 iMessage 桥接服务(macOS 专用);adapter 通过其 REST API 接收/发送 iMessage;SUPPORTS_MESSAGE_EDITING = False
Lesson接入封闭平台的成本由平台决定,不由 Hermes 决定 —— WhatsApp(Node.js 桥接)/ Signal(signal-cli daemon)/ BlueBubbles(iMessage REST)/ Weixin(iLink + AES)每个 adapter 都实现了"从标准 ABC 到平台特定传输"的完整链路。Hermes 的 BasePlatformAdapter 抽象做得足够薄,才能承载这 4 种完全不同的桥接策略而不产生耦合。新增一个封闭平台的成本是"写一个新 adapter",而不是"修改 Hermes core"。
本节小结
- WhatsAppAdapter(gateway/platforms/whatsapp.py 第 233 行)Node.js web client 桥接;WhatsAppCloudAdapter(gateway/platforms/whatsapp_cloud.py 第 178 行)Meta Graph API
- QQAdapter(gateway/platforms/qqbot/adapter.py 第 154 行)官方 Bot WebSocket Gateway + REST API + chunked upload
- YuanbaoAdapter(gateway/platforms/yuanbao.py 第 4981 行)腾讯混元 Protobuf 协议
- SignalAdapter(gateway/platforms/signal.py 第 174 行)signal-cli HTTP daemon;BlueBubblesAdapter(gateway/platforms/bluebubbles.py 第 112 行)iMessage REST API
八、插件 adapter 发现机制 ─ Discord / Mattermost / Teams
Layer 视角 ─ 插件 adapter 这一层解决什么?
Hermes 的平台生态不只靠内置 adapter。Discord / Mattermost / Teams / Google Chat / IRC / Line 等平台通过 plugins/platforms/ 目录以插件形式接入。这些插件 adapter 和内置 adapter 一样继承 BasePlatformAdapter,但通过 plugin.yaml 声明,由 Platform._missing_() 按需创建 pseudo-member,被 _scan_bundled_plugin_platforms 发现。
8.1 插件目录结构
结构:
plugins/platforms/ 目录下的 11 个插件平台:
- discord —— plugins/platforms/discord/adapter.py(第 716 行 class DiscordAdapter),discord.py WebSocket
- mattermost —— plugins/platforms/mattermost/adapter.py(第 71 行 class MattermostAdapter),Mattermost HTTP API
- google_chat / irc / line / ntfy / photon / raft / simplex / teams —— 其他平台插件
- 每个插件目录有 plugin.yaml 或 plugin.yml 声明文件
8.2 插件发现流程
行为:
gateway/config.py 第 214 行 _scan_bundled_plugin_platforms 的扫描逻辑:
- 扫描 plugins/platforms/ 下的子目录
- 每个子目录必须有 __init__.py(Python package)和 plugin.yaml(或 plugin.yml,Hermes plugin manifest)
- 目录名(lowercase)加入 bundled plugin names set
- 后续 Platform("discord") 首次访问时触发 _missing_(),创建并缓存 DiscordAdapter 的 pseudo-member
8.3 DiscordAdapter ─ 插件 adapter 示例
源码视角 ─ plugins/platforms/discord/adapter.py 第 716 行:
class DiscordAdapter(BasePlatformAdapter):
"""
Discord bot adapter.
Handles:
- Receiving messages from servers and DMs
- Sending responses back
- Handling slash commands and components
- Thread support
- Role-based access control
"""
关键设计:
- discord.py —— Discord 官方 Python SDK,通过 WebSocket 连接 Discord Gateway
- 1440-min auto-archive threads —— Discord 线程默认 1440 分钟后自动归档,adapter 需要处理线程归档事件
- role-based access —— Discord 服务器角色系统与 Hermes 的 _is_user_authorized 集成
- guild scope —— Discord 的"服务器"(guild)概念,每个 guild 有独立的 channel 结构
本节小结
- 插件 adapter 通过 plugins/platforms/ 目录 + plugin.yaml 声明接入 Hermes
- _scan_bundled_plugin_platforms(第 214 行)扫描发现,_missing_()(第 169 行)动态创建 pseudo-member
- DiscordAdapter(plugins/platforms/discord/adapter.py 第 716 行)discord.py WebSocket + role-based access
- 插件 adapter 和内置 adapter 一样继承 BasePlatformAdapter,接入方式完全相同
九、FAQ 20 问
FAQ 分组说明
本节围绕 24 个平台 adapter 的差异化设计,每条都是"读整个系列前最该知道的事"。从 Platform enum 机制、接入协议分类、媒体处理、安全验签、插件发现五个维度展开。
Q1. Platform enum 有多少内置成员?
24 个内置成员。gateway/config.py 第 136 行 class Platform(Enum) 定义 LOCAL(第 144 行)+ 23 个平台成员:TG / DISCORD / WHATSAPP / WHATSAPP_CLOUD / SLACK / SIGNAL / MATTERMOST / MATRIX / HOMEASSISTANT / EMAIL / SMS / DINGTALK / API_SERVER / WEBHOOK / MSGRAPH_WEBHOOK / FEISHU / WECOM / WECOM_CALLBACK / WEIXIN / BLUEBUBBLES / QQBOT / YUANBAO / RELAY。
Q2. Platform._missing_() 怎么保证 identity 稳定性?
缓存在 _value2member_map_。gateway/config.py 第 169-211 行 —— pseudo-member 用 object.__new__(cls) 创建,存入 _value2member_map_ 和 _member_map_。同一进程内后续 Platform("discord") 命中 cache,直接返回已有对象,确保 Platform("discord") is Platform("discord") 为 True(is 比较对象 identity,不是 == 比较值)。
Q3. 为什么 QQAdapter 和 YuanbaoAdapter 用 __getattr__ lazy load?
避免启动时 import 整块重型依赖。gateway/platforms/__init__.py 第 34-41 行 PEP 562 __getattr__ —— QQAdapter 依赖 chunked_upload + keyboards + onboard(QQ 特有的大型模块),YuanbaoAdapter 依赖 websocket 栈。CLI 启动时(不管有没有开 gateway)eager import 会额外花 48ms + 8MB RSS。lazy import 把成本移到首次访问时。
Q4. TGAdapter 的 lazy install 怎么 rebind 类型?
全局 rebind 模块级变量。gateway/platforms/TG.py 第 112-170 行 check_TG_requirements() —— import 成功后执行 Update = _Update; Bot = _Bot; Application = _App; ...(第 148-169 行),把模块级全局变量 rebind 到真实类型。这样类定义中的类型注解(如 Application)在运行时能正确解析。
Q5. 飞书的三层身份标识优先级是什么?
union_id > user_id > open_id。gateway/platforms/feishu.py 第 43-45 行注释明确说明 —— session key 用 user_id_alt(union_id)而不是 user_id(open_id)。因为 union_id 在同一开发者账号的所有应用间稳定,open_id 只在单个应用内有效。
Q6. WeixinAdapter 的长轮询超时是多少?
35 秒。gateway/platforms/weixin.py 第 86 行 LONG_POLL_TIMEOUT_MS = 35_000。HTTP 请求最长等 35 秒才返回,减少空轮询次数。另有 API_TIMEOUT_MS = 15s,CONFIG_TIMEOUT_MS = 10s,QR_TIMEOUT_MS = 35s。
Q7. WeixinAdapter 的 errcode = -14 和 -2 分别代表什么?
-14 = session 过期(需重新授权);-2 = 频率限制(backoff 30s 重试)。gateway/platforms/weixin.py 第 94-95 行常量定义。session 过期需要 adapter 重新触发 QR 登录流程;频率限制是 iLink 平台的调用配额限制,adapter 内部处理退避重试。
Q8. EmailAdapter 为什么不用 IMAP IDLE(服务器推送)?
因为大多数邮箱不支持 IDLE,尤其是 Gmail。gateway/platforms/email.py 第 303 行 EmailAdapter —— 轮询模式(默认 15s)兼容所有 IMAP 邮箱,是更务实的选择。IMAP IDLE 只在服务器明确支持时才能用,而 Hermes 的目标是开箱即用。
Q9. WebhookAdapter 的 deliver_only 是什么?
跳过 agent,直接把 webhook payload 路由到其他平台。gateway/platforms/webhook.py 第 16-19 行 docstring —— 用于 Supabase 实时事件 / 监控告警 / inter-agent pings 等场景,不需要 LLM 推理,sub-second 投递更重要。
Q10. WeComAdapter 和 WecomCallbackAdapter 有什么区别?
接入模式不同:WebSocket 长连接 vs HTTP XML 回调。gateway/platforms/wecom.py 第 142 行 WeComAdapter 用 WebSocket(wss://openws.work.weixin.qq.com)主动接收事件;gateway/platforms/wecom_callback.py 被动接收企业微信的 HTTP POST XML 加密消息,用 defusedxml 防 XXE。选哪个取决于企业微信应用的类型。
Q11. 为什么飞书的 MAX_MESSAGE_LENGTH 是 8000?
飞书单条消息的字符上限是 8000。gateway/platforms/feishu.py 第 1416 行 MAX_MESSAGE_LENGTH = 8000。相比之下:TG 上限 4096(gateway/platforms/TG.py 第 349 行),微信 2000(gateway/platforms/weixin.py 第 1143 行),QQ 4000(gateway/platforms/qqbot/constants.py 第 54 行 MAX_MESSAGE_LENGTH = 4000),飞书 8000 是所有内置 adapter 中最宽松的。
Q12. supports_code_blocks 哪些 adapter 是 True?
飞书 / 微信 / Matrix / Discord(加 4 个插件)。gateway/platforms/feishu.py 第 1412 行 supports_code_blocks = True;gateway/platforms/weixin.py 第 1141 行 supports_code_blocks = True;gateway/platforms/matrix.py 第 777 行 supports_code_blocks = True;plugins/platforms/discord/adapter.py 第 733 行 supports_code_blocks = True。这些平台原生支持 Markdown/fenced code 渲染,不需要 HTML 包裹。
Q13. SUPPORTS_MESSAGE_EDITING 哪些 adapter 是 False?
QQ / Signal / BlueBubbles / WeCom。gateway/platforms/qqbot/adapter.py 第 158 行 SUPPORTS_MESSAGE_EDITING = False;gateway/platforms/signal.py 第 181 行 SUPPORTS_MESSAGE_EDITING = False;gateway/platforms/bluebubbles.py 第 114 行 SUPPORTS_MESSAGE_EDITING = False;gateway/platforms/wecom.py 第 146 行 SUPPORTS_MESSAGE_EDITING = False。这些平台的 API 不支持编辑已发消息。
Q14. 为什么 EmailAdapter 用 IPv4 only?
避免 IMAP/SMTP 连接时的多网络栈问题。gateway/platforms/email.py 第 69 行 _create_ipv4_connection —— EmailAdapter 用同步 smtplib(在 executor thread 中运行),IPv6 在某些企业网络/Docker 环境下可能有解析顺序问题。显式 AF_INET 约束避免碰壁。
Q15. WhatsAppAdapter 为什么需要 WhatsAppBehaviorMixin?
WhatsAppAdapter 和 WhatsAppCloudAdapter 共享行为但底层协议不同。gateway/platforms/whatsapp_common.py 中的 WhatsAppBehaviorMixin 定义两个 adapter 共用的逻辑(如消息格式化、media URL 处理、typing 状态)。继承关系:WhatsAppAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter)。
Q16. _scan_bundled_plugin_platforms 怎么发现插件 adapter?
扫描 plugins/platforms/ 子目录,检查 plugin.yaml 或 plugin.yml 存在。gateway/config.py 第 214-232 行 —— 对每个子目录验证是目录 + __init__.py 存在 + (plugin.yaml OR plugin.yml) 存在。目录名 lowercase 加入 bundled set。Discord / Mattermost / Teams 等 11 个平台通过此机制被发现。
Q17. 为什么 TGAdapter 需要 fallback IP?
国内访问 TG API 需要代理,fallback IP 做故障切换。gateway/platforms/TG_network.py 的 TGFallbackTransport —— 通过预配置的 fallback IP 列表(如 proxy 出口 IP)绕过 DNS 污染 / 企业防火墙。PTB 的 request 底层替换为带 fallback 的 HTTPX transport。
Q18. SignalAdapter 依赖的 signal-cli 是什么?
signal-cli 是 Signal 的命令行客户端,提供 HTTP daemon 模式。gateway/platforms/signal.py 第 174 行 class SignalAdapter docstring —— signal-cli 启动 HTTP daemon 后暴露 REST API(如 /v1/receive/{number}、/v1/send/{number}),adapter 通过 HTTP 与之通信,自己不直接对接 Signal 协议。
Q19. MessageDeduplicator 在哪些 adapter 中使用?
微信 / 钉钉 / 企业微信。gateway/platforms/helpers.py 中定义的 MessageDeduplicator 被多个 adapter 复用(通过 from gateway.platforms.helpers import MessageDeduplicator)。微信用 TTL=300s 去重(gateway/platforms/weixin.py 第 1159 行),钉钉用 max_size=1000(gateway/platforms/dingtalk.py 第 207 行),企业微信也用(gateway/platforms/wecom.py 第 184 行)。用于防止长轮询和 Webhook 平台的重放攻击。
Q20. 插件 adapter 和内置 adapter 的接入方式有什么不同?
没有任何不同。两者都继承 BasePlatformAdapter,都实现 3 个 @abstractmethod(connect / disconnect / send),都通过 Platform enum 标识。区别只是发现方式:内置 adapter 在 gateway/platforms/ 代码库里,插件 adapter 在 plugins/platforms/ 目录里(通过 _scan_bundled_plugin_platforms 发现)。
FAQ 全篇总纲
- Platform enum:24 个内置 + 动态 pseudo-member + identity 稳定性(Q1 / Q2 / Q3)
- 接入协议:长轮询 / WebSocket / Webhook / 特殊桥接分类(Q4 / Q6 / Q8 / Q10)
- 媒体处理:AES-CDN / IMAP 附件 / media group / CDN 上传(Q7 / Q14)
- 安全验签:HMAC / defusedxml XXE / HMAC 时序攻击(Q9 / Q10 / Q17)
- 能力差异:supports_code_blocks / SUPPORTS_MESSAGE_EDITING / MAX_MESSAGE_LENGTH(Q11 / Q12 / Q13)
- 插件发现:_scan_bundled_plugin_platforms / _missing_ / plugin.yaml(Q16 / Q20)
下一篇预告
第 11 篇 — 记忆系统插件化架构(敬请期待)
Layer 5 平台适配器层覆盖完了 —— Hermes 现在能接入 24 个内置平台 + 11 个插件平台的消息。本系列前 5 篇讲了 Agent 怎么调工具(Layer 2)、怎么跑命令(Layer 2.5)、怎么记长期事实(Layer 3)、Gateway 怎么处理 chat(Layer 4)、适配器怎么接入各平台(Layer 5)。
下一篇切到记忆系统的插件化架构:
- MemoryProvider ABC —— honcho / mem0 / supermemory / byterover / hindsight / holographic / openviking / retaindb 各怎么实现记忆读写
- MemoryManager —— 怎么路由到当前 provider
- post_setup 钩子 —— 怎么在
hermes memory setup时完成 provider 特定初始化
Layer 5 / Layer-adapter 五层结构完整后,下一步是 Layer 6 —— Cron 调度 + 子代理委托。

浙公网安备 33010602011771号