跳到主要内容

扩展仪表盘

Hermes Web 仪表盘(hermes dashboard)旨在无需分叉代码库即可重新换肤和扩展。它暴露了三个层级:

  1. 主题 (Themes) — YAML 文件,用于重绘仪表盘的调色板、排版、布局以及各组件的装饰样式。将文件放入 ~/.hermes/dashboard-themes/;它将出现在主题切换器中。
  2. UI 插件 (UI plugins) — 一个包含 manifest.json 和 JavaScript 捆绑包的目录,用于注册标签页、替换内置页面、通过页面作用域插槽增强页面,或将组件注入命名的 Shell 插槽中。
  3. 后端插件 (Backend plugins) — 位于该插件目录内的 Python 文件,暴露一个 FastAPI router;路由挂载在 /api/plugins/<name>/ 下,并从插件的 UI 中调用。

这三者均支持运行时即插即用:无需克隆仓库,无需执行 npm run build,也无需修补仪表盘源代码。本页是这三者的规范参考文档。

如果您只想使用仪表盘,请参阅 Web 仪表盘。如果您想为终端 CLI(而非 Web 仪表盘)重新换肤,请参阅 皮肤与主题 — CLI 皮肤系统与仪表盘主题无关。

组件如何组合

主题和插件相互独立但协同工作。主题可以独立存在(仅一个 YAML 文件)。插件也可以独立存在(仅一个标签页)。两者结合可让您构建带有自定义 HUD 的完整视觉换肤 — 示例 strike-freedom-cockpit 演示(位于 hermes-example-plugins 配套仓库中 — 安装步骤参见 组合主题 + 插件演示)正是这样做的。


目录


主题

主题是存储在 ~/.hermes/dashboard-themes/ 中的 YAML 文件。文件名无关紧要(系统使用的是主题的 name: 字段),但惯例是 <name>.yaml。所有字段均为可选 — 缺失的键会回退到内置的 default 主题,因此主题可以小至仅包含一种颜色。

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

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

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

刷新仪表盘。点击标题栏中的调色板图标并选择 Neon。背景变为黑色,文本和强调色变为品红色,所有衍生颜色(卡片、边框、柔和色、环等)均通过 CSS 中的 color-mix() 从该双色三元组重新计算得出。

这就是全部入门内容:一个文件,两种颜色。以下内容均为可选的精炼配置。

调色板、排版、布局

这三个模块是主题的核心。它们相互独立 — 您可以覆盖其中一个,而保留其他不变。

调色板(3 层)

调色板由三层颜色加上暖光晕影颜色和噪点颗粒倍增器组成。仪表盘的设计系统级联通过 CSS color-mix() 从此三元组派生出每个兼容 shadcn 的令牌(card、popover、muted、border、primary、destructive、ring 等)。覆盖三种颜色会级联影响整个 UI。

描述
palette.background最深的画布颜色 — 通常接近黑色。驱动页面背景和卡片填充色。
palette.midground主要文本和强调色。大多数 UI 装饰样式读取此颜色(前景文本、按钮轮廓、焦点环)。
palette.foreground顶层高亮色。默认主题将其设置为 alpha 为 0 的白色(不可见);希望顶部有明亮强调色的主题可以提高其 alpha 值。
palette.warmGlowrgba(...) 字符串,用作 <Backdrop /> 的晕影颜色。
palette.noiseOpacity0–1.2 的颗粒叠加层倍增器。越低越柔和,越高越粗糙。

每层接受 {hex: "#RRGGBB", alpha: 0.0–1.0} 或纯十六进制字符串(alpha 默认为 1.0)。

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

排版

