H繁中版
文件核心功能extending the dashboard
<!-- Source: https://hermesbible.com/docs/user-guide/features/extending-the-dashboard -->

章節:核心功能 · 網址:https://hermesbible.com/docs/user-guide/features/extending-the-dashboard

Hermes 網頁控制台(hermes dashboard)設計為可在不分叉原始碼的情況下進行換肤與擴充。共暴露三個層級:

  1. 主題(Themes) — YAML 檔案,用於重新描繪控制台的色盤、字體排印、佈局及各元件的外觀。將檔案放入 ~/.hermes/dashboard-themes/ 即可出現在主題選擇器中。
  2. UI 外掛(UI plugins) — 包含 manifest.json 與 JavaScript bundle 的目錄,可註冊分頁、替換內建頁面、透過頁面範圍插槽(page-scoped slots)增強頁面,或將元件注入至指定的 shell 插槽。
  3. 後端外掛(Backend plugins) — 外掛目錄中的 Python 檔案,暴露 FastAPI router;路由掛載於 /api/plugins/<name>/ 下,並從外掛的 UI 端呼叫。

以上三者皆為執行時直接放入:不需要複製 repo、不需要 npm run build、不需要修改控制台原始碼。本頁為三者的完整參考文件。

若只想使用控制台,請參閱 Web Dashboard。若想重新設計終端 CLI 的外觀(而非網頁控制台),請參閱 Skins & Themes — CLI 皮膚系統與控制台主題無關。

注意 — 組件如何組合

