ByteNoteByteNote

字节笔记本

2026年6月21日

hermes教程-扩展仪表板

API中转
¥120

Hermes 网络仪表板(hermes dashboard)的设计支持在不分叉代码库的情况下进行换肤和扩展。它暴露了三个层次:

  1. 主题(Themes) — YAML 文件,可重新绘制仪表板的调色板、排版、布局以及每个组件的样式。将文件放入 ~/.hermes/dashboard-themes/ 后,它就会出现在主题切换器中。
  2. UI 插件(UI plugins) — 一个包含 manifest.json 和 JavaScript 包的目录,用于注册标签页、替换内置页面、通过页面级插槽增强页面,或将组件注入到命名的外壳插槽中。
  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 主题,因此一个主题可以小到只有一个颜色。

快速入门 — 你的第一个主题

bash
mkdir -p ~/.hermes/dashboard-themes
yaml
## ~/.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)。

yaml
palette:
  background:
    hex: "#05091a"
    alpha: 1.0
  midground: "#d8f0ff"          # 纯十六进制,alpha = 1.0
  foreground:
    hex: "#ffffff"
    alpha: 0                    # 不可见的顶层
  warmGlow: "rgba(255, 199, 55, 0.24)"
  noiseOpacity: 0.7

排版

类型描述
fontSans字符串正文的 CSS font-family 堆栈(应用于 htmlbody)。
fontMono字符串代码块、<code>.font-mono 实用类的 CSS font-family 堆栈。
fontDisplay字符串可选的标题/显示堆栈。回退到 fontSans
fontUrl字符串可选的外部样式表 URL。在主题切换时作为 <link rel="stylesheet"> 注入到 <head> 中。相同的 URL 不会重复注入。适用于 Google Fonts、Bunny Fonts、自托管的 @font-face 样式表 — 任何可链接的都可以。
baseSize字符串根字体大小 — 控制 rem 比例。例如 "14px""16px"
lineHeight字符串默认行高。例如 "1.5""1.65"
letterSpacing字符串默认字间距。例如 "0""0.01em""-0.01em"
yaml
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)

仪表板标题栏中的主题选择器在主题列表下方有一个字体部分。在那里选择任何字体,它会覆盖当前活动主题的正文字体 — 该选择独立于主题,并在主题切换时保持不变(存储在 config.yamldashboard.font 下)。选择主题默认可清除覆盖并回退到活动主题自身的 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 实用类都会按比例变化。
yaml
layout:
  radius: "0"
  density: compact

布局变体

layoutVariant 选择整体外壳布局。缺失时默认为 "standard"

变体行为
standard单列,最大宽度 1600px(默认)。
cockpit左侧边栏导轨(260px)+ 主内容。由插件通过 sidebar 插槽填充 — 请参阅 外壳插槽。没有插件时,导轨显示占位符。
tiled移除最大宽度限制,使页面可以使用整个视口宽度。
yaml
layoutVariant: cockpit

当前变体暴露为 document.documentElement.dataset.layoutVariant,因此 customCSS 中的原始 CSS 可以通过 :root[data-layout-variant="cockpit"] ... 来定位它。

主题资源(作为 CSS 变量的图片)

为主题附带艺术作品 URL。每个命名的插槽成为一个 CSS 变量(--theme-asset-<name>),内置外壳和任何插件都可以读取它。bg 插槽自动连接到背景中;其他插槽面向插件。

yaml
assets:
  bg: "https://example.com/hero-bg.jpg"           # 自动连接到 <Backdrop />
  hero: "/my-images/strike-freedom.png"           # 用于插件侧边栏
  crest: "/my-images/crest.svg"                   # 用于 header-left 插件
  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 读取:

javascript
// 在插件插槽中
const hero = getComputedStyle(document.documentElement)
  .getPropertyValue("--theme-asset-hero").trim();

组件样式覆盖