类型描述
fontSansstring正文内容的 CSS font-family 堆栈(应用于 html, body)。
fontMonostring代码块、<code>.font-mono 工具类的 CSS font-family 堆栈。
fontDisplaystring可选的标题/展示用字体堆栈。回退至 fontSans
fontUrlstring可选的外部样式表 URL。在切换主题时,作为 <link rel="stylesheet"> 注入到 <head> 中。同一 URL 永远不会被注入两次。适用于 Google Fonts、Bunny Fonts、自托管的 @font-face 样式表——任何可链接的资源。
baseSizestring根字体大小——控制 rem 比例。例如 "14px""16px"
lineHeightstring默认行高。例如 "1.5""1.65"
letterSpacingstring默认字间距。例如 "0""0.01em""-0.01em"
typography:
fontSans: '"Orbitron", "Eurostile", "Impact", sans-serif'
fontMono: '"Share Tech Mono", ui-monospace, monospace'
fontDisplay: '"Orbitron", "Eurostile", sans-serif'
fontUrl: "https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700&family=Share+Tech+Mono&display=swap"
baseSize: "14px"
lineHeight: "1.5"
letterSpacing: "0.04em"
从 UI 更改字体(无需 YAML)

仪表板标题栏中的主题选择器在主题列表下方有一个 Font(字体)部分。在此处选择任意字体,它将覆盖当前激活主题的正文字体——该选择独立于主题,并在主题切换后保持持久化(存储在 config.yamldashboard.font 下)。选择 Theme default(主题默认值)以清除覆盖并回退到激活主题自身的 fontSans

该选择器提供了一份精选目录(系统字体堆栈以及一组涵盖无衬线/衬线/等宽字体的 Google Fonts 家族)。它故意接受自由文本的字体 URL——因为字体的样式表是作为 <link> 注入的,所以该目录保持了注入源的固定性。对于完全自定义的字体,请如上所示在主题 YAML 中设置 fontSans + fontUrl。主题的 fontMono(代码块、终端)始终不受 UI 覆盖的影响。

布局

描述
radius任意 CSS 长度("0""0.25rem""0.5rem""1rem"、...)圆角令牌。映射到 --radius 并级联到 --radius-sm/md/lg/xl——所有圆角元素会同步变化。
densitycompact | comfortable | spacious作为 --spacing-mul CSS 变量应用的间距倍数。compact = 0.85×comfortable = 1.0×(默认),spacious = 1.2×。缩放 Tailwind 的基础间距,因此 padding、gap 和 space-between 工具类都会按比例变化。
layout:
radius: "0"
density: compact

布局变体

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

变体行为
standard单列,最大宽度 1600px(默认)。
cockpit左侧边栏轨道(260px)+ 主要内容。由插件通过 sidebar 插槽填充——参见 Shell slots。如果没有插件,轨道将显示占位符。
tiled取消最大宽度限制,使页面可以使用完整的视口宽度。
layoutVariant: cockpit

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

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

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

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

值接受:

  • 纯 URL——自动包裹在 url(...) 中。
  • 预包裹的 url(...)linear-gradient(...)radial-gradient(...) 表达式——原样使用。
  • "none"——明确选择不使用。

每个资源也会作为 --theme-asset-<name>-raw(未包裹的 URL)发出,以防插件需要将其传递给 <img src> 而不是 background-image

插件通过普通 CSS 或 JS 读取这些变量:

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

组件样式覆盖

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

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

支持的桶:cardheaderfootersidebartabprogressbadgebackdroppage

属性名使用驼峰式命名法(clipPath),并以短横线分隔格式(clip-path)发出。值是纯 CSS 字符串——CSS 接受的任何内容(clip-pathborder-imagebackgroundbox-shadowanimation、...)。

颜色覆盖

大多数主题不需要此功能——三层调色板派生出每个 shadcn 令牌。当您需要衍生过程无法产生的特定强调色时(例如柔和色调主题的更柔和的破坏性红色,或品牌的特定成功绿色),请使用 colorOverrides

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

支持的键:cardcardForegroundpopoverpopoverForegroundprimaryprimaryForegroundsecondarysecondaryForegroundmutedmutedForegroundaccentaccentForegrounddestructivedestructiveForegroundsuccesswarningborderinputring

每个键与 --color-<kebab> CSS 变量一一映射(例如 primaryForeground--color-primary-foreground)。此处设置的任何键仅对当前活动主题生效,并优先于调色板级联——切换到其他主题时会清除这些覆盖。