主題與外掛彼此獨立但可互相搭配。主題可以獨立運作(僅需一個 YAML 檔案)。外掛也可以獨立運作(僅需一個分頁)。兩者結合時可打造完整的視覺換肤效果搭配自訂 HUD — 範例 strike-freedom-cockpit demo(位於 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.warmGlowrgba(...) 字串,作為 <Backdrop /> 的暈影顏色。
palette.noiseOpacity0–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

鍵值類型說明
fontSansstring內文的 CSS font-family 堆疊(套用至 htmlbody)。
fontMonostring程式碼區塊、<code>.font-mono 工具類別的 CSS font-family 堆疊。
fontDisplaystring選填的標題/展示字體堆疊。預設回退至 fontSans
fontUrlstring選填的外部樣式表網址。主題切換時以 <link rel="stylesheet"> 注入 <head>。相同網址不會重複注入。適用於 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

選擇器提供精選目錄(系統字體堆疊加上一組涵蓋 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 — 所有圓角元素同步變動。
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

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: {}

支援的桶:cardheaderfootersidebartabprogressbadgebackdroppage

屬性名稱使用 camelCase(clipPath),輸出為 kebab-case(clip-path)。值為純 CSS 字串 — 任何 CSS 接受的值皆可(clip-pathborder-imagebackgroundbox-shadowanimation、...)。

Color overrides

大多數主題不需要此功能 — 三層色盤會衍生出所有 shadcn 色票。當你需要特定的強調色而衍生無法產生時(例如粉彩主題用的柔和 destructive 紅色、品牌指定的 success 綠色),可使用 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 無法表達的選擇器層級外觀調整 — 偽元素、動畫、媒體查詢、主題範圍覆寫 — 可將原始 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 Tealdefault深青色 + 奶油色系統字體,15px0.5rem 圓角,comfortable
Hermes Teal (Large)default-large同預設系統字體,18px,行高 1.650.5rem 圓角,spacious
Midnightmidnight深藍紫色Inter + JetBrains Mono,14px0.75rem 圓角,comfortable
Emberember暖 crimson + 青銅色Spectral(serif)+ IBM Plex Mono,15px0.25rem 圓角,comfortable
Monomono灰階IBM Plex Sans + IBM Plex Mono,13px0 圓角,compact
Cyberpunkcyberpunk黑底霓虹綠全域 Share Tech Mono,14px0 圓角,compact
Rosérose粉紅色 + 象牙色Fraunces(serif)+ DM Mono,16px1rem 圓角,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.yamldashboard.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簡短說明(顯示於控制台管理介面)。
iconLucide 圖示名稱。預設為 Puzzle。未知名稱回退至 Puzzle
versionSemver 字串。預設為 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() 完成。在此列出插槽可讓發現介面顯示更多資訊。
entryJS bundle 相對 dashboard/ 的路徑。預設為 dist/index.js
cssCSS 檔案相對路徑,會以 <link> 標籤注入。
api含 FastAPI 路由的 Python 檔案路徑。掛載於 /api/plugins/<name>/

可用圖示

外掛使用 Lucide 圖示名稱。控制台按名稱映射 — 未知名稱靜默回退至 Puzzle

目前映射的圖示:ActivityBarChart3ClockCodeDatabaseEyeFileTextGlobeHeartKeyRoundMessageSquarePackagePuzzleSettingsShieldSparklesStarTerminalWrenchZap

需要其他圖示?向 web/src/App.tsxICON_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導航下方的全寬橫幅。
sidebarCockpit 側邊欄導軌 — 僅在 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 讓外掛不出現在側邊欄 — 它沒有獨立頁面。
  • slots manifest 欄位僅作為文件輔助。實際綁定由 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/data
  • POST /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

外掛載入生命週期

  1. 控制台載入。main.tsx 將 SDK 暴露至 window.__HERMES_PLUGIN_SDK__,註冊表暴露至 window.__HERMES_PLUGINS__
  2. App.tsx 呼叫 usePlugins() → 呼叫 GET /api/dashboard/plugins
  3. 對每個 manifest:注入 CSS <link>(如有宣告),然後 <script> 標籤載入 JS bundle。
  4. 外掛的 IIFE 執行並呼叫 window.__HERMES_PLUGINS__.register(name, Component) — 並可選地為每個插槽呼叫 .registerSlot(name, slot, Component)
  5. 控制台根據 manifest 解析已註冊的元件,將分頁加入導航(除非 hidden),並將元件掛載為路由。

外掛在腳本載入後有最多 2 秒 的時間呼叫 register()。之後控制台停止等待並完成初始渲染。若外掛稍後註冊,它仍然會出現 — 導航是響應式的。

若外掛的腳本載入失敗(404、語法錯誤、IIFE 期間例外),控制台會在瀏覽器控制台記錄警告並繼續運作。


組合式主題 + 外掛 demo

strike-freedom-cockpit 外掛(附屬 repo hermes-example-plugins)是完整的換肤 demo。它將主題 YAML 與僅插槽外掛配對,在不分叉控制台的情況下打造 cockpit 風格的 HUD。

展示內容:

  • 完整主題使用了 palette、typography、fontUrllayoutVariant: cockpitassetscomponentStyles(切角卡片、漸層背景)、colorOverridescustomCSS(掃描線覆蓋)。
  • 僅插槽外掛(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/themesGET列出可用主題 + 活躍主題名稱。內建主題回傳 {name, label, description};使用者主題還包含 definition 欄位,含完整標準化主題物件。
/api/dashboard/themePUT設定活躍主題。Body:{"name": "midnight"}。儲存至 config.yamldashboard.theme

外掛端點

端點方法說明
/api/dashboard/pluginsGET列出已發現的外掛(含 manifest,移除內部欄位)。
/api/dashboard/plugins/rescanGET強制重新掃描外掛目錄(無需重啟)。
/dashboard-plugins/<name>/<path>GET從外掛的 dashboard/ 目錄提供靜態素材。路徑遍歷已被封鎖。
/api/plugins/<name>/**外掛註冊的後端路由。

SDK on window

全域變數類型提供者
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. 確認 manifest 位於 ~/.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 以確認外掛 bundle 確實有執行。

外掛後端路由回傳 404。

  1. 確認 manifest 的 "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 — import 錯誤會記錄在此。

切換主題時我的 colorOverrides 被清除了。 colorOverrides 限定於活躍主題,切換主題時會清除 — 這是設計如此。若你需要持久的覆寫,請將它們放入主題的 YAML 中,而非即時選擇器。

主題的 customCSS 被截斷了。 customCSS 區塊每個主題上限 32 KiB。將大型樣式表拆分至多個主題,或改用透過 css 欄位注入完整樣式表的外掛(無大小限制)。

我想在 PyPI 上發布外掛。 控制台外掛透過目錄結構安裝,而非 pip 入口點。目前最乾淨的散佈方式是讓使用者將 git repo 克隆至 ~/.hermes/plugins/。控制台外掛的 pip 安裝器目前尚未整合。



Kanban worker lanes