Agent工坊

【Agent工坊】Hermes Agent浏览器插件系统v2.0全拆解:从硬编码到可插拔架构的进化之路

【Agent工坊】Hermes Agent浏览器插件系统v2.0全拆解:从硬编码到可插拔架构的进化之路

Hermes Agent 刚刚完成了一次底层架构大重构——浏览器自动化从3个硬编码供应商变成了完全可插拔的插件系统,新增542行代码,引入BrowserProvider抽象基类。这篇文章带你理解这次重构的全部细节,以及如何为Hermes写一个自定义浏览器插件。


发生了什么

2026年5月17日,Hermes Agent 主仓库连续合并了6个commit,完成了一次酝酿已久的浏览器子系统重构。核心变化只有一句话:浏览器云供应商从硬编码的3个if/else分支,变成了一个基于插件的注册-发现-路由系统


重构涉及的文件量:


  • 新增 agent/browser_provider.py(155行,BrowserProvider ABC)
  • 新增 agent/browser_registry.py(221行,注册中心+4级优先级路由)
  • 新增 plugins/browser/browserbase/provider.py(292行,第1个迁移的插件)
  • 新增 plugins/browser/browser_use/provider.py(浏览器使用插件 + 托管网关)
  • 新增 plugins/browser/firecrawl/provider.py(Firecrawl云浏览器)
  • 修改 hermes_cli/plugins.py(+32行,register_browser_provider 注册入口)
  • 修改 hermes_cli/tools_config.py(工具选择器UI改为动态注入)
  • 删除 tools/browser_providers/ 整个目录(旧的硬编码供应商模块)

总计:+530行新增,-12行删除——这是一次建立在现有接口契约之上的干净重构,旧的生命周期约定被逐位保留。


为什么要重构

旧架构的核心问题在代码里一眼可见。tools/browser_tool.py 中有类似这样的逻辑:


# 旧架构(已删除):硬编码的三路分支
def _get_cloud_provider():
    if has_browser_use_creds():
        return BrowserUseProvider()
    elif has_browserbase_creds():
        return BrowserbaseProvider()
    elif has_firecrawl_creds():
        return FirecrawlProvider()
    return None  # 回退本地模式


当你想添加第四个云浏览器供应商时,你需要:


  1. tools/browser_providers/ 新建一个模块
  2. _get_cloud_provider() 里加一个新的 elif 分支
  3. 在工具选择器UI里加一行硬编码的供应商配置

这使得添加新供应商成为"内部人操作"——每次都要改核心代码。而Hermes的web_search子系统早在PR #25182就完成了同样的插件化迁移,浏览器子系统是最后一个还在用硬编码模式的模块。


新架构:三层结构

▲ 图1:浏览器插件三层架构 — BrowserProvider ABC抽象基类 → BrowserRegistry注册中心路由 → 三个独立插件实现

重构后的浏览器子系统有三层清晰的抽象:


第1层:BrowserProvider ABC(接口契约)

# agent/browser_provider.py(核心抽象)
class BrowserProvider(abc.ABC):
    @abc.abstractmethod
    def is_available(self) -> bool:
        """检查凭据是否配置"""
        ...

    @abc.abstractmethod
    def create_session(self) -> dict:
        """创建云浏览器会话,返回会话元数据"""
        ...

    @abc.abstractmethod
    def close_session(self, session_meta: dict) -> None:
        """关闭会话"""
        ...


核心约束——每个插件必须实现三个方法 is_available()create_session()close_session(),返回的会话元数据遵守统一的字典结构:


{
    "session_name": str,        # agent-browser --session 唯一名称
    "bb_session_id": str,       # 供应商会话ID(遗留键名,保持兼容)
    "cdp_url": str,             # CDP WebSocket URL
    "features": dict,           # 启用的特性标志
    "external_call_id": str,    # 可选,托管网关计费密钥
}