原始 customCSS

对于 componentStyles 无法表达的 selector 级别的界面定制——如伪元素、动画、媒体查询、主题作用域覆盖——可以将原始 CSS 放入 customCSS

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

CSS 会在应用主题时作为单个作用域 <style data-hermes-theme-css> 标签注入,并在切换主题时清理。每个主题限制为 32 KiB。

内置主题

每个内置主题都附带自己的调色板、排版和布局——切换主题会产生除颜色之外的可见变化。

主题调色板排版布局
Hermes Teal (default)深青色 + 奶油色系统字体栈,15px0.5rem 圆角,舒适间距
Hermes Teal (Large) (default-large)同 default系统字体栈,18px,行高 1.650.5rem 圆角,宽松间距
Midnight (midnight)深蓝紫色Inter + JetBrains Mono,14px0.75rem 圆角,舒适间距
Ember (ember)暖 crimson + 青铜色Spectral (衬线) + IBM Plex Mono,15px0.25rem 圆角,舒适间距
Mono (mono)灰度IBM Plex Sans + IBM Plex Mono,13px0 圆角,紧凑间距
Cyberpunk (cyberpunk)黑色背景上的霓虹绿全局使用 Share Tech Mono,14px0 圆角,紧凑间距
Rosé (rose)粉色 + 象牙白Fraunces (衬线) + DM Mono,16px1rem 圆角,宽松间距

引用 Google Fonts 的主题(除 Hermes Teal 外)会按需加载样式表——首次切换到该主题时,会将一个 <link> 标签注入到 <head> 中。

完整主题 YAML 参考

所有配置项都在一个文件中——复制并删除不需要的部分:

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

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

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

layout:
radius: "0.75rem"
density: comfortable

layoutVariant: standard # standard | cockpit | tiled

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

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

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

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

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


插件

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

插件不打包 React 或 UI 组件。它们使用暴露在 window.__HERMES_PLUGIN_SDK__ 上的 Plugin SDK。这使得插件 bundle 非常小(通常只有几 KB),并避免版本冲突。

快速开始 — 你的第一个插件

创建目录结构:

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

编写 manifest:

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

编写 JS bundle(普通 IIFE — 无需构建步骤):

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

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

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

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

刷新仪表盘 — 你的标签页将出现在导航栏中,位于 Skills 之后。

跳过 React.createElement

如果你更喜欢 JSX,可以使用任何 bundler(esbuild、Vite、rollup),将 React 设为 external 并输出 IIFE。唯一硬性要求是最终文件是一个可通过 <script> 加载的单个 JS 文件。React 从不被打包;它来自 SDK.React

目录布局

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

单个插件目录可以包含三个正交扩展:

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

这些都不是必需的;只包含你需要的层。

Manifest 参考

{
"name": "my-plugin",
"label": "My Plugin",
"description": "What this plugin does",
"icon": "Sparkles",
"version": "1.0.0",
"tab": {
"path": "/my-plugin",
"position": "after:skills",
"override": "/",
"hidden": false
},
"slots": ["sidebar", "header-left"],
"entry": "dist/index.js",
"css": "dist/style.css",
"api": "plugin_api.py"
}
字段必填描述
name唯一的插件标识符。小写,允许使用连字符。用于 URL 和注册。
label在导航标签页中显示的显示名称。
description简短描述(显示在仪表板管理界面中)。
iconLucide 图标名称。默认为 Puzzle。未知名称将回退到 Puzzle
versionSemver 版本字符串。默认为 0.0.0
tab.path标签页的 URL 路径(例如 /my-plugin)。
tab.position插入标签页的位置。"end"(默认)、"after:<path>""before:<path>" — 冒号后的值是目标标签页的路径段(无前导斜杠)。示例:"after:skills""before:config"
tab.override设置为内置路由路径("/""/sessions""/config" 等)以替换该页面,而不是添加新标签页。参见 替换内置页面
tab.hidden当为 true 时,注册组件和任何插槽,但不向导航栏添加标签页。由仅插槽插件使用。参见 仅插槽插件
slots此插件填充的命名 shell 插槽。仅用于文档辅助 — 实际注册通过 JS bundle 中的 registerSlot() 进行。在此列出插槽可使发现界面更具信息量。
entry相对于 dashboard/ 的 JS bundle 路径。默认为 dist/index.js
css要作为 <link> 标签注入的 CSS 文件路径。
api包含 FastAPI 路由的 Python 文件路径。挂载在 /api/plugins/<name>/

