添加平台适配器

本指南涵盖了向 Hermes 网关添加新消息平台的内容。平台适配器将 Hermes 连接到外部消息服务(Telegram、Discord、企业微信等),使用户能够通过该服务与智能体交互。

:::tip 有两种方式添加平台:

  • 插件方式(推荐用于社区/第三方):将插件目录放入 ~/.hermes/plugins/ —— 无需修改任何核心代码。请参见下文插件方式
  • 内置方式:在代码、配置和文档中修改 20+ 个文件。请参见下文内置方式清单。 :::

架构概览

用户 ↔ 消息平台 ↔ 平台适配器 ↔ Gateway Runner ↔ AIAgent

每个适配器都继承自 gateway/platforms/base.pyBasePlatformAdapter,并实现:

  • connect() — 建立连接(WebSocket、长轮询、HTTP 服务器等)(抽象方法)
  • disconnect() — 干净关闭 (抽象方法)
  • send() — 向聊天发送文本消息 (抽象方法)
  • send_typing() — 显示正在输入指示(可选覆盖)
  • get_chat_info() — 返回聊天元数据(可选覆盖)

入站消息由适配器接收,并通过 self.handle_message(event) 转发,基类会将其路由到网关运行器。

插件方式(推荐)

插件系统让您无需修改任何 Hermes 核心代码即可添加平台适配器。您的插件包含两个文件的目录:

~/.hermes/plugins/my-platform/
  PLUGIN.yaml      # 插件元数据
  adapter.py       # 适配器类 + register() 入口点

PLUGIN.yaml

插件元数据。requires_envoptional_env 块会自动填充 hermes config UI 条目(请参见下文hermes config 中展示环境变量)。

name: my-platform
label: My Platform
kind: platform
version: 1.0.0
description: 我的自定义消息平台适配器
author: Your Name
requires_env:
  - MY_PLATFORM_TOKEN          # 纯字符串即可
  - name: MY_PLATFORM_CHANNEL  # 或使用富字典以获得更好的 UX
    description: "要加入的频道"
    prompt: "Channel"
    password: false
optional_env:
  - name: MY_PLATFORM_HOME_CHANNEL
    description: "Cron 投递的默认频道"
    password: false

adapter.py

import os
from gateway.platforms.base import (
    BasePlatformAdapter, SendResult, MessageEvent, MessageType,
)
from gateway.config import Platform, PlatformConfig
 
 
class MyPlatformAdapter(BasePlatformAdapter):
    def __init__(self, config: PlatformConfig):
        super().__init__(config, Platform("my_platform"))
        extra = config.extra or {}
        self.token = os.getenv("MY_PLATFORM_TOKEN") or extra.get("token", "")
 
    async def connect(self) -> bool:
        # 连接到平台 API,启动监听器
        self._mark_connected()
        return True
 
    async def disconnect(self) -> None:
        self._mark_disconnected()
 
    async def send(self, chat_id, content, reply_to=None, metadata=None):
        # 通过平台 API 发送消息
        return SendResult(success=True, message_id="...")
 
    async def get_chat_info(self, chat_id):
        return {"name": chat_id, "type": "dm"}
 
 
def check_requirements() -> bool:
    return bool(os.getenv("MY_PLATFORM_TOKEN"))
 
 
def validate_config(config) -> bool:
    extra = getattr(config, "extra", {}) or {}
    return bool(os.getenv("MY_PLATFORM_TOKEN") or extra.get("token"))
 
 
def _env_enablement() -> dict | None:
    token = os.getenv("MY_PLATFORM_TOKEN", "").strip()
    channel = os.getenv("MY_PLATFORM_CHANNEL", "").strip()
    if not (token and channel):
        return None
    seed = {"token": token, "channel": channel}
    home = os.getenv("MY_PLATFORM_HOME_CHANNEL")
    if home:
        seed["home_channel"] = {"chat_id": home, "name": "Home"}
    return seed
 
 