注意:bb_session_id 这个键名来自Browserbase时代的遗留命名——它的值是当前供应商的会话ID,不管实际用的是哪个供应商。重构时为了不让 browser_tool.py 需要翻译键名,保留了这个历史命名。


第2层:BrowserRegistry(注册-发现-路由)

▲ 图2:4级路由优先级解析流程 — 显式配置 > 单供应商自动 > 遗留顺序(browser-use→browserbase) > 本地回退

# agent/browser_registry.py(核心路由逻辑)
# 4级优先级:
# 1. config.yaml 中显式指定 browser.cloud_provider(忽略可用性检查)
# 2. 只有一个供应商可用时自动使用
# 3. 遗留优先级:browser-use → browserbase(Firecrawl不参与自动选择)
# 4. 以上都不满足 → 回退本地浏览器模式


最关键的设计决策在规则3:Firecrawl 故意不参与自动选择。原因是Firecrawl的API Key同时用于web-extract和cloud-browser两个子系统——如果用户的 FIRECRAWL_API_KEY 只是为了网页内容提取,自动将其路由到付费云浏览器会造成意外扣费。只有用户显式设置 browser.cloud_provider: firecrawl 才会使用Firecrawl云浏览器。


规则4也有一个精妙之处:如果调用 _resolve('browserbase')BROWSERBASE_API_KEY 未设置,解析器会返回该供应商实例而不是 None——这让分发器可以向用户输出一条有意义的错误消息"BROWSERBASE_API_KEY未设置",而不是静默切换到其他后端。


第3层:插件模块(具体实现)

▲ 图3:插件注册代码结构 — register_browser_provider入口 → PluginContext注册 → 自动发现与类型检查

plugins/browser/
├── browserbase/
│   ├── __init__.py     # 自动注册入口
│   ├── plugin.yaml     # 插件元数据
│   └── provider.py     # BrowserProvider实现(292行)
├── browser_use/
│   ├── __init__.py
│   ├── plugin.yaml
│   └── provider.py     # 双认证模式(直接API Key / Nous托管网关)
└── firecrawl/
    ├── __init__.py
    ├── plugin.yaml
    └── provider.py     # FIRECRAWL_API_KEY直接认证


每个插件通过 PluginContext.register_browser_provider() 在import时自动注册。hermes_cli/plugins.py 新增了与 register_web_search_provider() 镜像的注册入口:


# 插件注册接口(hermes_cli/plugins.py)
def register_browser_provider(self, provider) -> None:
    """注册云浏览器后端"""
    from agent.browser_provider import BrowserProvider
    from agent.browser_registry import register_provider

    if not isinstance(provider, BrowserProvider):
        logger.warning("插件 %s 的浏览器供应商未继承BrowserProvider,忽略", ...)
        return
    register_provider(provider)


实操:写一个自定义浏览器插件

▲ 图4:会话生命周期管理 — create_session创建实例 → 浏览器自动化操作 → close_session销毁会话

假设你要为Hermes添加一个自建浏览器池的插件。以下是完整步骤:


步骤1:创建插件目录结构

mkdir -p ~/.hermes/plugins/browser/my-pool/
cd ~/.hermes/plugins/browser/my-pool/


步骤2:编写 `plugin.yaml`

name: my-browser-pool
version: "1.0.0"
kind: backend
description: "自建浏览器池——管理一组预热的Chromium实例"


步骤3:实现 `provider.py`

"""自建浏览器池插件"""
import os
from agent.browser_provider import BrowserProvider

class MyBrowserPoolProvider(BrowserProvider):
    name = "my-pool"

    def is_available(self) -> bool:
        return bool(os.getenv("MY_POOL_ENDPOINT"))

    def create_session(self) -> dict:
        # 调用自建池的API获取浏览器实例
        import requests
        resp = requests.post(
            f"{os.environ['MY_POOL_ENDPOINT']}/sessions",
            timeout=30
        )
        data = resp.json()
        return {
            "session_name": f"mypool-{data['id']}",
            "bb_session_id": data["id"],
            "cdp_url": data["cdp_url"],
            "features": {"network": True, "screenshot": True},
            "external_call_id": "",
        }

    def close_session(self, session_meta: dict) -> None:
        import requests
        requests.delete(
            f"{os.environ['MY_POOL_ENDPOINT']}/sessions/{session_meta['bb_session_id']}",
            timeout=15
        )


