跳到主要內容

擴展儀表盤

Hermes Web 儀表盤(hermes dashboard)旨在無需分叉代碼庫即可重新換膚和擴展。它暴露了三個層級:

  1. 主題 (Themes) — YAML 文件,用於重繪儀表盤的調色板、排版、佈局以及各組件的裝飾樣式。將文件放入 ~/.hermes/dashboard-themes/;它將出現在主題切換器中。
  2. UI 插件 (UI plugins) — 一個包含 manifest.json 和 JavaScript 捆綁包的目錄,用於註冊標籤頁、替換內置頁面、通過頁面作用域插槽增強頁面,或將組件注入命名的 Shell 插槽中。
  3. 後端插件 (Backend plugins) — 位於該插件目錄內的 Python 文件,暴露一個 FastAPI router;路由掛載在 /api/plugins/<name>/ 下,並從插件的 UI 中調用。

這三者均支持運行時即插即用:無需克隆倉庫,無需執行 npm run build,也無需修補儀表盤源代碼。本頁是這三者的規範參考文檔。

如果您只想使用儀表盤,請參閱 Web 儀表盤。如果您想為終端 CLI(而非 Web 儀表盤)重新換膚,請參閱 皮膚與主題 — CLI 皮膚系統與儀表盤主題無關。

組件如何組合

主題和插件相互獨立但協同工作。主題可以獨立存在(僅一個 YAML 文件)。插件也可以獨立存在(僅一個標籤頁)。兩者結合可讓您構建帶有自定義 HUD 的完整視覺換膚 — 示例 strike-freedom-cockpit 演示(位於 hermes-example-plugins 配套倉庫中 — 安裝步驟參見 組合主題 + 插件演示)正是這樣做的。


目錄


主題

主題是存儲在 ~/.hermes/dashboard-themes/ 中的 YAML 文件。文件名無關緊要(系統使用的是主題的 name: 字段),但慣例是 <name>.yaml。所有字段均為可選 — 缺失的鍵會回退到內置的 default 主題,因此主題可以小至僅包含一種顏色。

快速入門 — 您的第一個主題

mkdir -p ~/.hermes/dashboard-themes
# ~/.hermes/dashboard-themes/neon.yaml
name: neon
label: Neon
description: Pure magenta on black

palette:
background: "#000000"
midground: "#ff00ff"

刷新儀表盤。點擊標題欄中的調色板圖標並選擇 Neon。背景變為黑色,文本和強調色變為品紅色,所有衍生顏色(卡片、邊框、柔和色、環等)均通過 CSS 中的 color-mix() 從該雙色三元組重新計算得出。

這就是全部入門內容:一個文件,兩種顏色。以下內容均為可選的精煉配置。

調色板、排版、佈局

這三個模塊是主題的核心。它們相互獨立 — 您可以覆蓋其中一個,而保留其他不變。

調色板(3 層)

調色板由三層顏色加上暖光暈影顏色和噪點顆粒倍增器組成。儀表盤的設計系統級聯通過 CSS color-mix() 從此三元組派生出每個兼容 shadcn 的令牌(card、popover、muted、border、primary、destructive、ring 等)。覆蓋三種顏色會級聯影響整個 UI。

描述
palette.background最深的畫布顏色 — 通常接近黑色。驅動頁面背景和卡片填充色。
palette.midground主要文本和強調色。大多數 UI 裝飾樣式讀取此顏色(前景文本、按鈕輪廓、焦點環)。
palette.foreground頂層高亮色。默認主題將其設置為 alpha 為 0 的白色(不可見);希望頂部有明亮強調色的主題可以提高其 alpha 值。
palette.warmGlowrgba(...) 字符串,用作 <Backdrop /> 的暈影顏色。
palette.noiseOpacity0–1.2 的顆粒疊加層倍增器。越低越柔和,越高越粗糙。

每層接受 {hex: "#RRGGBB", alpha: 0.0–1.0} 或純十六進制字符串(alpha 默認為 1.0)。

palette:
background:
hex: "#05091a"
alpha: 1.0
midground: "#d8f0ff" # bare hex, alpha = 1.0
foreground:
hex: "#ffffff"
alpha: 0 # invisible top layer
warmGlow: "rgba(255, 199, 55, 0.24)"
noiseOpacity: 0.7

排版

類型描述
fontSansstring正文內容的 CSS font-family 堆棧(應用於 html, body)。
fontMonostring代碼塊、<code>.font-mono 工具類的 CSS font-family 堆棧。
fontDisplaystring可選的標題/展示用字體堆棧。回退至 fontSans
fontUrlstring可選的外部樣式表 URL。在切換主題時,作為 <link rel="stylesheet"> 注入到 <head> 中。同一 URL 永遠不會被注入兩次。適用於 Google Fonts、Bunny Fonts、自託管的 @font-face 樣式表——任何可鏈接的資源。
baseSizestring根字體大小——控制 rem 比例。例如 "14px""16px"
lineHeightstring默認行高。例如 "1.5""1.65"
letterSpacingstring默認字間距。例如 "0""0.01em""-0.01em"
typography:
fontSans: '"Orbitron", "Eurostile", "Impact", sans-serif'
fontMono: '"Share Tech Mono", ui-monospace, monospace'
fontDisplay: '"Orbitron", "Eurostile", sans-serif'
fontUrl: "https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700&family=Share+Tech+Mono&display=swap"
baseSize: "14px"
lineHeight: "1.5"
letterSpacing: "0.04em"
從 UI 更改字體(無需 YAML)

儀表板標題欄中的主題選擇器在主題列表下方有一個 Font(字體)部分。在此處選擇任意字體,它將覆蓋當前激活主題的正文字體——該選擇獨立於主題,並在主題切換後保持持久化(存儲在 config.yamldashboard.font 下)。選擇 Theme default(主題默認值)以清除覆蓋並回退到激活主題自身的 fontSans

該選擇器提供了一份精選目錄(系統字體堆棧以及一組涵蓋無襯線/襯線/等寬字體的 Google Fonts 家族)。它故意接受自由文本的字體 URL——因為字體的樣式表是作為 <link> 注入的,所以該目錄保持了注入源的固定性。對於完全自定義的字體,請如上所示在主題 YAML 中設置 fontSans + fontUrl。主題的 fontMono(代碼塊、終端)始終不受 UI 覆蓋的影響。

佈局

描述
radius任意 CSS 長度("0""0.25rem""0.5rem""1rem"、...)圓角令牌。映射到 --radius 並級聯到 --radius-sm/md/lg/xl——所有圓角元素會同步變化。
densitycompact | comfortable | spacious作為 --spacing-mul CSS 變量應用的間距倍數。compact = 0.85×comfortable = 1.0×(默認),spacious = 1.2×。縮放 Tailwind 的基礎間距,因此 padding、gap 和 space-between 工具類都會按比例變化。
layout:
radius: "0"
density: compact

佈局變體

layoutVariant 選擇整體外殼佈局。缺失時默認為 "standard"

變體行為
standard單列,最大寬度 1600px(默認)。
cockpit左側邊欄軌道(260px)+ 主要內容。由插件通過 sidebar 插槽填充——參見 Shell slots。如果沒有插件,軌道將顯示佔位符。
tiled取消最大寬度限制,使頁面可以使用完整的視口寬度。
layoutVariant: cockpit

當前變體暴露為 document.documentElement.dataset.layoutVariant,因此 customCSS 中的原始 CSS 可以通過 :root[data-layout-variant="cockpit"] ... 對其進行定位。

主題資源(作為 CSS 變量的圖片)

隨主題一起提供藝術作品 URL。每個命名插槽成為一個 CSS 變量(--theme-asset-<name>),內置外殼和任何插件都可以讀取。bg 插槽自動連接到背景;其他插槽面向插件。

assets:
bg: "https://example.com/hero-bg.jpg" # auto-wired into <Backdrop />
hero: "/my-images/strike-freedom.png" # for plugin sidebars
crest: "/my-images/crest.svg" # for header-left plugins
logo: "/my-images/logo.png"
sidebar: "/my-images/rail.png"
header: "/my-images/header-art.png"
custom:
scanLines: "/my-images/scanlines.png" # → --theme-asset-custom-scanLines

值接受:

  • 純 URL——自動包裹在 url(...) 中。
  • 預包裹的 url(...)linear-gradient(...)radial-gradient(...) 表達式——原樣使用。
  • "none"——明確選擇不使用。

每個資源也會作為 --theme-asset-<name>-raw(未包裹的 URL)發出,以防插件需要將其傳遞給 <img src> 而不是 background-image

插件通過普通 CSS 或 JS 讀取這些變量:

// In a plugin slot
const hero = getComputedStyle(document.documentElement)
.getPropertyValue("--theme-asset-hero").trim();

組件樣式覆蓋

componentStyles 重新設置單個外殼組件的樣式,而無需編寫 CSS 選擇器。每個桶(bucket)中的條目成為 CSS 變量(--component-<bucket>-<kebab-property>),由外殼的共享組件讀取。因此,card: 覆蓋應用於每個 <Card>header: 應用於應用欄,等等。

componentStyles:
card:
clipPath: "polygon(12px 0, 100% 0, 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%, 0 12px)"
background: "linear-gradient(180deg, rgba(10, 22, 52, 0.85), rgba(5, 9, 26, 0.92))"
boxShadow: "inset 0 0 0 1px rgba(64, 200, 255, 0.28)"
header:
background: "linear-gradient(180deg, rgba(16, 32, 72, 0.95), rgba(5, 9, 26, 0.9))"
tab:
clipPath: "polygon(6px 0, 100% 0, calc(100% - 6px) 100%, 0 100%)"
sidebar: {}
backdrop: {}
footer: {}
progress: {}
badge: {}
page: {}

支持的桶:cardheaderfootersidebartabprogressbadgebackdroppage

屬性名使用駝峰式命名法(clipPath),並以短橫線分隔格式(clip-path)發出。值是純 CSS 字符串——CSS 接受的任何內容(clip-pathborder-imagebackgroundbox-shadowanimation、...)。

顏色覆蓋

大多數主題不需要此功能——三層調色板派生出每個 shadcn 令牌。當您需要衍生過程無法產生的特定強調色時(例如柔和色調主題的更柔和的破壞性紅色,或品牌的特定成功綠色),請使用 colorOverrides

colorOverrides:
primary: "#ffce3a"
primaryForeground: "#05091a"
accent: "#3fd3ff"
ring: "#3fd3ff"
destructive: "#ff3a5e"
border: "rgba(64, 200, 255, 0.28)"

支持的鍵:cardcardForegroundpopoverpopoverForegroundprimaryprimaryForegroundsecondarysecondaryForegroundmutedmutedForegroundaccentaccentForegrounddestructivedestructiveForegroundsuccesswarningborderinputring

每個鍵與 --color-<kebab> CSS 變量一一映射(例如 primaryForeground--color-primary-foreground)。此處設置的任何鍵僅對當前活動主題生效,並優先於調色板級聯——切換到其他主題時會清除這些覆蓋。

原始 customCSS

對於 componentStyles 無法表達的 selector 級別的界面定製——如偽元素、動畫、媒體查詢、主題作用域覆蓋——可以將原始 CSS 放入 customCSS

customCSS: |
/* Scanline overlay — only visible when cockpit variant is active. */
:root[data-layout-variant="cockpit"] body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 100;
background: repeating-linear-gradient(to bottom,
transparent 0px, transparent 2px,
rgba(64, 200, 255, 0.035) 3px, rgba(64, 200, 255, 0.035) 4px);
mix-blend-mode: screen;
}

CSS 會在應用主題時作為單個作用域 <style data-hermes-theme-css> 標籤注入,並在切換主題時清理。每個主題限制為 32 KiB。

內置主題

每個內置主題都附帶自己的調色板、排版和佈局——切換主題會產生除顏色之外的可見變化。

主題調色板排版佈局
Hermes Teal (default)深青色 + 奶油色系統字體棧,15px0.5rem 圓角,舒適間距
Hermes Teal (Large) (default-large)同 default系統字體棧,18px,行高 1.650.5rem 圓角,寬鬆間距
Midnight (midnight)深藍紫色Inter + JetBrains Mono,14px0.75rem 圓角,舒適間距
Ember (ember)暖 crimson + 青銅色Spectral (襯線) + IBM Plex Mono,15px0.25rem 圓角,舒適間距
Mono (mono)灰度IBM Plex Sans + IBM Plex Mono,13px0 圓角,緊湊間距
Cyberpunk (cyberpunk)黑色背景上的霓虹綠全局使用 Share Tech Mono,14px0 圓角,緊湊間距
Rosé (rose)粉色 + 象牙白Fraunces (襯線) + DM Mono,16px1rem 圓角,寬鬆間距

引用 Google Fonts 的主題(除 Hermes Teal 外)會按需加載樣式表——首次切換到該主題時,會將一個 <link> 標籤注入到 <head> 中。

完整主題 YAML 參考

所有配置項都在一個文件中——複製並刪除不需要的部分:

# ~/.hermes/dashboard-themes/ocean.yaml
name: ocean
label: Ocean Deep
description: Deep sea blues with coral accents

# 3-layer palette (accepts {hex, alpha} or bare hex)
palette:
background:
hex: "#0a1628"
alpha: 1.0
midground:
hex: "#a8d0ff"
alpha: 1.0
foreground:
hex: "#ffffff"
alpha: 0.0
warmGlow: "rgba(255, 107, 107, 0.35)"
noiseOpacity: 0.7

typography:
fontSans: "Poppins, system-ui, sans-serif"
fontMono: "Fira Code, ui-monospace, monospace"
fontDisplay: "Poppins, system-ui, sans-serif" # optional
fontUrl: "https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&family=Fira+Code:wght@400;500&display=swap"
baseSize: "15px"
lineHeight: "1.6"
letterSpacing: "-0.003em"

layout:
radius: "0.75rem"
density: comfortable

layoutVariant: standard # standard | cockpit | tiled

assets:
bg: "https://example.com/ocean-bg.jpg"
hero: "/my-images/kraken.png"
crest: "/my-images/anchor.svg"
logo: "/my-images/logo.png"
custom:
pattern: "/my-images/waves.svg"

componentStyles:
card:
boxShadow: "inset 0 0 0 1px rgba(168, 208, 255, 0.18)"
header:
background: "linear-gradient(180deg, rgba(10, 22, 40, 0.95), rgba(5, 9, 26, 0.9))"

colorOverrides:
destructive: "#ff6b6b"
ring: "#ff6b6b"

customCSS: |
/* Any additional selector-level tweaks */