componentStyles 无需编写 CSS 选择器即可重新设置各个外壳组件的样式。每个桶的条目成为 CSS 变量(--component-<bucket>-<kebab-property>),外壳的共享组件会读取这些变量。因此 card: 覆盖适用于每个 <Card>header: 适用于应用栏等。

yaml
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 形式(clip-path)发出。值是纯 CSS 字符串 — CSS 接受的任何内容(clip-pathborder-imagebackgroundbox-shadowanimation 等)。

颜色覆盖

大多数主题不需要这个 — 3 层调色板会派生出每个 shadcn 令牌。当你想要一个派生无法产生的特定强调色时(例如柔和主题的更柔和的破坏性红色,或品牌的特定成功绿色),使用 colorOverrides

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

支持的键:cardcardForegroundpopoverpopoverForegroundprimaryprimaryForegroundsecondarysecondaryForegroundmutedmutedForegroundaccentaccentForegrounddestructivedestructiveForegroundsuccesswarningborderinputring

每个键 1:1 映射到 --color-<kebab> CSS 变量(例如 primaryForeground--color-primary-foreground)。在此设置的任何键仅在当前活动主题中胜出调色板级联 — 切换到另一个主题会清除覆盖。

原始 customCSS

对于 componentStyles 无法表达的选择器级样式 — 伪元素、动画、媒体查询、主题范围的覆盖 — 将原始 CSS 放入 customCSS

yaml
customCSS: |
  /* 扫描线覆盖层 — 仅在 cockpit 变体激活时可见。 */
  :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)与默认相同系统堆栈,18px,行高 1.650.5rem 圆角,宽敞
Midnight (midnight)深蓝紫色Inter + JetBrains Mono,14px0.75rem 圆角,舒适
Ember (ember)暖深红色 + 青铜色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 参考

一个文件中的所有旋钮 — 复制并修剪不需要的部分:

yaml
## ~/.hermes/dashboard-themes/ocean.yaml
name: ocean
label: Ocean Deep
description: Deep sea blues with coral accents
## 3 层调色板(接受 {hex, alpha} 或纯十六进制)
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: |
  /* 任何额外的选择器级调整 */

创建文件后刷新仪表板。从标题栏实时切换主题 — 点击调色板图标。选择会持久化到 config.yamldashboard.theme 下,并在重新加载时恢复。


插件

仪表板插件是一个包含 manifest.json、预构建的 JS 包,以及可选的 CSS 文件和包含 FastAPI 路由的 Python 文件的目录。插件位于 ~/.hermes/plugins/<name>/ 中,与其他 Hermes 插件相邻 — 仪表板扩展是该插件目录内的一个 dashboard/ 子文件夹,因此一个插件可以从单个安装同时扩展 CLI/网关和仪表板。

插件不捆绑 React 或 UI 组件。它们使用暴露在 window.__HERMES_PLUGIN_SDK__ 上的插件 SDK。这使插件包保持小巧(通常只有几 KB)并避免版本冲突。

快速入门 — 你的第一个插件

创建目录结构:

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

编写清单:

json
// ~/.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 包(一个纯 IIFE — 无需构建步骤):

javascript
// ~/.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 作为外部依赖并输出 IIFE。唯一硬性要求是最终文件是一个可通过 <script> 加载的单个 JS 文件。React 从不被捆绑;它来自 SDK.React

目录布局

text
~/.hermes/plugins/my-plugin/
├── plugin.yaml              # 可选 — 现有的 CLI/网关插件清单
├── __init__.py              # 可选 — 现有的 CLI/网关钩子
└── dashboard/               # 仪表板扩展
    ├── manifest.json        # 必需 — 标签页配置、图标、入口点
    ├── dist/
    │   ├── index.js         # 必需 — 预构建的 JS 包(IIFE)
    │   └── style.css        # 可选 — 自定义 CSS
    └── plugin_api.py        # 可选 — 后端 API 路由(FastAPI)

单个插件目录可以携带三个正交的扩展:

  • plugin.yaml + __init__.py — CLI/网关插件(参见 插件页面)。
  • dashboard/manifest.json + dashboard/dist/index.js — 仪表板 UI 插件。
  • dashboard/plugin_api.py — 仪表板后端路由。

