外掛系統
職責
外掛 (plugin) 是 Hermes 的擴展骨架:provider profile、平台轉接器 (adapter)、context engine、cron provider、記憶後端、瀏覽器、圖像/影片生成……都透過外掛機制接入。plugins/ 提供 PluginContext(註冊入口)+ 工具函式,各外掛子目錄按領域自包含。它和 ToolRegistry/PlatformRegistry 協同:外掛只負責「宣告並註冊」,註冊表 (registry) 負責「查詢與調度」。
設計動機
為什麼是「目錄約定 + 模組匯入時自註冊」,而不是顯式 plugins.yaml?核心是降低新增能力的摩擦:把子目錄扔進 $HERMES_HOME/plugins/model-providers/<name>/,__init__.py 被發現時自動 import,模組級程式碼呼叫 register_provider() 完成註冊——不用改設定檔。代價是排序和覆蓋要靠註冊表策略(命名空間、override 顯式 opt-in)兜底,換來的是熱插拔和第三方外掛零摩擦接入。
關鍵檔案
plugins/__init__.py—PluginContext、註冊入口(register_platform、register_provider等)plugin_utils.py— 外掛共用工具函式model-providers/— LLM provider profile(見 Providers)platforms/— Telegram/Discord/Slack 等平台轉接器(見平台轉接器)context_engine/— 上下文 (context) 引擎擴展cron_providers/— cron 排程後端memory/— 記憶後端browser//web/— 瀏覽器與 Web 能力image_gen//video_gen/— 多模態生成observability//security-guidance/— 可觀測與安全
資料流
- 啟動期
init_agent(agent/agent_init.py:276)觸發外掛發現。 - 每個外掛子目錄被匯入,模組級程式碼呼叫
PluginContext的註冊方法:- model-provider →
register_provider:53 - platform →
platform_registry.register:231 - 工具 (tool) →
registry.register:365
- model-provider →
- 註冊表(單例)成為查詢真相,主迴圈與網關 (gateway) 只讀註冊表,不直接依賴具體外掛。
- 外掛可熱插拔:卸載只需
unregister,註冊表處理優先級與覆蓋。
關鍵程式碼
目錄掃描發現外掛
_discover_providers 是 model-providers 的發現入口。它掃描兩個目錄(倉庫內建 + $HERMES_HOME 使用者目錄),對每個子目錄呼叫 _import_plugin_dir:
def _discover_providers() -> None:
"""1. Bundled at <repo>/plugins/model-providers/<name>/
2. User at $HERMES_HOME/plugins/model-providers/<name>/
Later steps win on name collision."""
global _discovered
if _discovered: return
_discovered = True
if _BUNDLED_PLUGINS_DIR.is_dir():
for child in sorted(_BUNDLED_PLUGINS_DIR.iterdir()):
if child.is_dir() and not child.name.startswith(("_", ".")):
_import_plugin_dir(child, "bundled")
user_dir = _user_plugins_dir()
if user_dir is not None:
for child in sorted(user_dir.iterdir()):
if child.is_dir() and not child.name.startswith(("_", ".")):
_import_plugin_dir(child, "user")「使用者目錄覆蓋內建」就來自這個順序:兩步都呼叫 register_provider,後者覆蓋前者。_/. 前綴的目錄被忽略,留出 __pycache__ 之類的逃生口。
自註冊:模組匯入即註冊
_import_plugin_dir 用 importlib.util 把目錄當模組載入,模組級程式碼執行時呼叫 register_provider(profile) 把 profile 推進全域註冊表:
module_name = (f"plugins.model_providers.{safe_name}" if source == "bundled"
else f"_hermes_user_provider_{safe_name}")
spec = importlib.util.spec_from_file_location(
module_name, init_file, submodule_search_locations=[str(plugin_dir)])
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)bundled 和 user 走不同模組命名空間,避免兩個 HERMES_HOME 下的同名 profile 在 sys.modules 裡互相 alias。
註冊表:防止默默覆蓋內建工具
registry.register 預設拒絕跨 toolset 的同名覆蓋。MCP 之間允許覆蓋(刷新場景合法),但外掛想替內建工具必須顯式 override=True,且要先有 operator 在 config 裡 allow_tool_override: true 才放行——否則直接拋 PermissionError,而不是默默替換。owner 判定基於 handler.__globals__["__name__"],定義時綁定,外掛沒法用巢狀 lambda 把覆蓋洗白成「來自內建模組」。詳見 registry.register:365-436。
邊界與失敗
- 外掛 import 失敗:
_import_plugin_dir把例外吞掉只打 warning,並從sys.modules彈出避免半載入狀態。一個壞外掛不會拖死整個 agent,但對應的 provider/platform 就拿不到,需要看日誌才能發現。 - 覆蓋內建工具沒顯式 opt-in:
registry.register拋PermissionError而不是默默替換;operator 必須在 config.yaml 裡寫plugins.entries.<plugin_id>.allow_tool_override: true才放行。 - context engine 不可深拷貝:多 agent 共享的 context engine 在 child agent 啟動時
copy.deepcopy隔離 budget 狀態;外掛若持有鎖、DB 連線等不可深拷貝物件,深拷貝失敗會 fallback 到內建 compressor。
小結
外掛 = 「子目錄 + 註冊」。所有跨切面能力(provider、平台、工具、記憶、cron、多模態)都走同一套註冊骨架,核心迴圈因此保持穩定。新增能力 = 新建一個外掛子目錄並註冊,不必動 agent 主幹。