def register(ctx):
    """插件入口点——由 Hermes 插件系统调用。"""
    ctx.register_platform(
        name="my_platform",
        label="My Platform",
        adapter_factory=lambda cfg: MyPlatformAdapter(cfg),
        check_fn=check_requirements,
        validate_config=validate_config,
        required_env=["MY_PLATFORM_TOKEN"],
        install_hint="pip install my-platform-sdk",
        # 基于环境变量的自动配置——在适配器构造前将
        # 环境变量填入 PlatformConfig.extra。
        # 请参见下方的"基于环境变量的自动配置"部分。
        env_enablement_fn=_env_enablement,
        # Cron 主频道投递支持。使 deliver=my_platform 的 cron
        # 任务无需修改 cron/scheduler.py 即可路由。
        # 请参见下方的"Cron 投递"部分。
        cron_deliver_env_var="MY_PLATFORM_HOME_CHANNEL",
        # 按平台的用户授权环境变量
        allowed_users_env="MY_PLATFORM_ALLOWED_USERS",
        allow_all_env="MY_PLATFORM_ALLOW_ALL_USERS",
        # 智能分块的消息长度限制(0 = 无限制)
        max_message_length=4000,
        # 注入到系统提示词中的 LLM 指引
        platform_hint=(
            "您正在通过 My Platform 聊天。"
            "它支持 Markdown 格式。"
        ),
        # 显示
        emoji="💬",
    )
 
    # 可选:注册平台特定工具
    ctx.register_tool(
        name="my_platform_search",
        toolset="my_platform",
        schema={...},
        handler=my_search_handler,
    )

配置

用户在 config.yaml 中配置平台:

gateway:
  platforms:
    my_platform:
      enabled: true
      extra:
        token: "..."
        channel: "#general"

或者通过环境变量(适配器在 __init__ 中读取)。

插件系统自动处理的内容

当您调用 ctx.register_platform() 时,以下集成点会自动为您处理——无需修改核心代码:

集成点工作原理
网关适配器创建注册表在内置 if/elif 链之前被检查
配置解析Platform._missing_() 接受任何平台名称
已连接平台验证调用注册表 validate_config()
用户授权检查 allowed_users_env / allow_all_env
仅环境变量自动启用env_enablement_fn 填充 PlatformConfig.extra + home_channel
YAML 配置桥接apply_yaml_config_fnconfig.yaml 键转换为环境变量 / extras
Cron 投递cron_deliver_env_var 使 deliver=<name> 生效
hermes config UI 条目plugin.yaml 中的 requires_env / optional_env 自动填充
send_message 工具通过实时网关适配器路由
Webhook 跨平台投递检查注册表中已知的平台
/update 命令访问allow_update_command 标志
频道目录插件平台包含在枚举中
系统提示词提示platform_hint 注入到 LLM 上下文中
消息分块max_message_length 用于智能拆分
PII 脱敏pii_safe 标志
hermes status显示带 (plugin) 标签的插件平台
hermes gateway setup插件平台出现在设置菜单中
hermes tools / hermes skills插件平台在按平台配置中
令牌锁定(多配置文件)connect() 中使用 acquire_scoped_lock()
孤立配置警告插件缺失时记录描述性日志

基于环境变量的自动配置

大多数用户通过在 ~/.hermes/.env 中设置环境变量来配置平台,而不是编辑 config.yamlenv_enablement_fn 钩子让您的插件在适配器构造之前就能获取这些环境变量,因此 hermes gateway statusget_connected_platforms() 和 cron 投递在无需实例化平台 SDK 的情况下即可看到正确的状态。

def _env_enablement() -> dict | None:
    """从环境变量填充 PlatformConfig.extra。
 
    在 load_gateway_config() 期间由平台注册表调用。
    当平台未达到最低配置时返回 None——
    调用者随后跳过自动启用。返回 dict 以填充 extras。
 
    特殊的 'home_channel' 键会被提取出来,成为
    PlatformConfig 上的 HomeChannel 数据类;其他所有键
    都合并到 PlatformConfig.extra 中。
    """
    token = os.getenv("MY_PLATFORM_TOKEN", "").strip()
    channel = os.getenv("MY_PLATFORM_CHANNEL", "").strip()
    if not (token and channel):
        return None
    seed = {"token": token, "channel": channel}
    home = os.getenv("MY_PLATFORM_HOME_CHANNEL")
    if home:
        seed["home_channel"] = {
            "chat_id": home,
            "name": os.getenv("MY_PLATFORM_HOME_CHANNEL_NAME", "Home"),
        }
    return seed
 
 