它们都不是必需的;只包含你需要的层。

清单参考

json
{
  "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此插件填充的命名外壳插槽。仅作为文档辅助 — 实际注册发生在 JS 包中,通过 registerSlot() 进行。在此列出插槽使发现界面更丰富。
entry相对于 dashboard/ 的 JS 包路径。默认为 dist/index.js
css要作为 <link> 标签注入的 CSS 文件路径。
api包含 FastAPI 路由的 Python 文件路径。挂载在 /api/plugins/<name>/ 下。

可用图标

插件使用 Lucide 图标名称。仪表板按名称映射这些图标 — 未知名称静默回退到 Puzzle

当前映射:ActivityBarChart3ClockCodeDatabaseEyeFileTextGlobeHeartKeyRoundMessageSquarePackagePuzzleSettingsShieldSparklesStarTerminalWrenchZap

需要不同的图标?向 web/src/App.tsxICON_MAP 提交 PR — 纯增量更改。

插件 SDK

插件所需的一切都在 window.__HERMES_PLUGIN_SDK__ 上。插件不应直接导入 React。

javascript
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 客户端 + 原始获取器
SDK.api                      // 类型化客户端 — getStatus、getSessions、getConfig 等
SDK.fetchJSON                // 用于自定义端点的原始 fetch(插件注册的路由)

// 实用工具
SDK.utils.cn                 // Tailwind 类合并器(clsx + twMerge)
SDK.utils.timeAgo            // 从 Unix 时间戳生成 "5m ago"
SDK.utils.isoTimeAgo         // 从 ISO 字符串生成 "5m ago"

// Hooks
SDK.useI18n                  // 用于多语言插件的 i18n hook

调用你的插件后端

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

fetchJSON 注入会话认证令牌,将错误作为抛出的异常暴露,并自动解析 JSON。

调用内置 Hermes 端点

javascript
// 代理状态
SDK.api.getStatus().then((s) => console.log("Version:", s.version));

// 最近的会话
SDK.api.getSessions(10).then((resp) => console.log(resp.sessions.length));

参见 Web 仪表板 → REST API 获取完整列表。

外壳插槽

插槽允许插件将组件注入到应用外壳的命名位置 — 驾驶舱侧边栏、标题栏、页脚、覆盖层 — 而无需占用整个标签页。多个插件可以填充同一个插槽;它们按注册顺序堆叠渲染。

从插件包内部注册:

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

插槽目录

外壳级插槽(在应用 chrome 的任何位置渲染):

插槽位置
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 页面顶部添加一个横幅卡片:

javascript
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 结合使用。

外壳仅对上述插槽渲染 <PluginSlot name="..." />。注册表接受其他名称用于嵌套插件 UI — 插件可以通过 SDK.components.PluginSlot 暴露自己的插槽。

重新注册和 HMR

如果相同的 (plugin, slot) 对注册两次,后一次调用会替换前一次 — 这与 React HMR 期望的插件重新挂载行为一致。

替换内置页面(tab.override

tab.override 设置为内置路由路径会使插件的组件替换该页面,而不是添加新标签页。当主题想要自定义主页(/)但希望保持仪表板其余部分不变时很有用。

json
{
  "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)。参见 外壳插槽 → 插槽目录 中的完整目录。

最小示例 — 在 Sessions 页面顶部固定一个横幅:

json
// ~/.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"
}
javascript
// ~/.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 清单字段仅作为文档。实际绑定发生在 JS 包中,通过 registerSlot() 进行。
  • 多个插件可以声明同一个页面级插槽。它们按注册顺序堆叠渲染。
  • 当没有插件注册时,零占用:内置页面完全按原样渲染。

一个参考插件(hermes-example-plugins 中的 example-dashboard)提供了一个实时演示,向 sessions:top 注入了一个横幅 — 安装它以查看端到端模式。

仅插槽插件(tab.hidden

tab.hidden: true 时,插件注册其组件(用于直接 URL 访问)和任何插槽,但从不向导航添加标签页。用于仅存在于注入插槽中的插件 — 标题徽章、侧边栏 HUD、覆盖层。

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

包仍然使用占位符组件调用 register()(以防有人直接访问 URL 的好习惯),然后调用 registerSlot() 完成实际工作。

后端 API 路由

插件可以通过在清单中设置 api 来注册 FastAPI 路由。创建文件并导出一个 router

python
## ~/.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 代码库导入:

python
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 文件并在清单中引用它:

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

该文件在插件加载时作为 <link> 标签注入。使用特定的类名以避免与仪表板样式冲突,并引用仪表板的 CSS 变量以保持主题感知:

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

发现结果在每个仪表板进程中被缓存。添加新插件后,执行以下任一操作:

bash
## 强制重新扫描,无需重启
curl http://127.0.0.1:9119/api/dashboard/plugins/rescan

…或重启 hermes dashboard

插件加载生命周期

  1. 仪表板加载。main.tsxwindow.__HERMES_PLUGIN_SDK__ 上暴露 SDK,在 window.__HERMES_PLUGINS__ 上暴露注册表。
  2. App.tsx 调用 usePlugins() → 获取 GET /api/dashboard/plugins
  3. 对于每个清单:注入 CSS <link>(如果声明),然后加载 JS 包的 <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 变量读取主题提供的艺术作品,因此切换主题会更改英雄/徽章,而无需更改插件代码。

安装:

bash
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。驾驶舱侧边栏出现,徽章显示在标题中,标语替换了页脚。切换回 Hermes Teal,插件保持安装但不可见(sidebar 插槽仅在 cockpit 布局变体下渲染)。

阅读插件源代码(配套仓库中的 strike-freedom-cockpit/dashboard/dist/index.js)以了解它如何读取 CSS 变量、防范不支持插槽的旧仪表板,以及如何从一个包中注册三个插槽。


API 参考

主题端点

端点方法描述
/api/dashboard/themesGET列出可用主题 + 活动名称。内置主题返回 {name, label, description};用户主题还包括一个 definition 字段,包含完整的规范化主题对象。
/api/dashboard/themePUT设置活动主题。请求体:{"name": "midnight"}。持久化到 config.yamldashboard.theme 下。

插件端点

端点方法描述
/api/dashboard/pluginsGET列出发现的插件(包含清单,排除内部字段)。
/api/dashboard/plugins/rescanGET强制重新扫描插件目录,无需重启。
/dashboard-plugins/<name>/<path>GET从插件的 dashboard/ 目录提供静态资源。路径遍历被阻止。
/api/plugins/<name>/**插件注册的后端路由。

window 上的 SDK

全局变量类型提供者
window.__HERMES_PLUGIN_SDK__对象registry.ts — React、hooks、UI 组件、API 客户端、实用工具。
window.__HERMES_PLUGINS__.register(name, Component)函数注册插件的主组件。
window.__HERMES_PLUGINS__.registerSlot(name, slot, Component)函数注册到命名的外壳插槽。

故障排除

我的主题没有出现在选择器中。 检查文件是否在 ~/.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. 打开浏览器开发者工具 → 网络 — 确认 manifest.jsonindex.js 和任何 CSS 加载成功,没有 404。
  4. 打开浏览器开发者工具 → 控制台 — 查找 IIFE 期间的错误或 window.__HERMES_PLUGINS__ is undefined(表示 SDK 未初始化,通常是更早的 React 渲染崩溃)。
  5. 验证你的包使用与 manifest.json:name 相同的名称 调用了 window.__HERMES_PLUGINS__.register(...)

插槽注册的组件没有渲染。 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 上发布一个插件。 仪表板插件通过目录布局安装,而不是通过 pip 入口点。目前最干净的发布路径是一个用户克隆到 ~/.hermes/plugins/ 的 git 仓库。基于 pip 的仪表板插件安装程序目前尚未实现。



分享: