擴展儀表盤
Hermes Web 儀表盤(hermes dashboard)旨在無需分叉代碼庫即可重新換膚和擴展。它暴露了三個層級:
- 主題 (Themes) — YAML 文件,用於重繪儀表盤的調色板、排版、佈局以及各組件的裝飾樣式。將文件放入
~/.hermes/dashboard-themes/;它將出現在主題切換器中。 - UI 插件 (UI plugins) — 一個包含
manifest.json和 JavaScript 捆綁包的目錄,用於註冊標籤頁、替換內置頁面、通過頁面作用域插槽增強頁面,或將組件注入命名的 Shell 插槽中。 - 後端插件 (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.warmGlow | rgba(...) 字符串,用作 <Backdrop /> 的暈影顏色。 |
palette.noiseOpacity | 0–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
排版
| 鍵 | 類型 | 描述 |
|---|---|---|
fontSans | string | 正文內容的 CSS font-family 堆棧(應用於 html, body)。 |
fontMono | string | 代碼塊、<code>、.font-mono 工具類的 CSS font-family 堆棧。 |
fontDisplay | string | 可選的標題/展示用字體堆棧。回退至 fontSans。 |
fontUrl | string | 可選的外部樣式表 URL。在切換主題時,作為 <link rel="stylesheet"> 注入到 <head> 中。同一 URL 永遠不會被注入兩次。適用於 Google Fonts、Bunny Fonts、自託管的 @font-face 樣式表——任何可鏈接的資源。 |
baseSize | string | 根字體大小——控制 rem 比例。例如 "14px"、"16px"。 |
lineHeight | string | 默認行高。例如 "1.5"、"1.65"。 |
letterSpacing | string | 默認字間距。例如 "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.yaml 的 dashboard.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——所有圓角元素會同步變化。 |
density | compact | 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: {}
支持的桶:card、header、footer、sidebar、tab、progress、badge、backdrop、page。
屬性名使用駝峰式命名法(clipPath),並以短橫線分隔格式(clip-path)發出。值是純 CSS 字符串——CSS 接受的任何內容(clip-path、border-image、background、box-shadow、animation、...)。
顏色覆蓋
大多數主題不需要此功能——三層調色板派生出每個 shadcn 令牌。當您需要衍生過程無法產生的特定強調色時(例如柔和色調主題的更柔和的破壞性紅色,或品牌的特定成功綠色),請使用 colorOverrides。
colorOverrides:
primary: "#ffce3a"
primaryForeground: "#05091a"
accent: "#3fd3ff"
ring: "#3fd3ff"
destructive: "#ff3a5e"
border: "rgba(64, 200, 255, 0.28)"
支持的鍵:card、cardForeground、popover、popoverForeground、primary、primaryForeground、secondary、secondaryForeground、muted、mutedForeground、accent、accentForeground、destructive、destructiveForeground、success、warning、border、input、ring。
每個鍵與 --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) | 深青色 + 奶油色 | 系統字體棧,15px | 0.5rem 圓角,舒適間距 |
Hermes Teal (Large) (default-large) | 同 default | 系統字體棧,18px,行高 1.65 | 0.5rem 圓角,寬鬆間距 |
Midnight (midnight) | 深藍紫色 | Inter + JetBrains Mono,14px | 0.75rem 圓角,舒適間距 |
Ember (ember) | 暖 crimson + 青銅色 | Spectral (襯線) + IBM Plex Mono,15px | 0.25rem 圓角,舒適間距 |
Mono (mono) | 灰度 | IBM Plex Sans + IBM Plex Mono,13px | 0 圓角,緊湊間距 |
Cyberpunk (cyberpunk) | 黑色背景上的霓虹綠 | 全局使用 Share Tech Mono,14px | 0 圓角,緊湊間距 |
Rosé (rose) | 粉色 + 象牙白 | Fraunces (襯線) + DM Mono,16px | 1rem 圓角,寬鬆間距 |
引用 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 之後。
如果你更喜歡 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 | 否 | 簡短描述(顯示在儀表板管理界面中)。 |
icon | 否 | Lucide 圖標名稱。默認為 Puzzle。未知名稱將回退到 Puzzle。 |
version | 否 | Semver 版本字符串。默認為 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。
當前已映射:Activity、BarChart3、Clock、Code、Database、Eye、FileText、Globe、Heart、KeyRound、MessageSquare、Package、Puzzle、Settings、Shield、Sparkles、Star、Terminal、Wrench、Zap。
需要不同的圖標?請向 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/dataPOST /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。
插件加載生命週期
- 儀表盤加載。
main.tsx將 SDK 暴露在window.__HERMES_PLUGIN_SDK__上,並將註冊表暴露在window.__HERMES_PLUGINS__上。 App.tsx調用usePlugins()→ 獲取GET /api/dashboard/plugins。- 對於每個清單:注入 CSS
<link>(如果已聲明),然後加載 JS bundle 的<script>標籤。 - 插件的 IIFE 運行並調用
window.__HERMES_PLUGINS__.register(name, Component)—— 以及可選地為每個插槽調用.registerSlot(name, slot, Component)。 - 儀表盤根據清單解析已註冊的組件,將選項卡添加到導航中(除非設置為
hidden),並將組件作為路由掛載。
插件在其腳本加載後有最多 2 秒 的時間來調用 register()。此後,儀表盤將停止等待並完成初始渲染。如果插件稍後註冊,它仍然會出現 —— 導航是響應式的。
如果插件的腳本加載失敗(404、語法錯誤、IIFE 期間異常),儀表盤會在瀏覽器控制檯中記錄警告並繼續運行而不加載該插件。
組合主題 + 插件演示
strike-freedom-cockpit 插件(配套倉庫 hermes-example-plugins)是一個完整的換膚演示。它將主題 YAML 與僅插槽插件配對,無需分叉儀表盤即可生成駕駛艙風格的 HUD。
演示內容:
- 一個完整主題,使用調色板、排版、
fontUrl、layoutVariant: cockpit、assets、componentStyles(缺角卡片圓角、漸變背景)、colorOverrides和customCSS(掃描線疊加層)。 - 一個僅插槽插件(
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/themes | GET | 列出可用主題 + 活動名稱。內置主題返回 {name, label, description};用戶主題還包括一個 definition 字段,包含完整的規範化主題對象。 |
/api/dashboard/theme | PUT | 設置活動主題。請求體:{"name": "midnight"}。持久化到 config.yaml 下的 dashboard.theme。 |
插件端點
| 端點 | 方法 | 描述 |
|---|---|---|
/api/dashboard/plugins | GET | 列出已發現的插件(包含清單,減去內部字段)。 |
/api/dashboard/plugins/rescan | GET | 強制重新掃描插件目錄,無需重啟。 |
/dashboard-plugins/<name>/<path> | GET | 提供插件 dashboard/ 目錄中的靜態資源。阻止路徑遍歷。 |
/api/plugins/<name>/* | * | 插件註冊的後端路由。 |
window 上的 SDK
| 全局變量 | 類型 | 提供者 |
|---|---|---|
window.__HERMES_PLUGIN_SDK__ | object | registry.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 中。
我的插件選項卡未顯示。
- 檢查清單是否位於
~/.hermes/plugins/<name>/dashboard/manifest.json(注意dashboard/子目錄)。 - 執行
curl http://127.0.0.1:9119/api/dashboard/plugins/rescan以強制重新發現。 - 打開瀏覽器開發者工具 → Network(網絡)—— 確認
manifest.json、index.js和任何 CSS 加載時沒有 404 錯誤。 - 打開瀏覽器開發者工具 → Console(控制檯)—— 查找 IIFE 期間的錯誤或
window.__HERMES_PLUGINS__ is undefined(表明 SDK 未初始化,通常是早期的 React 渲染崩潰所致)。 - 驗證你的 bundle 是否調用了
window.__HERMES_PLUGINS__.register(...),且使用的名稱與manifest.json:name完全相同。
插槽註冊的組件未渲染。
sidebar 插槽僅在當前激活的主題具有 layoutVariant: cockpit 時才會渲染。其他插槽始終會渲染。如果你向一個沒有匹配項的插槽註冊,請在 registerSlot 內部添加 console.log 以確認插件包確實已運行。
插件後端路由返回 404。
- 確認清單文件中包含
"api": "plugin_api.py",且該路徑指向dashboard/內存在的一個文件。 - 重啟
hermes dashboard—— 插件 API 路由僅在啟動時掛載一次,不會在重新掃描時掛載。 - 檢查
plugin_api.py是否導出了模塊級別的router = APIRouter()。其他導出名稱不會被識別。 - 跟蹤查看
~/.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 插件安裝程序。