創建文件後刷新儀表盤。從標題欄實時切換主題——點擊調色板圖標。選擇將持久化保存到 config.yaml 中的 dashboard.theme 下,並在重新加載時恢復。


插件

儀表盤插件是一個包含 manifest.json、預構建 JS bundle 的目錄,可選包含 CSS 文件和帶有 FastAPI 路由的 Python 文件。插件位於 ~/.hermes/plugins/<name>/ 中與其他 Hermes 插件並列——儀表盤擴展是該插件目錄內的 dashboard/ 子文件夾,因此一個插件可以從單次安裝中同時擴展 CLI/gateway 和儀表盤。

插件不打包 React 或 UI 組件。它們使用暴露在 window.__HERMES_PLUGIN_SDK__ 上的 Plugin SDK。這使得插件 bundle 非常小(通常只有幾 KB),並避免版本衝突。

快速開始 — 你的第一個插件

創建目錄結構:

mkdir -p ~/.hermes/plugins/my-plugin/dashboard/dist

編寫 manifest:

// ~/.hermes/plugins/my-plugin/dashboard/manifest.json
{
"name": "my-plugin",
"label": "My Plugin",
"icon": "Sparkles",
"version": "1.0.0",
"tab": {
"path": "/my-plugin",
"position": "after:skills"
},
"entry": "dist/index.js"
}

編寫 JS bundle(普通 IIFE — 無需構建步驟):

// ~/.hermes/plugins/my-plugin/dashboard/dist/index.js
(function () {
"use strict";

const SDK = window.__HERMES_PLUGIN_SDK__;
const { React } = SDK;
const { Card, CardHeader, CardTitle, CardContent } = SDK.components;

function MyPage() {
return React.createElement(Card, null,
React.createElement(CardHeader, null,
React.createElement(CardTitle, null, "My Plugin"),
),
React.createElement(CardContent, null,
React.createElement("p", { className: "text-sm text-muted-foreground" },
"Hello from my custom dashboard tab.",
),
),
);
}

window.__HERMES_PLUGINS__.register("my-plugin", MyPage);
})();

刷新儀表盤 — 你的標籤頁將出現在導航欄中,位於 Skills 之後。

跳過 React.createElement

如果你更喜歡 JSX,可以使用任何 bundler(esbuild、Vite、rollup),將 React 設為 external 並輸出 IIFE。唯一硬性要求是最終文件是一個可通過 <script> 加載的單個 JS 文件。React 從不被打包;它來自 SDK.React

目錄佈局

~/.hermes/plugins/my-plugin/
├── plugin.yaml # optional — existing CLI/gateway plugin manifest
├── __init__.py # optional — existing CLI/gateway hooks
└── dashboard/ # dashboard extension
├── manifest.json # required — tab config, icon, entry point
├── dist/
│ ├── index.js # required — pre-built JS bundle (IIFE)
│ └── style.css # optional — custom CSS
└── plugin_api.py # optional — backend API routes (FastAPI)

單個插件目錄可以包含三個正交擴展:

  • plugin.yaml + __init__.py — CLI/gateway 插件(參見插件頁面)。
  • dashboard/manifest.json + dashboard/dist/index.js — 儀表盤 UI 插件。
  • dashboard/plugin_api.py — 儀表盤後端路由。

這些都不是必需的;只包含你需要的層。

Manifest 參考

{
"name": "my-plugin",
"label": "My Plugin",
"description": "What this plugin does",
"icon": "Sparkles",
"version": "1.0.0",
"tab": {
"path": "/my-plugin",
"position": "after:skills",
"override": "/",
"hidden": false
},
"slots": ["sidebar", "header-left"],
"entry": "dist/index.js",
"css": "dist/style.css",
"api": "plugin_api.py"
}
字段必填描述
name唯一的插件標識符。小寫,允許使用連字符。用於 URL 和註冊。
label在導航標籤頁中顯示的顯示名稱。
description簡短描述(顯示在儀表板管理界面中)。
iconLucide 圖標名稱。默認為 Puzzle。未知名稱將回退到 Puzzle
versionSemver 版本字符串。默認為 0.0.0
tab.path標籤頁的 URL 路徑(例如 /my-plugin)。
tab.position插入標籤頁的位置。"end"(默認)、"after:<path>""before:<path>" — 冒號後的值是目標標籤頁的路徑段(無前導斜槓)。示例:"after:skills""before:config"
tab.override設置為內置路由路徑("/""/sessions""/config" 等)以替換該頁面,而不是添加新標籤頁。參見 替換內置頁面
tab.hidden當為 true 時,註冊組件和任何插槽,但不向導航欄添加標籤頁。由僅插槽插件使用。參見 僅插槽插件
slots此插件填充的命名 shell 插槽。僅用於文檔輔助 — 實際註冊通過 JS bundle 中的 registerSlot() 進行。在此列出插槽可使發現界面更具信息量。
entry相對於 dashboard/ 的 JS bundle 路徑。默認為 dist/index.js
css要作為 <link> 標籤注入的 CSS 文件路徑。
api包含 FastAPI 路由的 Python 文件路徑。掛載在 /api/plugins/<name>/