步骤4:编写 `__init__.py`(自动注册)

"""自建浏览器池插件——自动注册入口"""
from .provider import MyBrowserPoolProvider

def setup(context):
    """Hermes在加载插件时调用此函数"""
    context.register_browser_provider(MyBrowserPoolProvider())


步骤5:启用插件并配置

~/.hermes/config.yaml 中:


plugins:
  enabled:

    - my-browser-pool

browser:
  cloud_provider: my-pool  # 显式指定供应商


步骤6:验证

hermes config validate
# 输出应包含:Plugin 'my-browser-pool' registered browser provider: my-pool

hermes browser navigate "https://example.com"
# 如果MY_POOL_ENDPOINT已配置,将路由到你的自建池


踩坑提醒

坑1:名字冲突

插件名(plugin.yaml中的name)和供应商名(provider.name)是两个不同的东西。供应商名是 browser.cloud_provider 配置匹配的值,插件名是启用/禁用插件的标识。确保两者一致可避免配置时的困惑。


坑2:`is_available()` 的语义

is_available() 只检查凭据是否配置——不代表服务可达。如果你的API端点挂了但 is_available() 返回True,Hermes会尝试调用 create_session() 然后收到超时错误。建议在 create_session() 里做好异常处理。


坑3:`bb_session_id` 遗留命名

这是从Browserbase时代留下的键名。你的供应商必须把它的会话ID放在这个键里——Hermes的工具层代码只读 session_meta["bb_session_id"]。不要创建新的键名。


坑4:用户插件路径 vs 内置插件

内置插件在 /plugins/browser/ 下,自动加载(kind: backend)。用户插件在 ~/.hermes/plugins/browser/ 下,需要 plugins.enabled 显式启用。记得把自定义插件放在用户目录下。


与web_search插件系统的对比

这次重构刻意镜像了PR #25182引入的web_search插件模式:


维度web_searchbrowser(新)
ABCWebSearchProviderBrowserProvider
注册入口register_web_search_provider()register_browser_provider()
路由_resolve() + 功能分派(search/extract/crawl)_resolve() + 单一生命周期
工具选择器_plugin_web_search_providers()_plugin_browser_providers()
区别每个供应商可实现不同功能子集每个供应商实现完整生命周期

关键差异:web_search子系统有功能分派(search/extract/crawl可以由不同供应商实现),而browser子系统每个供应商都实现完整生命周期——注册中心只负责选择,不做能力路由。这使得browser的注册逻辑更简单。


这对AI创业者意味着什么

1. 企业级部署场景

有自建浏览器池需求的企业现在可以通过一个插件就接入Hermes,不需要维护fork。如果你的公司有10个员工每天用Hermes做网页自动化,自建池可以:


  • 统一管理浏览器实例生命周期
  • 集中控制网络出口IP
  • 按部门配额限流

2. 供应商锁定不再是问题

如果Browserbase明天涨价3倍,你只需换一个插件——config.yaml里改一行 browser.cloud_provider。旧架构下这种切换需要改核心代码然后重新部署。


3. 插件生态的想象空间

这是Hermes第2个完成插件化的核心子系统(第1个是web_search)。随着更多子系统采用同样的ABC+Registry模式,Hermes将从"一个工具"演化为"一个平台"——第三方开发者可以独立发布扩展,而不需要等官方合并PR。


相关链接

本文基于Hermes Agent 2026年5月17日最新commits撰写,信息截至发稿时准确。插件API可能随版本更新变化,请以官方文档为准。


本文由AI辅助创作,经人工审核编辑发布