可用图标

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

当前已映射:ActivityBarChart3ClockCodeDatabaseEyeFileTextGlobeHeartKeyRoundMessageSquarePackagePuzzleSettingsShieldSparklesStarTerminalWrenchZap

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

插件 SDK

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

const SDK = window.__HERMES_PLUGIN_SDK__;

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

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

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

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

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

调用插件的后端

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

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

调用内置 Hermes 端点

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

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

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

Shell 插槽

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

在插件 bundle 内部注册:

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

插槽目录

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

插槽位置
backdrop<Backdrop /> 层栈内,位于噪声层之上。
header-left在顶部栏中 Hermes 品牌标识之前。
header-right在顶部栏中主题/语言切换器之前。
header-banner导航栏下方的全宽条带。
sidebar驾驶舱侧边栏轨道 — 仅在 layoutVariant === "cockpit" 时渲染
pre-main在路由出口上方(在 <main> 内)。
post-main在路由出口下方(在 <main> 内)。
footer-left页脚单元格内容(替换默认值)。
footer-right页脚单元格内容(替换默认值)。
overlay固定定位层,位于所有其他内容之上。适用于单独使用 customCSS 无法实现的 chrome 效果(扫描线、暗角等)。

页面范围插槽(仅在指定的内置页面上渲染 — 使用这些插槽将小部件、卡片或工具栏注入现有页面,而无需覆盖整个路由):

插槽渲染位置
sessions:top / sessions:bottom/sessions 页面的顶部/底部。
analytics:top / analytics:bottom/analytics 页面的顶部/底部。
logs:top / logs:bottom/logs 的顶部(过滤器工具栏上方)/底部(日志查看器下方)。
cron:top / cron:bottom/cron 页面的顶部/底部。
skills:top / skills:bottom/skills 页面的顶部/底部。
config:top / config:bottom/config 页面的顶部/底部。
env:top / env:bottom/env(密钥)页面的顶部/底部。
docs:top / docs:bottom/docs 的顶部(iframe 上方)/底部。
chat:top / chat:bottom/chat 的顶部/底部(仅在启用嵌入式聊天时激活)。

示例 — 在 Sessions 页面顶部添加横幅卡片:

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

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

如果您的插件仅增强现有页面且不需要自己的侧边栏标签页,请将页面范围插槽与 tab.hidden: true 结合使用。

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

重新注册与 HMR

如果同一个 (plugin, slot) 对被注册了两次,后一次调用将替换前一次——这符合 React HMR 对插件重新挂载行为的预期。