可用圖標

插件使用 Lucide 圖標名稱。儀表板按名稱映射這些圖標 — 未知名稱會靜默回退到 Puzzle

當前已映射:ActivityBarChart3ClockCodeDatabaseEyeFileTextGlobeHeartKeyRoundMessageSquarePackagePuzzleSettingsShieldSparklesStarTerminalWrenchZap

需要不同的圖標?請向 web/src/App.tsx 中的 ICON_MAP 提交 PR — 純增量更改。

插件 SDK

插件所需的一切都在 window.__HERMES_PLUGIN_SDK__ 上。插件絕不應直接導入 React。

const SDK = window.__HERMES_PLUGIN_SDK__;

// React + hooks
SDK.React // the React instance
SDK.hooks.useState
SDK.hooks.useEffect
SDK.hooks.useCallback
SDK.hooks.useMemo
SDK.hooks.useRef
SDK.hooks.useContext
SDK.hooks.createContext

// UI components (shadcn/ui primitives)
SDK.components.Card
SDK.components.CardHeader
SDK.components.CardTitle
SDK.components.CardContent
SDK.components.Badge
SDK.components.Button
SDK.components.Input
SDK.components.Label
SDK.components.Select
SDK.components.SelectOption
SDK.components.Separator
SDK.components.Tabs
SDK.components.TabsList
SDK.components.TabsTrigger
SDK.components.PluginSlot // render a named slot (useful for nested plugin UIs)

// Hermes API client + raw fetcher
SDK.api // typed client — getStatus, getSessions, getConfig, ...
SDK.fetchJSON // raw fetch for custom endpoints (plugin-registered routes)

// Utilities
SDK.utils.cn // Tailwind class merger (clsx + twMerge)
SDK.utils.timeAgo // "5m ago" from unix timestamp
SDK.utils.isoTimeAgo // "5m ago" from ISO string

// Hooks
SDK.useI18n // i18n hook for multi-language plugins

調用插件的後端

SDK.fetchJSON("/api/plugins/my-plugin/data")
.then((data) => console.log(data))
.catch((err) => console.error("API call failed:", err));

fetchJSON 注入會話認證令牌,將錯誤作為拋出的異常呈現,並自動解析 JSON。

調用內置 Hermes 端點

// Agent status
SDK.api.getStatus().then((s) => console.log("Version:", s.version));

// Recent sessions
SDK.api.getSessions(10).then((resp) => console.log(resp.sessions.length));

完整列表參見 Web 儀表板 → REST API

Shell 插槽

插槽允許插件將組件注入應用 shell 的命名位置 — 駕駛艙側邊欄、頁眉、頁腳、覆蓋層 — 而無需佔用整個標籤頁。多個插件可以填充同一個插槽;它們按註冊順序堆疊渲染。

在插件 bundle 內部註冊:

window.__HERMES_PLUGINS__.registerSlot("my-plugin", "sidebar", MySidebar);
window.__HERMES_PLUGINS__.registerSlot("my-plugin", "header-left", MyCrest);

插槽目錄

Shell 全局插槽(在應用框架的任何位置渲染):

插槽位置
backdrop<Backdrop /> 層棧內,位於噪聲層之上。
header-left在頂部欄中 Hermes 品牌標識之前。
header-right在頂部欄中主題/語言切換器之前。
header-banner導航欄下方的全寬條帶。
sidebar駕駛艙側邊欄軌道 — 僅在 layoutVariant === "cockpit" 時渲染
pre-main在路由出口上方(在 <main> 內)。
post-main在路由出口下方(在 <main> 內)。
footer-left頁腳單元格內容(替換默認值)。
footer-right頁腳單元格內容(替換默認值)。
overlay固定定位層,位於所有其他內容之上。適用於單獨使用 customCSS 無法實現的 chrome 效果(掃描線、暗角等)。

頁面範圍插槽(僅在指定的內置頁面上渲染 — 使用這些插槽將小部件、卡片或工具欄注入現有頁面,而無需覆蓋整個路由):

插槽渲染位置
sessions:top / sessions:bottom/sessions 頁面的頂部/底部。
analytics:top / analytics:bottom/analytics 頁面的頂部/底部。
logs:top / logs:bottom/logs 的頂部(過濾器工具欄上方)/底部(日誌查看器下方)。
cron:top / cron:bottom/cron 頁面的頂部/底部。
skills:top / skills:bottom/skills 頁面的頂部/底部。
config:top / config:bottom/config 頁面的頂部/底部。
env:top / env:bottom/env(密鑰)頁面的頂部/底部。
docs:top / docs:bottom/docs 的頂部(iframe 上方)/底部。
chat:top / chat:bottom/chat 的頂部/底部(僅在啟用嵌入式聊天時激活)。

示例 — 在 Sessions 頁面頂部添加橫幅卡片:

function PinnedSessionsBanner() {
return React.createElement(Card, null,
React.createElement(CardContent, { className: "py-2 text-xs" },
"Pinned note injected by my-plugin"),
);
}

window.__HERMES_PLUGINS__.registerSlot("my-plugin", "sessions:top", PinnedSessionsBanner);

如果您的插件僅增強現有頁面且不需要自己的側邊欄標籤頁,請將頁面範圍插槽與 tab.hidden: true 結合使用。

Shell 僅為上述插槽渲染 <PluginSlot name="..." />。註冊表接受其他名稱用於嵌套插件 UI — 插件可以通過 SDK.components.PluginSlot 暴露其自己的插槽。

重新註冊與 HMR

如果同一個 (plugin, slot) 對被註冊了兩次,後一次調用將替換前一次——這符合 React HMR 對插件重新掛載行為的預期。

替換內置頁面(tab.override

tab.override 設置為內置路由路徑,會使插件的組件替換該頁面,而不是添加新標籤頁。當主題想要自定義主頁(/)但希望保持儀表板其餘部分完整時,此功能非常有用。

{
"name": "my-home",
"label": "Home",
"tab": {
"path": "/my-home",
"override": "/",
"position": "end"
},
"entry": "dist/index.js"
}

設置 override 後:

  • 路由器中位於 / 的原始頁面組件將被移除。
  • 你的插件將在 / 處渲染。
  • 不會為 tab.path 添加導航標籤頁(這正是覆蓋的目的)。

只有一個插件可以覆蓋給定路徑。如果有兩個插件聲稱覆蓋同一路徑,第一個插件勝出,第二個插件將被忽略並在開發模式下發出警告。

如果你只需要向現有頁面添加卡片或工具欄而不接管整個頁面,請改用頁面作用域插槽

增強內置頁面(頁面作用域插槽)

通過 tab.override 進行完全替換是一種重型操作——你的插件現在擁有整個頁面,包括我們未來對該頁面發佈的任何更新。大多數情況下,你只是想在現有頁面中添加橫幅、卡片或工具欄。這就是頁面作用域插槽的用途。

每個內置頁面都暴露 <page>:top<page>:bottom 插槽,分別在其內容區域的頂部和底部渲染。你的插件通過調用 registerSlot() 來填充其中一個插槽——內置頁面保持正常工作,而你的組件與其並列渲染。

可用插槽:sessions:*analytics:*logs:*cron:*skills:*config:*env:*docs:*chat:*(每個都有 :top:bottom)。請參閱 Shell 插槽 → 插槽目錄 中的完整目錄。

最小示例——在 Sessions 頁面頂部固定一個橫幅:

// ~/.hermes/plugins/session-notes/dashboard/manifest.json
{
"name": "session-notes",
"label": "Session Notes",
"tab": { "path": "/session-notes", "hidden": true },
"slots": ["sessions:top"],
"entry": "dist/index.js"
}
// ~/.hermes/plugins/session-notes/dashboard/dist/index.js
(function () {
const SDK = window.__HERMES_PLUGIN_SDK__;
const { React } = SDK;
const { Card, CardContent } = SDK.components;

function Banner() {
return React.createElement(Card, null,
React.createElement(CardContent, { className: "py-2 text-xs" },
"Remember to label important sessions before archiving."),
);
}

// Placeholder for the hidden tab.
window.__HERMES_PLUGINS__.register("session-notes", function () { return null; });

// The real work.
window.__HERMES_PLUGINS__.registerSlot("session-notes", "sessions:top", Banner);
})();

關鍵點:

  • tab.hidden: true 使插件不出現在側邊欄中——它沒有獨立的頁面。
  • slots 清單字段僅用於文檔說明。實際綁定是通過 JS bundle 中的 registerSlot() 發生的。
  • 多個插件可以聲明同一個頁面作用域插槽。它們按註冊順序堆疊渲染。
  • 當沒有插件註冊時零開銷:內置頁面完全按原樣渲染。

參考插件(hermes-example-plugins 中的 example-dashboard)提供了一個實時演示,將橫幅注入到 sessions:top 中——安裝它以端到端地查看此模式。

僅插槽插件(tab.hidden

tab.hidden: true 時,插件會註冊其組件(用於直接 URL 訪問)和任何插槽,但永遠不會嚮導航添加標籤頁。這適用於僅存在於向插槽注入內容的插件——例如標題徽章、側邊欄 HUD 或覆蓋層。

{
"name": "header-crest",
"label": "Header Crest",
"tab": {
"path": "/header-crest",
"position": "end",
"hidden": true
},
"slots": ["header-left"],
"entry": "dist/index.js"
}

Bundle 仍然使用佔位符組件調用 register()(以防有人直接訪問 URL,這是一種良好實踐),然後調用 registerSlot() 執行實際工作。

後端 API 路由

插件可以通過在清單中設置 api 來註冊 FastAPI 路由。創建文件並導出一個 router

# ~/.hermes/plugins/my-plugin/dashboard/plugin_api.py
from fastapi import APIRouter

router = APIRouter()

@router.get("/data")
async def get_data():
return {"items": ["one", "two", "three"]}

@router.post("/action")
async def do_action(body: dict):
return {"ok": True, "received": body}

路由掛載在 /api/plugins/<name>/ 下,因此上述示例變為:

  • GET /api/plugins/my-plugin/data
  • POST /api/plugins/my-plugin/action

插件 API 路由繞過會話令牌身份驗證,因為儀表板服務器默認綁定到 localhost。如果你運行不受信任的插件,請勿使用 --host 0.0.0.0 在公共接口上暴露儀表板——它們的路由也將變得可訪問。

訪問 Hermes 內部結構

後端路由在儀表板進程中運行,因此它們可以直接從 hermes-agent 代碼庫導入:

from fastapi import APIRouter
from hermes_state import SessionDB
from hermes_cli.config import load_config

router = APIRouter()

@router.get("/session-count")
async def session_count():
db = SessionDB()
try:
count = len(db.list_sessions(limit=9999))
return {"count": count}
finally:
db.close()

@router.get("/config-snapshot")
async def config_snapshot():
cfg = load_config()
return {"model": cfg.get("model", {})}

每個插件的自定義 CSS

如果你的插件需要超出 Tailwind 類和內聯 style= 的樣式,請添加一個 CSS 文件並在清單中引用它:

{
"css": "dist/style.css"
}

該文件在插件加載時作為 <link> 標籤注入。使用特定的類名以避免與儀表板樣式衝突,並引用儀表板的 CSS 變量以保持主題感知能力:

/* dist/style.css */
.my-plugin-chart {
border: 1px solid var(--color-border);
background: var(--color-card);
color: var(--color-card-foreground);
padding: 1rem;
}
.my-plugin-chart:hover {
border-color: var(--color-ring);
}

儀表板將每個 shadcn token 暴露為 --color-* 以及主題額外變量(--theme-asset-*--component-<bucket>-*--radius--spacing-mul)。引用這些變量,你的插件會自動隨活動主題重新換膚。

插件發現與重載

儀表板掃描以下三個目錄以查找 dashboard/manifest.json

優先級目錄來源標籤
1(衝突時勝出)~/.hermes/plugins/<name>/dashboard/user
2<repo>/plugins/memory/<name>/dashboard/bundled
2<repo>/plugins/<name>/dashboard/bundled
3./.hermes/plugins/<name>/dashboard/project — 僅在設置 HERMES_ENABLE_PROJECT_PLUGINS 時有效

發現結果在每個儀表板進程中被緩存。添加新插件後,要麼:

# Force a rescan without restart
curl http://127.0.0.1:9119/api/dashboard/plugins/rescan

……要麼重啟 hermes dashboard

插件加載生命週期

  1. 儀表盤加載。main.tsx 將 SDK 暴露在 window.__HERMES_PLUGIN_SDK__ 上,並將註冊表暴露在 window.__HERMES_PLUGINS__ 上。
  2. App.tsx 調用 usePlugins() → 獲取 GET /api/dashboard/plugins
  3. 對於每個清單:注入 CSS <link>(如果已聲明),然後加載 JS bundle 的 <script> 標籤。
  4. 插件的 IIFE 運行並調用 window.__HERMES_PLUGINS__.register(name, Component) —— 以及可選地為每個插槽調用 .registerSlot(name, slot, Component)
  5. 儀表盤根據清單解析已註冊的組件,將選項卡添加到導航中(除非設置為 hidden),並將組件作為路由掛載。

插件在其腳本加載後有最多 2 秒 的時間來調用 register()。此後,儀表盤將停止等待並完成初始渲染。如果插件稍後註冊,它仍然會出現 —— 導航是響應式的。

如果插件的腳本加載失敗(404、語法錯誤、IIFE 期間異常),儀表盤會在瀏覽器控制檯中記錄警告並繼續運行而不加載該插件。


組合主題 + 插件演示

strike-freedom-cockpit 插件(配套倉庫 hermes-example-plugins)是一個完整的換膚演示。它將主題 YAML 與僅插槽插件配對,無需分叉儀表盤即可生成駕駛艙風格的 HUD。

演示內容:

  • 一個完整主題,使用調色板、排版、fontUrllayoutVariant: cockpitassetscomponentStyles(缺角卡片圓角、漸變背景)、colorOverridescustomCSS(掃描線疊加層)。
  • 一個僅插槽插件(tab.hidden: true),註冊到三個插槽中:
    • sidebar —— 一個 MS-STATUS 面板,帶有由 SDK.api.getStatus() 驅動的實時遙測條。
    • header-left —— 一個陣營徽章,從活動主題中讀取 --theme-asset-crest
    • footer-right —— 替換默認組織行的自定義標語。
  • 插件通過 CSS 變量讀取主題提供的藝術作品,因此切換主題會改變英雄圖/徽章,而無需更改插件代碼。

安裝:

git clone https://github.com/NousResearch/hermes-example-plugins.git

# Theme
cp hermes-example-plugins/strike-freedom-cockpit/theme/strike-freedom.yaml \
~/.hermes/dashboard-themes/

# Plugin
cp -r hermes-example-plugins/strike-freedom-cockpit ~/.hermes/plugins/

打開儀表盤,從主題切換器中選擇 Strike Freedom。駕駛艙側邊欄出現,徽章顯示在標題中,標語替換了頁腳。切換回 Hermes Teal,插件保持安裝狀態但不可見(sidebar 插槽僅在 cockpit 佈局變體下渲染)。

閱讀插件源代碼(配套倉庫中的 strike-freedom-cockpit/dashboard/dist/index.js),瞭解它如何讀取 CSS 變量、針對不支持插槽的舊版儀表盤進行防護,以及如何從一個 bundle 中註冊三個插槽。


API 參考

主題端點

端點方法描述
/api/dashboard/themesGET列出可用主題 + 活動名稱。內置主題返回 {name, label, description};用戶主題還包括一個 definition 字段,包含完整的規範化主題對象。
/api/dashboard/themePUT設置活動主題。請求體:{"name": "midnight"}。持久化到 config.yaml 下的 dashboard.theme

插件端點

端點方法描述
/api/dashboard/pluginsGET列出已發現的插件(包含清單,減去內部字段)。
/api/dashboard/plugins/rescanGET強制重新掃描插件目錄,無需重啟。
/dashboard-plugins/<name>/<path>GET提供插件 dashboard/ 目錄中的靜態資源。阻止路徑遍歷。
/api/plugins/<name>/**插件註冊的後端路由。

window 上的 SDK

全局變量類型提供者
window.__HERMES_PLUGIN_SDK__objectregistry.ts — React、hooks、UI 組件、API 客戶端、工具函數。
window.__HERMES_PLUGINS__.register(name, Component)function註冊插件的主組件。
window.__HERMES_PLUGINS__.registerSlot(name, slot, Component)function註冊到命名的 shell 插槽中。

故障排除

我的主題未出現在選擇器中。 檢查文件是否位於 ~/.hermes/dashboard-themes/ 中並以 .yaml.yml 結尾。刷新頁面。運行 curl http://127.0.0.1:9119/api/dashboard/themes —— 你的主題應出現在響應中。如果 YAML 存在解析錯誤,儀表盤會將日誌記錄到 ~/.hermes/logs/ 下的 errors.log 中。

我的插件選項卡未顯示。

  1. 檢查清單是否位於 ~/.hermes/plugins/<name>/dashboard/manifest.json(注意 dashboard/ 子目錄)。
  2. 執行 curl http://127.0.0.1:9119/api/dashboard/plugins/rescan 以強制重新發現。
  3. 打開瀏覽器開發者工具 → Network(網絡)—— 確認 manifest.jsonindex.js 和任何 CSS 加載時沒有 404 錯誤。
  4. 打開瀏覽器開發者工具 → Console(控制檯)—— 查找 IIFE 期間的錯誤或 window.__HERMES_PLUGINS__ is undefined(表明 SDK 未初始化,通常是早期的 React 渲染崩潰所致)。
  5. 驗證你的 bundle 是否調用了 window.__HERMES_PLUGINS__.register(...),且使用的名稱與 manifest.json:name 完全相同

插槽註冊的組件未渲染。 sidebar 插槽僅在當前激活的主題具有 layoutVariant: cockpit 時才會渲染。其他插槽始終會渲染。如果你向一個沒有匹配項的插槽註冊,請在 registerSlot 內部添加 console.log 以確認插件包確實已運行。

插件後端路由返回 404。

  1. 確認清單文件中包含 "api": "plugin_api.py",且該路徑指向 dashboard/ 內存在的一個文件。
  2. 重啟 hermes dashboard —— 插件 API 路由僅在啟動時掛載一次,不會在重新掃描時掛載。
  3. 檢查 plugin_api.py 是否導出了模塊級別的 router = APIRouter()。其他導出名稱不會被識別。
  4. 跟蹤查看 ~/.hermes/logs/errors.log 中是否有 Failed to load plugin <name> API routes 錯誤 —— 導入錯誤會記錄在此處。

切換主題導致我的顏色覆蓋失效。 colorOverrides 的作用域限定於當前激活的主題,並在切換主題時被清除 —— 這是設計使然。如果你希望覆蓋設置持久生效,請將它們放在主題的 YAML 文件中,而不是實時切換器中。

主題的 customCSS 被截斷。 每個主題的 customCSS 塊上限為 32 KiB。可以將大型樣式表拆分到多個主題中,或者切換到通過其 css 字段注入完整樣式表的插件(無大小限制)。

我想在 PyPI 上發佈插件。 Dashboard 插件是通過目錄結構安裝的,而非通過 pip 入口點。目前最乾淨的分發路徑是讓用戶將 Git 倉庫克隆到 ~/.hermes/plugins/ 中。目前尚未配置基於 pip 的 Dashboard 插件安裝程序。