def register(ctx):
    ctx.register_platform(
        name="my_platform",
        label="My Platform",
        adapter_factory=lambda cfg: MyPlatformAdapter(cfg),
        check_fn=check_requirements,
        validate_config=validate_config,
        env_enablement_fn=_env_enablement,
        # ... 其他字段
    )

YAML→env 配置桥接

有些用户更喜欢设置 config.yaml 键(my_platform.require_mentionmy_platform.allowed_channels 等)而不是环境变量。apply_yaml_config_fn 钩子让您的插件自己完成这个转换,而不必强制核心 gateway/config.py 了解您平台的 YAML 结构。

import os
 
def _apply_yaml_config(yaml_cfg: dict, platform_cfg: dict) -> dict | None:
    """将 config.yaml `my_platform:` 键转换为环境变量 / extras。
 
    yaml_cfg     — 完整顶层的 config.yaml 字典
    platform_cfg — 平台自身的子字典(yaml_cfg.get("my_platform", {}))
 
    可以直接修改 os.environ(使用 `not os.getenv(...)` 守卫来
    保持环境变量 > YAML 的优先级),和/或返回一个合并到
    PlatformConfig.extra 的字典。返回 None 或 {} 表示无 extras。
    """
    if "require_mention" in platform_cfg and not os.getenv("MY_PLATFORM_REQUIRE_MENTION"):
        os.environ["MY_PLATFORM_REQUIRE_MENTION"] = str(platform_cfg["require_mention"]).lower()
    allowed = platform_cfg.get("allowed_channels")
    if allowed is not None and not os.getenv("MY_PLATFORM_ALLOWED_CHANNELS"):
        if isinstance(allowed, list):
            allowed = ",".join(str(v) for v in allowed)
        os.environ["MY_PLATFORM_ALLOWED_CHANNELS"] = str(allowed)
    return None  # 没有要合并到 PlatformConfig.extra 的内容
 
def register(ctx):
    ctx.register_platform(
        name="my_platform",
        ...,
        apply_yaml_config_fn=_apply_yaml_config,
    )

该钩子在 load_gateway_config() 期间、通用共享键循环(处理常见的键如 unauthorized_dm_behaviornotice_deliveryreply_prefixrequire_mention 等)之后、_apply_env_overrides() 之前被调用,因此您的插件只需要桥接平台特定的键。

钩子中抛出的异常会被吞掉并在调试级别记录——行为异常的插件绝不会中止网关配置加载。

Cron 投递

要让 deliver=my_platform 的 cron 任务路由到已配置的主频道,将 cron_deliver_env_var 设置为保存默认聊天/房间/频道 ID 的环境变量名称:

ctx.register_platform(
    name="my_platform",
    ...
    cron_deliver_env_var="MY_PLATFORM_HOME_CHANNEL",
)

调度器在解析 deliver=my_platform 任务的主目标时读取此环境变量,并在 _KNOWN_DELIVERY_PLATFORMS 样式的检查中也将该平台视为有效的 cron 目标。如果您的 env_enablement_fn 填充了 home_channel 字典(见上文),则该字典优先——cron_deliver_env_var 是在环境变量填充之前运行的 cron 任务的回退方案。

进程外 cron 投递

cron_deliver_env_var 使您的平台成为认可的 deliver= 目标。要使实际发送在 cron 任务与网关在不同进程中运行时成功(即 hermes cron runhermes gateway 分开运行),请注册一个 standalone_sender_fn

async def _standalone_send(
    pconfig,
    chat_id,
    message,
    *,
    thread_id=None,
    media_files=None,
    force_document=False,
):
    """打开临时连接 / 获取新令牌,发送,然后关闭。"""
    # ... 打开连接,发送消息,返回结果 ...
    return {"success": True, "message_id": "..."}
    # 或 {"error": "..."}
 
ctx.register_platform(
    name="my_platform",
    ...
    cron_deliver_env_var="MY_PLATFORM_HOME_CHANNEL",
    standalone_sender_fn=_standalone_send,
)