替换内置页面(tab.override

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

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

设置 override 后:

  • 路由器中位于 / 的原始页面组件将被移除。
  • 你的插件将在 / 处渲染。
  • 不会为 tab.path 添加导航标签页(这正是覆盖的目的)。

只有一个插件可以覆盖给定路径。如果有两个插件声称覆盖同一路径,第一个插件胜出,第二个插件将被忽略并在开发模式下发出警告。

如果你只需要向现有页面添加卡片或工具栏而不接管整个页面,请改用页面作用域插槽

增强内置页面(页面作用域插槽)

通过 tab.override 进行完全替换是一种重型操作——你的插件现在拥有整个页面,包括我们未来对该页面发布的任何更新。大多数情况下,你只是想在现有页面中添加横幅、卡片或工具栏。这就是页面作用域插槽的用途。

每个内置页面都暴露 <page>:top<page>:bottom 插槽,分别在其内容区域的顶部和底部渲染。你的插件通过调用 registerSlot() 来填充其中一个插槽——内置页面保持正常工作,而你的组件与其并列渲染。

可用插槽:sessions:*analytics:*logs:*cron:*skills:*config:*env:*docs:*chat:*(每个都有 :top:bottom)。请参阅 Shell 插槽 → 插槽目录 中的完整目录。

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

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

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

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

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

关键点:

  • tab.hidden: true 使插件不出现在侧边栏中——它没有独立的页面。
  • slots 清单字段仅用于文档说明。实际绑定是通过 JS bundle 中的 registerSlot() 发生的。
  • 多个插件可以声明同一个页面作用域插槽。它们按注册顺序堆叠渲染。
  • 当没有插件注册时零开销:内置页面完全按原样渲染。

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

仅插槽插件(tab.hidden

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

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

Bundle 仍然使用占位符组件调用 register()(以防有人直接访问 URL,这是一种良好实践),然后调用 registerSlot() 执行实际工作。

后端 API 路由

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

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

router = APIRouter()

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

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

路由挂载在 /api/plugins/<name>/ 下,因此上述示例变为:

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

插件 API 路由绕过会话令牌身份验证,因为仪表板服务器默认绑定到 localhost。如果你运行不受信任的插件,请勿使用 --host 0.0.0.0 在公共接口上暴露仪表板——它们的路由也将变得可访问。

访问 Hermes 内部结构

后端路由在仪表板进程中运行,因此它们可以直接从 hermes-agent 代码库导入:

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

router = APIRouter()

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

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

每个插件的自定义 CSS

如果你的插件需要超出 Tailwind 类和内联 style= 的样式,请添加一个 CSS 文件并在清单中引用它:

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

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

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

仪表板将每个 shadcn token 暴露为 --color-* 以及主题额外变量(--theme-asset-*--component-<bucket>-*--radius--spacing-mul)。引用这些变量,你的插件会自动随活动主题重新换肤。

插件发现与重载

仪表板扫描以下三个目录以查找 dashboard/manifest.json

优先级目录来源标签
1(冲突时胜出)~/.hermes/plugins/<name>/dashboard/user
2<repo>/plugins/memory/<name>/dashboard/bundled
2<repo>/plugins/<name>/dashboard/bundled
3./.hermes/plugins/<name>/dashboard/project — 仅在设置 HERMES_ENABLE_PROJECT_PLUGINS 时有效

发现结果在每个仪表板进程中被缓存。添加新插件后,要么:

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

……要么重启 hermes dashboard

插件加载生命周期

  1. 仪表盘加载。main.tsx 将 SDK 暴露在 window.__HERMES_PLUGIN_SDK__ 上,并将注册表暴露在 window.__HERMES_PLUGINS__ 上。
  2. App.tsx 调用 usePlugins() → 获取 GET /api/dashboard/plugins
  3. 对于每个清单:注入 CSS <link>(如果已声明),然后加载 JS bundle 的 <script> 标签。
  4. 插件的 IIFE 运行并调用 window.__HERMES_PLUGINS__.register(name, Component) —— 以及可选地为每个插槽调用 .registerSlot(name, slot, Component)
  5. 仪表盘根据清单解析已注册的组件,将选项卡添加到导航中(除非设置为 hidden),并将组件作为路由挂载。

插件在其脚本加载后有最多 2 秒 的时间来调用 register()。此后,仪表盘将停止等待并完成初始渲染。如果插件稍后注册,它仍然会出现 —— 导航是响应式的。

如果插件的脚本加载失败(404、语法错误、IIFE 期间异常),仪表盘会在浏览器控制台中记录警告并继续运行而不加载该插件。


组合主题 + 插件演示

strike-freedom-cockpit 插件(配套仓库 hermes-example-plugins)是一个完整的换肤演示。它将主题 YAML 与仅插槽插件配对,无需分叉仪表盘即可生成驾驶舱风格的 HUD。

演示内容:

  • 一个完整主题,使用调色板、排版、fontUrllayoutVariant: cockpitassetscomponentStyles(缺角卡片圆角、渐变背景)、colorOverridescustomCSS(扫描线叠加层)。
  • 一个仅插槽插件(tab.hidden: true),注册到三个插槽中:
    • sidebar —— 一个 MS-STATUS 面板,带有由 SDK.api.getStatus() 驱动的实时遥测条。
    • header-left —— 一个阵营徽章,从活动主题中读取 --theme-asset-crest
    • footer-right —— 替换默认组织行的自定义标语。
  • 插件通过 CSS 变量读取主题提供的艺术作品,因此切换主题会改变英雄图/徽章,而无需更改插件代码。

安装:

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

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

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

打开仪表盘,从主题切换器中选择 Strike Freedom。驾驶舱侧边栏出现,徽章显示在标题中,标语替换了页脚。切换回 Hermes Teal,插件保持安装状态但不可见(sidebar 插槽仅在 cockpit 布局变体下渲染)。

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


API 参考

主题端点

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

插件端点

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

window 上的 SDK

全局变量类型提供者
window.__HERMES_PLUGIN_SDK__objectregistry.ts — React、hooks、UI 组件、API 客户端、工具函数。
window.__HERMES_PLUGINS__.register(name, Component)function注册插件的主组件。
window.__HERMES_PLUGINS__.registerSlot(name, slot, Component)function注册到命名的 shell 插槽中。

故障排除

我的主题未出现在选择器中。 检查文件是否位于 ~/.hermes/dashboard-themes/ 中并以 .yaml.yml 结尾。刷新页面。运行 curl http://127.0.0.1:9119/api/dashboard/themes —— 你的主题应出现在响应中。如果 YAML 存在解析错误,仪表盘会将日志记录到 ~/.hermes/logs/ 下的 errors.log 中。

我的插件选项卡未显示。

  1. 检查清单是否位于 ~/.hermes/plugins/<name>/dashboard/manifest.json(注意 dashboard/ 子目录)。
  2. 执行 curl http://127.0.0.1:9119/api/dashboard/plugins/rescan 以强制重新发现。
  3. 打开浏览器开发者工具 → Network(网络)—— 确认 manifest.jsonindex.js 和任何 CSS 加载时没有 404 错误。
  4. 打开浏览器开发者工具 → Console(控制台)—— 查找 IIFE 期间的错误或 window.__HERMES_PLUGINS__ is undefined(表明 SDK 未初始化,通常是早期的 React 渲染崩溃所致)。
  5. 验证你的 bundle 是否调用了 window.__HERMES_PLUGINS__.register(...),且使用的名称与 manifest.json:name 完全相同

插槽注册的组件未渲染。 sidebar 插槽仅在当前激活的主题具有 layoutVariant: cockpit 时才会渲染。其他插槽始终会渲染。如果你向一个没有匹配项的插槽注册,请在 registerSlot 内部添加 console.log 以确认插件包确实已运行。

插件后端路由返回 404。

  1. 确认清单文件中包含 "api": "plugin_api.py",且该路径指向 dashboard/ 内存在的一个文件。
  2. 重启 hermes dashboard —— 插件 API 路由仅在启动时挂载一次,不会在重新扫描时挂载。
  3. 检查 plugin_api.py 是否导出了模块级别的 router = APIRouter()。其他导出名称不会被识别。
  4. 跟踪查看 ~/.hermes/logs/errors.log 中是否有 Failed to load plugin <name> API routes 错误 —— 导入错误会记录在此处。

切换主题导致我的颜色覆盖失效。 colorOverrides 的作用域限定于当前激活的主题,并在切换主题时被清除 —— 这是设计使然。如果你希望覆盖设置持久生效,请将它们放在主题的 YAML 文件中,而不是实时切换器中。

主题的 customCSS 被截断。 每个主题的 customCSS 块上限为 32 KiB。可以将大型样式表拆分到多个主题中,或者切换到通过其 css 字段注入完整样式表的插件(无大小限制)。

我想在 PyPI 上发布插件。 Dashboard 插件是通过目录结构安装的,而非通过 pip 入口点。目前最干净的分发路径是让用户将 Git 仓库克隆到 ~/.hermes/plugins/ 中。目前尚未配置基于 pip 的 Dashboard 插件安装程序。