章節:核心功能 · 網址:https://hermesbible.com/docs/user-guide/features/extending-the-dashboard
Hermes 網頁控制台(hermes dashboard)設計為可在不分叉原始碼的情況下進行換肤與擴充。共暴露三個層級:
- 主題(Themes) — YAML 檔案,用於重新描繪控制台的色盤、字體排印、佈局及各元件的外觀。將檔案放入
~/.hermes/dashboard-themes/即可出現在主題選擇器中。 - UI 外掛(UI plugins) — 包含
manifest.json與 JavaScript bundle 的目錄,可註冊分頁、替換內建頁面、透過頁面範圍插槽(page-scoped slots)增強頁面,或將元件注入至指定的 shell 插槽。 - 後端外掛(Backend plugins) — 外掛目錄中的 Python 檔案,暴露 FastAPI
router;路由掛載於/api/plugins/<name>/下,並從外掛的 UI 端呼叫。
以上三者皆為執行時直接放入:不需要複製 repo、不需要 npm run build、不需要修改控制台原始碼。本頁為三者的完整參考文件。
若只想使用控制台,請參閱 Web Dashboard。若想重新設計終端 CLI 的外觀(而非網頁控制台),請參閱 Skins & Themes — CLI 皮膚系統與控制台主題無關。
注意 — 組件如何組合
主題與外掛彼此獨立但可互相搭配。主題可以獨立運作(僅需一個 YAML 檔案)。外掛也可以獨立運作(僅需一個分頁)。兩者結合時可打造完整的視覺換肤效果搭配自訂 HUD — 範例
strike-freedom-cockpitdemo(位於hermes-example-plugins附屬 repo — 安裝步驟請參閱組合式主題 + 外掛 demo)即為此用法的完整示範。
目錄
Themes
主題是儲存在 ~/.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() 從這個雙色組合重新計算。
這就是完整的入門流程:一個檔案,兩個顏色。以下所有內容都是可選的進階調整。
Palette, typography, layout
這三個區塊是主題的核心。各自獨立 — 可以只覆寫其中一個,其餘保持預設。
Palette(三層色盤)
色盤由三個顏色層加上暖色光暈(warmGlow)與雜訊紋理乘數組成。控制台的設計系統會從這三個顏色透過 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} 格式或純 hex 字串(alpha 預設為 1.0)。
palette:
background:
hex: "#05091a"
alpha: 1.0
midground: "#d8f0ff" # 純 hex,alpha = 1.0
foreground:
hex: "#ffffff"
alpha: 0 # 不可見的頂層
warmGlow: "rgba(255, 199, 55, 0.24)"
noiseOpacity: 07
Typography
| 鍵值 | 類型 | 說明 |
|---|---|---|
fontSans | string | 內文的 CSS font-family 堆疊(套用至 html、body)。 |
fontMono | string | 程式碼區塊、<code>、.font-mono 工具類別的 CSS font-family 堆疊。 |
fontDisplay | string | 選填的標題/展示字體堆疊。預設回退至 fontSans。 |
fontUrl | string | 選填的外部樣式表網址。主題切換時以 <link rel="stylesheet"> 注入 <head>。相同網址不會重複注入。適用於 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。
選擇器提供精選目錄(系統字體堆疊加上一組涵蓋 sans / serif / mono 的 Google Fonts 字族)。它不接受自由輸入的字體網址 — 因為字體樣式表以 <link> 注入,目錄需固定注入來源。若需完全自訂字體,請在主題 YAML 中設定 fontSans + fontUrl,如上述所示。主題的 fontMono(程式碼區塊、終端)永遠不會被 UI 覆寫影響。
Layout
| 鍵值 | 值 | 說明 |
|---|---|---|
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
Layout variants
layoutVariant 決定整體 shell 佈局。未設定時預設為 "standard"。
| 變體 | 行為 |
|---|---|
standard | 單欄,最大寬度 1600px(預設)。 |
cockpit | 左側側邊欄導軌(260px)+ 主要內容區。由外掛透過 sidebar 插槽填充 — 請參閱 Shell 插槽。無外掛時導軌顯示佔位符。 |
tiled | 取消最大寬度限制,頁面可使用完整視窗寬度。 |
layoutVariant: cockpit
目前的變體暴露為 document.documentElement.dataset.layoutVariant,因此 customCSS 中的原始 CSS 可透過 :root[data-layout-variant="cockpit"] ... 來針對它。
Theme assets(圖片作為 CSS 變數)
可隨主題提供素材圖片網址。每個命名插槽都會成為一個 CSS 變數(--theme-asset-<name>),供內建 shell 與任何外掛讀取。bg 插槽會自動連接至 backdrop;其餘插槽供外掛使用。
assets:
bg: "https://example.com/hero-bg.jpg" # 自動連接至 <Backdrop />
hero: "/my-images/strike-freedom.png" # 用於外掛側邊欄
crest: "/my-images/crest.svg" # 用於頂部列左側外掛
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(...)、linear-gradient(...)、radial-gradient(...)表達式 — 原樣使用。 "none"— 顯式排除。
每個素材也會以 --theme-asset-<name>-raw(未包裹的網址)形式輸出,供外掛傳入 <img src> 而非 background-image 時使用。
外掛透過純 CSS 或 JS 讀取:
// 在外掛插槽中
const hero = getComputedStyle(document.documentElement)
.getPropertyValue("--theme-asset-hero").trim();
Component chrome overrides
componentStyles 可在不撰寫 CSS 選擇器的情況下重新樣式化個別 shell 元件。每個桶(bucket)的條目會成為 CSS 變數(--component-<bucket>-<kebab-property>),供 shell 的共享元件讀取。因此 card: 覆寫會套用至所有 <Card>、header: 套用至 app bar,依此類推。
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。
屬性名稱使用 camelCase(clipPath),輸出為 kebab-case(clip-path)。值為純 CSS 字串 — 任何 CSS 接受的值皆可(clip-path、border-image、background、box-shadow、animation、...)。
Color overrides
大多數主題不需要此功能 — 三層色盤會衍生出所有 shadcn 色票。當你需要特定的強調色而衍生無法產生時(例如粉彩主題用的柔和 destructive 紅色、品牌指定的 success 綠色),可使用 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 無法表達的選擇器層級外觀調整 — 偽元素、動畫、媒體查詢、主題範圍覆寫 — 可將原始 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。
Built-in themes
每個內建主題都有自己的色盤、字體排印與佈局 — 切換時除了顏色之外還會產生明顯的視覺變化。
| 主題 | 色盤 | 字體排印 | 佈局 |
|---|---|---|---|
Hermes Teal(default) | 深青色 + 奶油色 | 系統字體,15px | 0.5rem 圓角,comfortable |
Hermes Teal (Large)(default-large) | 同預設 | 系統字體,18px,行高 1.65 | 0.5rem 圓角,spacious |
Midnight(midnight) | 深藍紫色 | Inter + JetBrains Mono,14px | 0.75rem 圓角,comfortable |
Ember(ember) | 暖 crimson + 青銅色 | Spectral(serif)+ IBM Plex Mono,15px | 0.25rem 圓角,comfortable |
Mono(mono) | 灰階 | IBM Plex Sans + IBM Plex Mono,13px | 0 圓角,compact |
Cyberpunk(cyberpunk) | 黑底霓虹綠 | 全域 Share Tech Mono,14px | 0 圓角,compact |
Rosé(rose) | 粉紅色 + 象牙色 | Fraunces(serif)+ DM Mono,16px | 1rem 圓角,spacious |
引用 Google Fonts 的主題(除 Hermes Teal 外)會按需載入樣式表 — 首次切換時會在 <head> 中注入 <link> 標籤。
Full theme YAML reference
所有設定項集中於一個檔案 — 複製並裁剪你需要的部分:
# ~/.hermes/dashboard-themes/ocean.yaml
name: ocean
label: Ocean Deep
description: Deep sea blues with coral accents
# 三層色盤(接受 {hex, alpha} 或純 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" # 選填
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 下,並在重新載入時恢復。
Plugins
控制台外掛是一個包含 manifest.json、預編譯 JS bundle,以及可選 CSS 檔案和含 FastAPI 路由的 Python 檔案的目錄。外掛與其他 Hermes 外掛一起位於 ~/.hermes/plugins/<name>/ — 控台擴充功能是該外掛目錄中的 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,可使用任何打包工具(esbuild、Vite、rollup),將 React 設為 external 並以 IIFE 格式輸出。唯一硬性要求是最終檔案為可透過
<script>載入的單一 JS 檔案。React 永遠不會被打包;它來自SDK.React。
目錄結構
~/.hermes/plugins/my-plugin/
├── plugin.yaml # 選填 — 現有 CLI/gateway 外掛 manifest
├── __init__.py # 選填 — 現有 CLI/gateway hooks
└── dashboard/ # 控制台擴充
├── manifest.json # 必要 — 分頁設定、圖示、進入點
├── dist/
│ ├── index.js # 必要 — 預編譯 JS bundle(IIFE)
│ └── style.css # 選填 — 自訂 CSS
└── plugin_api.py # 選填 — 後端 API 路由(FastAPI)
單一外掛目錄可承載三個正交的擴充功能:
plugin.yaml+__init__.py— CLI/gateway 外掛(請參閱 plugins 頁面)。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 | 是 | 唯一的外掛識別碼。小寫,可用連字號。用於網址與註冊。 |
label | 是 | 導航分頁上顯示的名稱。 |
description | 否 | 簡短說明(顯示於控制台管理介面)。 |
icon | 否 | Lucide 圖示名稱。預設為 Puzzle。未知名稱回退至 Puzzle。 |
version | 否 | Semver 字串。預設為 0.0.0。 |
tab.path | 是 | 分頁的網址路徑(例如 /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 | 是 | JS bundle 相對 dashboard/ 的路徑。預設為 dist/index.js。 |
css | 否 | CSS 檔案相對路徑,會以 <link> 標籤注入。 |
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 — 純新增變更。
Plugin SDK
外掛所需的一切都在 window.__HERMES_PLUGIN_SDK__ 上。外掛永遠不應直接 import React。
const SDK = window.__HERMES_PLUGIN_SDK__;
// React + hooks
SDK.React // React 實例
SDK.hooks.useState
SDK.hooks.useEffect
SDK.hooks.useCallback
SDK.hooks.useMemo
SDK.hooks.useRef
SDK.hooks.useContext
SDK.hooks.createContext
// UI 元件(shadcn/ui 原語)
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 // 渲染命名插槽(適用於巢狀外掛 UI)
// Hermes API 用戶端 + 原始 fetcher
SDK.api // 型別化用戶端 — getStatus、getSessions、getConfig、...
SDK.fetchJSON // 自訂端點的原始 fetch(外掛註冊的路由)
// 工具函式
SDK.utils.cn // Tailwind class 合併器(clsx + twMerge)
SDK.utils.timeAgo // 從 unix 時間戳產生 "5m ago"
SDK.utils.isoTimeAgo // 從 ISO 字串產生 "5m ago"
// Hooks
SDK.useI18n // 多語系外掛的 i18n hook
呼叫外掛的後端
SDK.fetchJSON("/api/plugins/my-plugin/data")
.then((data) => console.log(data))
.catch((err) => console.error("API call failed:", err));
fetchJSON 會注入 session auth token,以拋出例外的方式呈現錯誤,並自動解析 JSON。
呼叫內建 Hermes 端點
// Agent 狀態
SDK.api.getStatus().then((s) => console.log("Version:", s.version));
// 近期 sessions
SDK.api.getSessions(10).then((resp) => console.log(resp.sessions.length));
完整清單請參閱 Web Dashboard → REST API。
Shell 插槽
插槽讓外掛將元件注入至 app shell 的指定位置 — cockpit 側邊欄、頂部列、頁尾、覆蓋圖層 — 而無需佔用整個分頁。多個外掛可填充同一插槽;它們按註冊順序堆疊渲染。
在插掛 bundle 內部註冊:
window.__HERMES_PLUGINS__.registerSlot("my-plugin", "sidebar", MySidebar);
window.__HERMES_PLUGINS__.registerSlot("my-plugin", "header-left", MyCrest);
插槽目錄
Shell 全域插槽(在 app shell 任意位置渲染):
| 插槽 | 位置 |
|---|---|
backdrop | <Backdrop /> 層級堆疊內部,雜訊層之上。 |
header-left | 頂部列中 Hermes 品牌之前。 |
header-right | 頂部列中主題/語言切換器之前。 |
header-banner | 導航下方的全寬橫幅。 |
sidebar | Cockpit 側邊欄導軌 — 僅在 layoutVariant === "cockpit" 時渲染。 |
pre-main | 路由出口之上(<main> 內部)。 |
post-main | 路由出口之下(<main> 內部)。 |
footer-left | 頁尾儲存格內容(取代預設值)。 |
footer-right | 頁尾儲存格內容(取代預設值)。 |
overlay | 所有其他元素之上的固定定位層。適用於 customCSS 無法單獨實現的外觀效果(掃描線、暈影)。 |
頁面範圍插槽(僅在指定的內建頁面上渲染 — 用於將小工具、卡片或工具列注入現有頁面,無需替換整個路由):
| 插槽 | 渲染位置 |
|---|---|
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(Keys)頁面的頂部 / 底部。 |
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新增導航分頁(替換就是目的)。
只有一個外掛可以覆寫特定路徑。若兩個外掛聲稱相同的 override,第一個生效,第二個會被忽略並在開發模式下顯示警告。
若你只想在現有頁面上新增卡片或工具列而非完全接管,請使用頁面範圍插槽。
增強內建頁面(頁面範圍插槽)
透過 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."),
);
}
// 隱藏分頁的佔位元件。
window.__HERMES_PLUGINS__.register("session-notes", function () { return null; });
// 實際功能。
window.__HERMES_PLUGINS__.registerSlot("session-notes", "sessions:top", Banner);
})();
重點:
tab.hidden: true讓外掛不出現在側邊欄 — 它沒有獨立頁面。slotsmanifest 欄位僅作為文件輔助。實際綁定由 JS bundle 透過registerSlot()完成。- 多個外掛可聲稱相同的頁面範圍插槽。它們按註冊順序堆疊渲染。
- 無外掛註冊時零足跡:內建頁面渲染完全不變。
參考外掛(hermes-example-plugins 中的 example-dashboard)提供了完整 demo,會在 sessions:top 注入橫幅 — 安裝它即可端到端體驗此模式。
Slot-only plugins(tab.hidden)
當 tab.hidden: true 時,外掛會註冊其元件(供直接網址訪問)與任何插槽,但永遠不會在導航中新增分頁。供僅存在於插槽注入的外掛使用 — 頂部列徽章、側邊欄 HUD、覆蓋圖層。
{
"name": "header-crest",
"label": "Header Crest",
"tab": {
"path": "/header-crest",
"position": "end",
"hidden": true
},
"slots": ["header-left"],
"entry": "dist/index.js"
}
Bundle 仍會呼叫 register() 並傳入佔位元件(以防有人直接訪問網址的良好實踐),然後再呼叫 registerSlot() 進行實際工作。
後端 API 路由
外掛可透過在 manifest 中設定 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 路由繞過 session-token 驗證,因為控制台伺服器預設綁定至 localhost。若你運行不受信任的外掛,請勿以 --host 0.0.0.0 將控制台暴露於公開介面 — 它們的路由也會變得可達。
存取 Hermes 內部
後端路由在控制台行程內運行,因此可直接從 hermes-agent 原始碼 import:
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 檔案並在 manifest 中引用:
{
"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 色票為 --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 時有效 |
發現結果會被快取於每個控制台行程。新增外掛後,可執行:
# 強制重新掃描(無需重啟)
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。- 對每個 manifest:注入 CSS
<link>(如有宣告),然後<script>標籤載入 JS bundle。 - 外掛的 IIFE 執行並呼叫
window.__HERMES_PLUGINS__.register(name, Component)— 並可選地為每個插槽呼叫.registerSlot(name, slot, Component)。 - 控制台根據 manifest 解析已註冊的元件,將分頁加入導航(除非
hidden),並將元件掛載為路由。
外掛在腳本載入後有最多 2 秒 的時間呼叫 register()。之後控制台停止等待並完成初始渲染。若外掛稍後註冊,它仍然會出現 — 導航是響應式的。
若外掛的腳本載入失敗(404、語法錯誤、IIFE 期間例外),控制台會在瀏覽器控制台記錄警告並繼續運作。
組合式主題 + 外掛 demo
strike-freedom-cockpit 外掛(附屬 repo hermes-example-plugins)是完整的換肤 demo。它將主題 YAML 與僅插槽外掛配對,在不分叉控制台的情況下打造 cockpit 風格的 HUD。
展示內容:
- 完整主題使用了 palette、typography、
fontUrl、layoutVariant: cockpit、assets、componentStyles(切角卡片、漸層背景)、colorOverrides與customCSS(掃描線覆蓋)。 - 僅插槽外掛(
tab.hidden: true)註冊至三個插槽:sidebar— MS-STATUS 面板,包含由SDK.api.getStatus()驅動的即時遙測條。header-left— 從活躍主題讀取--theme-asset-crest的陣營徽章。footer-right— 取代預設組織文字的自訂標語。
- 外掛透過 CSS 變數讀取主題提供的素材,因此切換主題時會自動更換 hero/crest 而無需修改外掛程式碼。
安裝:
git clone https://github.com/NousResearch/hermes-example-plugins.git
# 主題
cp hermes-example-plugins/strike-freedom-cockpit/theme/strike-freedom.yaml \
~/.hermes/dashboard-themes/
# 外掛
cp -r hermes-example-plugins/strike-freedom-cockpit ~/.hermes/plugins/
開啟控制台,從主題選擇器中選擇 Strike Freedom。cockpit 側邊欄出現,頂部列顯示徽章,標語取代頁尾。切換回 Hermes Teal,外掛仍然安裝但不可見(sidebar 插槽僅在 cockpit 佈局變體下渲染)。
閱讀外掛原始碼(附屬 repo 中的 strike-freedom-cockpit/dashboard/dist/index.js)以了解它如何讀取 CSS 變數、防禦不支援插槽的舊版控制台,以及如何從單一 bundle 註冊三個插槽。
API 參考
主題端點
| 端點 | 方法 | 說明 |
|---|---|---|
/api/dashboard/themes | GET | 列出可用主題 + 活躍主題名稱。內建主題回傳 {name, label, description};使用者主題還包含 definition 欄位,含完整標準化主題物件。 |
/api/dashboard/theme | PUT | 設定活躍主題。Body:{"name": "midnight"}。儲存至 config.yaml 的 dashboard.theme。 |
外掛端點
| 端點 | 方法 | 說明 |
|---|---|---|
/api/dashboard/plugins | GET | 列出已發現的外掛(含 manifest,移除內部欄位)。 |
/api/dashboard/plugins/rescan | GET | 強制重新掃描外掛目錄(無需重啟)。 |
/dashboard-plugins/<name>/<path> | GET | 從外掛的 dashboard/ 目錄提供靜態素材。路徑遍歷已被封鎖。 |
/api/plugins/<name>/* | * | 外掛註冊的後端路由。 |
SDK on window
| 全域變數 | 類型 | 提供者 |
|---|---|---|
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。
我的外掛分頁沒有出現。
- 確認 manifest 位於
~/.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 以確認外掛 bundle 確實有執行。
外掛後端路由回傳 404。
- 確認 manifest 的
"api": "plugin_api.py"指向dashboard/內的現有檔案。 - 重啟
hermes dashboard— 外掛 API 路由在啟動時掛載一次,不會在重新掃描時掛載。 - 確認
plugin_api.py匯出模組層級的router = APIRouter()。其他匯出名稱不會被識別。 - 檢視
~/.hermes/logs/errors.log中的Failed to load plugin <name> API routes— import 錯誤會記錄在此。
切換主題時我的 colorOverrides 被清除了。
colorOverrides 限定於活躍主題,切換主題時會清除 — 這是設計如此。若你需要持久的覆寫,請將它們放入主題的 YAML 中,而非即時選擇器。
主題的 customCSS 被截斷了。
customCSS 區塊每個主題上限 32 KiB。將大型樣式表拆分至多個主題,或改用透過 css 欄位注入完整樣式表的外掛(無大小限制)。
我想在 PyPI 上發布外掛。
控制台外掛透過目錄結構安裝,而非 pip 入口點。目前最乾淨的散佈方式是讓使用者將 git repo 克隆至 ~/.hermes/plugins/。控制台外掛的 pip 安裝器目前尚未整合。