为什么需要这个钩子:内置平台(Telegram、Discord、Slack 等)在 tools/send_message_tool.py 中提供了直接的 REST 辅助程序,因此 cron 可以在不将网关保存在同一进程中的情况下投递。插件平台历史上依赖于 _gateway_runner_ref(),它在网关进程外部返回 None,因此没有 standalone_sender_fn 时,cron 端发送会失败并显示 No live adapter for platform '<name>'

该函数接收与实时适配器相同的 pconfigchat_id,以及可选的 thread_idmedia_filesforce_document 关键字参数。返回 {"success": True, "message_id": ...} 视为成功投递;返回 {"error": "..."} 则在 cron 的 delivery_errors 中显示消息。函数内抛出的异常会被调度器捕获并报告为 Plugin standalone send failed: <reason>。参考实现见 plugins/platforms/{irc,teams,google_chat}/adapter.py

hermes config 中展示环境变量

hermes_cli/config.py 在导入时扫描 plugins/platforms/*/plugin.yaml,并从 requires_env 和(可选的)optional_env 块中自动填充 OPTIONAL_ENV_VARS。使用富字典形式可以提供正确的描述、提示、密码标志和 URL——CLI 设置 UI 会自动拾取这些信息。

# plugins/platforms/my_platform/plugin.yaml
name: my_platform-platform
label: My Platform
kind: platform
version: 1.0.0
description: >
  Hermes Agent 的 My Platform 网关适配器。
author: Your Name
requires_env:
  - name: MY_PLATFORM_TOKEN
    description: "来自 My Platform 控制台的 Bot API 令牌"
    prompt: "My Platform bot token"
    url: "https://my-platform.example.com/bots"
    password: true
  - name: MY_PLATFORM_CHANNEL
    description: "要加入的频道(例如 #hermes)"
    prompt: "Channel"
    password: false
optional_env:
  - name: MY_PLATFORM_HOME_CHANNEL
    description: "Cron 投递的默认频道(默认为 MY_PLATFORM_CHANNEL)"
    prompt: "主频道(或留空)"
    password: false
  - name: MY_PLATFORM_ALLOWED_USERS
    description: "允许与机器人对话的用户 ID(逗号分隔)"
    prompt: "允许的用户(逗号分隔)"
    password: false

支持的字典键: name(必需)、descriptionprompturlpassword(布尔值;省略时自动从 *_TOKEN / *_SECRET / *_KEY / *_PASSWORD / *_JSON 后缀检测)、category(默认为 "messaging")。

纯字符串条目(- MY_PLATFORM_TOKEN)仍然有效——它们会从插件的 label 自动派生通用描述。如果 OPTIONAL_ENV_VARS 中已存在同名的硬编码条目,则硬编码条目优先(向后兼容);plugin.yaml 形式作为回退方案。

平台特定的慢 LLM UX

某些平台有限制条件,会改变慢 LLM 响应的呈现方式:

  • LINE 发放单次使用的 reply token,在入站事件后大约 60 秒过期。使用该令牌回复是免费的;回退到计费的 Push API 则不然。如果 LLM 未能在截止日期前完成,则需要在”消耗付费的 Push 配额”或”在回复令牌过期前用更聪明的方式处理它”之间做出选择。
  • WhatsApp 在 24 小时后将会话标记为不活跃,此后只接受模板消息。
  • SMS 没有输入指示或渐进式更新的概念——长时间的响应只会让人觉得机器人离线了。

这些都是基础的 BasePlatformAdapter 无法预料的真实限制。插件接口有意留有空间,让适配器可以在基础输入循环之上添加平台特定的 UX,而无需扩展参数列表。

模式:覆盖 _keep_typing 以添加中途 UX

BasePlatformAdapter._keep_typing 是输入指示的心跳——它在 LLM 生成时作为后台任务运行,并在响应送达时取消。要在阈值处添加平台特定行为(例如在 45 秒时发送”仍在思考”气泡),在适配器中覆盖 _keep_typing,与 super()._keep_typing() 一起调度您自己的任务,并在 finally 中清理:

class LineAdapter(BasePlatformAdapter):
    async def _keep_typing(self, chat_id: str, *args, **kwargs) -> None:
        if self.slow_response_threshold <= 0:
            await super()._keep_typing(chat_id, *args, **kwargs)
            return
 
        async def _fire_at_threshold() -> None:
            try:
                await asyncio.sleep(self.slow_response_threshold)
            except asyncio.CancelledError:
                raise
            # 在此进行平台特定的工作——对于 LINE,发送一个 Template
            # Buttons "获取答案"气泡,使用缓存的回复令牌
            # 以便用户稍后可以通过回调查中的全新(免费)回复令牌获取缓存的响应
            await self._send_slow_response_button(chat_id)
 
        side_task = asyncio.create_task(_fire_at_threshold())
        try:
            await super()._keep_typing(chat_id, *args, **kwargs)
        finally:
            if not side_task.done():
                side_task.cancel()
                try:
                    await side_task
                except (asyncio.CancelledError, Exception):
                    pass

关键点:

  • 始终 await super()._keep_typing(...) 输入心跳本身是有用的——不要替换它,而是在它之上分层。
  • finally 中清理侧边任务。 当 LLM 完成(或 /stop 取消运行)时,网关会取消输入任务。您的侧边任务也必须观察该取消,否则它会残留并在响应已经发送后触发。
  • interrupt_session_activity 配合使用以在用户发出 /stop 时解决任何孤立的 UX 状态。对于 LINE,这意味着将回调查找缓存条目从 PENDING 转换为 ERROR,以便持久的”获取答案”按钮发送”运行已中断”消息而不是循环等待。

模式:覆盖 send 以通过缓存路由而不是立即发送

如果您的慢响应 UX 将响应缓存以供后续获取(LINE 的回调查找流程),您的 send 覆盖需要识别三种模式:

  1. 此聊天的待处理回调查找已激活 → 将响应缓存在 request_id 下,不要发送任何可见内容。
  2. 系统忙碌确认⚡ Interrupting⏳ Queued⏩ Steered)→ 绕过缓存并发送可见内容,以便用户看到网关对其输入的响应。
  3. 正常响应 → 像往常一样通过 reply-token-or-push 发送。
async def send(self, chat_id: str, content: str, **kw) -> SendResult:
    if _is_system_bypass(content):
        return await self._send_text_chunks(chat_id, content, force_push=False)
    pending_rid = self._pending_buttons.get(chat_id)
    if pending_rid:
        self._cache.set_ready(pending_rid, content)
        return SendResult(success=True, message_id=pending_rid)
    return await self._send_text_chunks(chat_id, content, force_push=False)

_SYSTEM_BYPASS_PREFIXES 是网关自身的忙碌确认前缀(💾)。始终让这些可见地通过,无论缓存的 UX 状态如何。

何时适合使用此模式

在以下情况下使用输入循环覆盖方法:

  • 平台的出站 API 有严格的时间窗口约束(单次使用回复令牌、过期粘性会话等)并且
  • 在该平台上可见的中途气泡是可接受的 UX。

在以下情况下使用更简单的 slow_response_threshold = 0 始终 Push 路径:

  • 平台没有有意义的免费与付费区分,或者
  • 用户社区更喜欢”加载中… 加载中… 完成”的静默-然后-响应,而不是交互式的中间气泡。

LINE 两者都支持:阈值默认为 45 秒用于免费回调查找获取,LINE_SLOW_RESPONSE_THRESHOLD=0 则回退到”始终 Push 回退”。

参考实现

参见 plugins/platforms/line/adapter.py 了解完整的 LINE 回调查找实现——RequestCache 状态机(PENDING → READY → DELIVERED,以及用于 /stopERROR)、在阈值触发 Template Buttons 气泡的 _keep_typing 覆盖、通过缓存路由的 send 覆盖,以及解决孤立 PENDING 条目的 interrupt_session_activity 覆盖。

参考实现(插件方式)

参见仓库中 plugins/platforms/irc/ 的完整工作示例——一个完全异步的 IRC 适配器,零外部依赖。plugins/platforms/teams/ 涵盖了 Bot Framework / Adaptive Cards,plugins/platforms/google_chat/ 涵盖了基于 OAuth 的 REST API,plugins/platforms/line/ 涵盖了带有平台特定慢 LLM UX 的 Webhook 驱动的 Messaging API。


Step-by-Step 清单(内置方式)

:::note 此清单用于将平台直接添加到 Hermes 核心代码库——通常由核心贡献者为官方支持的平台完成。社区/第三方平台应使用上面的插件方式。 :::

1. 平台枚举

gateway/config.pyPlatform 枚举中添加您的平台:

class Platform(str, Enum):
    # ... 已有平台 ...
    NEWPLAT = "newplat"

2. 适配器文件

创建 gateway/platforms/newplat.py

from gateway.config import Platform, PlatformConfig
from gateway.platforms.base import (
    BasePlatformAdapter, MessageEvent, MessageType, SendResult,
)
 
def check_newplat_requirements() -> bool:
    """如果依赖可用则返回 True。"""
    return SOME_SDK_AVAILABLE
 
class NewPlatAdapter(BasePlatformAdapter):
    def __init__(self, config: PlatformConfig):
        super().__init__(config, Platform.NEWPLAT)
        # 从 config.extra 字典读取配置
        extra = config.extra or {}
        self._api_key = extra.get("api_key") or os.getenv("NEWPLAT_API_KEY", "")
 
    async def connect(self) -> bool:
        # 设置连接,开始轮询/webhook
        self._mark_connected()
        return True
 
    async def disconnect(self) -> None:
        self._running = False
        self._mark_disconnected()
 
    async def send(self, chat_id, content, reply_to=None, metadata=None):
        # 通过平台 API 发送消息
        return SendResult(success=True, message_id="...")
 
    async def get_chat_info(self, chat_id):
        return {"name": chat_id, "type": "dm"}

对于入站消息,构建一个 MessageEvent 并调用 self.handle_message(event)

source = self.build_source(
    chat_id=chat_id,
    chat_name=name,
    chat_type="dm",  # 或 "group"
    user_id=user_id,
    user_name=user_name,
)
event = MessageEvent(
    text=content,
    message_type=MessageType.TEXT,
    source=source,
    message_id=msg_id,
)
await self.handle_message(event)

3. 网关配置(gateway/config.py

三个接触点:

  1. get_connected_platforms() — 添加对您的平台所需凭据的检查
  2. load_gateway_config() — 添加令牌环境变量映射条目:Platform.NEWPLAT: "NEWPLAT_TOKEN"
  3. _apply_env_overrides() — 将所有 NEWPLAT_* 环境变量映射到配置

4. 网关运行器(gateway/run.py

六个接触点:

  1. _create_adapter() — 添加 elif platform == Platform.NEWPLAT: 分支
  2. _is_user_authorized() allowed_users 映射Platform.NEWPLAT: "NEWPLAT_ALLOWED_USERS"
  3. _is_user_authorized() allow_all 映射Platform.NEWPLAT: "NEWPLAT_ALLOW_ALL_USERS"
  4. 早期环境变量检查 _any_allowlist 元组 — 添加 "NEWPLAT_ALLOWED_USERS"
  5. 早期环境变量检查 _allow_all 元组 — 添加 "NEWPLAT_ALLOW_ALL_USERS"
  6. _UPDATE_ALLOWED_PLATFORMS 冻结集合 — 添加 Platform.NEWPLAT

5. 跨平台投递

  1. gateway/platforms/webhook.py — 将 "newplat" 添加到投递类型元组
  2. cron/scheduler.py — 添加到 _KNOWN_DELIVERY_PLATFORMS 冻结集合和 _deliver_result() 平台映射

6. CLI 集成

  1. hermes_cli/config.py — 将所有 NEWPLAT_* 变量添加到 _EXTRA_ENV_KEYS
  2. hermes_cli/gateway.py — 向 _PLATFORMS 列表添加条目,包含键、标签、表情符号、token_var、设置说明和变量
  3. hermes_cli/platforms.py — 添加 PlatformInfo 条目,包含标签和 default_toolset(用于 skills_configtools_config TUI)
  4. hermes_cli/setup.py — 添加 _setup_newplat() 函数(可委托给 gateway.py)并将元组添加到消息平台列表
  5. hermes_cli/status.py — 添加平台检测条目:"NewPlat": ("NEWPLAT_TOKEN", "NEWPLAT_HOME_CHANNEL")
  6. hermes_cli/dump.py — 将 "newplat": "NEWPLAT_TOKEN" 添加到平台检测字典

7. 工具

  1. tools/send_message_tool.py — 将 "newplat": Platform.NEWPLAT 添加到平台映射
  2. tools/cronjob_tools.py — 将 newplat 添加到投递目标描述字符串中

8. 工具集

  1. toolsets.py — 添加 "hermes-newplat" 工具集定义,包含 _HERMES_CORE_TOOLS
  2. toolsets.py — 将 "hermes-newplat" 添加到 "hermes-gateway" 的 includes 列表

9. 可选:平台提示

agent/prompt_builder.py — 如果您的平台有特定的渲染限制(无 Markdown、消息长度限制等),向 _PLATFORM_HINTS 字典添加条目。这会将平台特定的指引注入到系统提示词中:

_PLATFORM_HINTS = {
    # ...
    "newplat": (
        "您正在通过 NewPlat 聊天。它支持 Markdown 格式"
        "但有 4000 字符的消息长度限制。"
    ),
}

并非所有平台都需要提示——仅在智能体行为应有所不同时添加。

10. 测试

创建 tests/gateway/test_newplat.py,覆盖:

  • 从配置构建适配器
  • 消息事件构建
  • 发送方法(模拟外部 API)
  • 平台特定功能(加密、路由等)

11. 文档

文件要添加的内容
website/docs/user-guide/messaging/newplat.md完整平台设置页面
website/docs/user-guide/messaging/index.md平台对比表、架构图、工具集表、安全部分、下一步链接
website/docs/reference/environment-variables.md所有 NEWPLAT_* 环境变量
website/docs/reference/toolsets-reference.mdhermes-newplat 工具集
website/docs/integrations/index.md平台链接
website/sidebars.ts文档页面的侧边栏条目
website/docs/developer-guide/architecture.md适配器数量 + 列表
website/docs/developer-guide/gateway-internals.md适配器文件列表

对等审计

在将新平台 PR 标记为完成之前,对照已建立的平台运行对等审计:

# 查找所有提及参考平台的 .py 文件
search_files "bluebubbles" output_mode="files_only" file_glob="*.py"
 
# 查找所有提及新平台的 .py 文件
search_files "newplat" output_mode="files_only" file_glob="*.py"
 
# 第一个集合中但不在第二个集合中的任何文件都是潜在差距

.md.ts 文件重复此操作。调查每个差距——是平台枚举(需要更新)还是平台特定引用(跳过)?

常见模式

长轮询适配器

如果您的适配器使用长轮询(如 Telegram 或微信),使用轮询循环任务:

async def connect(self):
    self._poll_task = asyncio.create_task(self._poll_loop())
    self._mark_connected()
 
async def _poll_loop(self):
    while self._running:
        messages = await self._fetch_updates()
        for msg in messages:
            await self.handle_message(self._build_event(msg))

回调/Webhook 适配器

如果平台将消息推送到您的端点(如企业微信回调),运行一个 HTTP 服务器:

async def connect(self):
    self._app = web.Application()
    self._app.router.add_post("/callback", self._handle_callback)
    # ... 启动 aiohttp 服务器
    self._mark_connected()
 
async def _handle_callback(self, request):
    event = self._build_event(await request.text())
    await self._message_queue.put(event)
    return web.Response(text="success")  # 立即确认

对于具有严格响应截止时间的平台(例如企业微信的 5 秒限制),始终立即确认,之后主动通过 API 投递智能体的回复。智能体会话运行时间为 3–30 分钟——在回调响应窗口内进行内联回复是不可行的。

令牌锁

如果适配器持有一个具有唯一凭据的持久连接,添加一个作用域锁以防止两个配置文件使用相同的凭据:

from gateway.status import acquire_scoped_lock, release_scoped_lock
 
async def connect(self):
    if not acquire_scoped_lock("newplat", self._token):
        logger.error("令牌已被其他配置文件使用")
        return False
    # ... 连接
 
async def disconnect(self):
    release_scoped_lock("newplat", self._token)

参考实现

适配器模式复杂度适合作为参考
bluebubbles.pyREST + webhook简单的 REST API 集成
weixin.py长轮询 + CDN媒体处理、加密
wecom_callback.py回调/webhookHTTP 服务器、AES 加密、多应用
telegram.py长轮询 + Bot API完整功能的适配器,包含群组、线程