【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 # 回退本地模式
当你想添加第四个云浏览器供应商时,你需要:
- 在
tools/browser_providers/新建一个模块 - 在
_get_cloud_provider()里加一个新的 elif 分支 - 在工具选择器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 内置插件
内置插件在 下,自动加载(kind: backend)。用户插件在 ~/.hermes/plugins/browser/ 下,需要 plugins.enabled 显式启用。记得把自定义插件放在用户目录下。
与web_search插件系统的对比
这次重构刻意镜像了PR #25182引入的web_search插件模式:
| 维度 | web_search | browser(新) |
|---|---|---|
| ABC | WebSearchProvider | BrowserProvider |
| 注册入口 | 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 浏览器插件重构提交 a15cdfb
- BrowserProvider ABC 提交 c6e6909
- 浏览器base插件提交 b8138ac
- Web Search插件化参考 PR #25182
本文基于Hermes Agent 2026年5月17日最新commits撰写,信息截至发稿时准确。插件API可能随版本更新变化,请以官方文档为准。
本文由AI辅助创作,经人工审核编辑发布
