扩展仪表板
Hermes 网页仪表板(hermes dashboard)设计为无需 fork 代码库即可重新着色和扩展。系统暴露了三个层级:
- 主题 — YAML 文件,用于重绘仪表板的配色、排版、布局以及各组件的外观样式。将文件放入
~/.hermes/dashboard-themes/目录中;它会自动出现在主题切换器中。 - UI 插件 — 一个包含
manifest.json的目录,外加一个 JavaScript 打包文件,可注册一个标签页、替换内置页面、通过页面级插槽增强某个页面,或向命名的壳层插槽注入组件。 - 后端插件 — 位于该插件目录内的一个 Python 文件,暴露一个 FastAPI
router;路由会挂载在/api/plugins/<name>/下,并由插件的 UI 调用。
这三者均支持运行时即插即用:无需克隆仓库、无需 npm run build、无需修改仪表板源码。本页面是这三个功能的权威参考文档。
如果你只想使用仪表板,请参阅 Web Dashboard。如果你想为终端 CLI(而非网页仪表板)更换皮肤,请参阅 Skins & Themes — CLI 皮肤系统与仪表板主题无关。
主题与插件相互独立但可协同工作。一个主题可以单独存在(仅需一个 YAML 文件)。一个插件也可以单独存在(仅需一个标签页)。两者结合可构建完整的视觉重塑,包括自定义 HUD——内置的 strike-freedom-cockpit 演示正是如此实现。详见文末“主题 + 插件联合演示”一节。
目录
主题
主题是存储在 ~/.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() 从这个三色组合重新计算得出。
这就是全部入门流程:一个文件,两种颜色。其余内容均为可选优化。
配色、排版、布局
这三个区块是主题的核心。它们彼此独立 —— 可以只覆盖其中一项,其余保持不变。
配色(三层结构)
配色方案由三色层级加上一个暖光晕染色和一个噪点颗粒倍增器组成。仪表板的设计系统级联会通过 CSS color-mix(),从这一三色组推导出所有 shadcn 兼容的 token(如 card、popover、muted、border、primary、destructive、ring 等)。覆盖三个颜色将影响整个 UI。
| 键 | 描述 |
|---|---|
palette.background | 最深的画布颜色 —— 通常接近黑色。决定页面背景和卡片填充。 |
palette.midground | 主要文字与强调色。大多数 UI 外观元素使用此颜色(前景文字、按钮描边、焦点环)。 |
palette.foreground | 顶层高光色。默认主题将其设为白色且透明度为 0(不可见);希望在顶部有明亮强调的风格可提高其透明度。 |
palette.warmGlow | 作为 rgba(...) 使用的字符串,由 <Backdrop /> 用于生成晕染效果。 |
palette.noiseOpacity | 0–1.2 倍数,控制颗粒叠加层的强度。值越低 = 越柔和,越高 = 越粗糙。 |
每层均可接受 {hex: "#RRGGBB", alpha: 0.0–1.0} 或裸露的十六进制颜色字符串(透明度默认为 1.0)。
palette:
background:
hex: "#05091a"
alpha: 1.0
midground: "#d8f0ff" # bare hex, alpha = 1.0
foreground:
hex: "#ffffff"
alpha: 0 # invisible top layer
warmGlow: "rgba(255, 199, 55, 0.24)"
noiseOpacity: 0.7
排版
| 键 | 类型 | 描述 |
|---|---|---|
fontSans | string | 正文文本的 CSS 字体堆栈(应用于 html、body)。 |
fontMono | string | 代码块、<code>、.font-mono 工具类的 CSS 字体堆栈。 |
fontDisplay | string | 可选标题/展示字体堆栈。若未设置则回退至 fontSans。 |
fontUrl | string | 可选外部样式表 URL。在主题切换时作为 <link rel="stylesheet"> 注入 <head>。同一 URL 不会被重复注入。支持 Google Fonts、Bunny Fonts、自托管 @font-face 样式表 —— 任何可链接的资源均可。 |
baseSize | string | 根字体大小 —— 控制 rem 缩放比例。例如 "14px"、"16px"。 |
lineHeight | string | 默认行高。例如 "1.5"、"1.65"。 |
letterSpacing | string | 默认字间距。例如 "0"、"0.01em"、"-0.01em"。 |
typography:
fontSans: '"Orbitron", "Eurostile", "Impact", sans-serif'
fontMono: '"Share Tech Mono", ui-monospace, monospace'
fontDisplay: '"Orbitron", "Eurostile", sans-serif'
fontUrl: "https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700&family=Share+Tech+Mono&display=swap"
baseSize: "14px"
lineHeight: "1.5"
letterSpacing: "0.04em"
布局
| 键 | 值 | 描述 |
|---|---|---|
radius | 任意 CSS 长度("0"、"0.25rem"、"0.5rem"、"1rem" 等) | 圆角令牌。映射到 --radius 并级联至 --radius-sm/md/lg/xl —— 所有圆角元素同步变化。 |
density | compact | comfortable | spacious | 应用于 --spacing-mul CSS 变量的间距倍数。compact = 0.85×、comfortable = 1.0×(默认)、spacious = 1.2×。缩放 Tailwind 的基础间距,因此 padding、gap 和 space-between 实用类均按比例调整。 |
layout:
radius: "0"
density: compact
布局变体
layoutVariant 选择整体壳层布局。当该字段缺失时,默认为 "standard"。
| 变体 | 行为 |
|---|---|
standard | 单列布局,最大宽度 1600px(默认)。 |
cockpit | 左侧侧边栏轨道(260px)+ 主内容区。由插件通过 sidebar 插槽填充 —— 参见 壳层插槽。若无插件,轨道将显示占位符。 |
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 或 JavaScript 读取这些内容:
// In a plugin slot
const hero = getComputedStyle(document.documentElement)
.getPropertyValue("--theme-asset-hero").trim();
组件样式覆盖
componentStyles 用于重定义外壳组件的个别部分,而无需编写 CSS 选择器。每个桶中的条目会转化为 CSS 变量(--component-<bucket>-<kebab-property>),由外壳共享组件读取。因此,card: 的覆盖规则适用于所有 <Card>,header: 作用于应用栏等。
componentStyles:
card:
clipPath: "polygon(12px 0, 100% 0, 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%, 0 12px)"
background: "linear-gradient(180deg, rgba(10, 22, 52, 0.85), rgba(5, 9, 26, 0.92))"
boxShadow: "inset 0 0 0 1px rgba(64, 200, 255, 0.28)"
header:
background: "linear-gradient(180deg, rgba(16, 32, 72, 0.95), rgba(5, 9, 26, 0.9))"
tab:
clipPath: "polygon(6px 0, 100% 0, calc(100% - 6px) 100%, 0 100%)"
sidebar: {}
backdrop: {}
footer: {}
progress: {}
badge: {}
page: {}
支持的桶:card、header、footer、sidebar、tab、progress、badge、backdrop、page。
属性名称使用驼峰命名法(clipPath),并以连字符格式输出(clip-path)。值为普通的 CSS 字符串——任何 CSS 接受的内容均可(clip-path、border-image、background、box-shadow、animation,……)。
颜色覆盖
大多数主题无需此功能——三层调色板已推导出所有 shadcn 样式变量。当您希望获得调色板无法生成的特定强调色时(例如,柔和的破坏性红色用于柔和主题,或品牌专属的成功绿色),可使用 colorOverrides。
colorOverrides:
primary: "#ffce3a"
primaryForeground: "#05091a"
accent: "#3fd3ff"
ring: "#3fd3ff"
destructive: "#ff3a5e"
border: "rgba(64, 200, 255, 0.28)"
支持的键名:card、cardForeground、popover、popoverForeground、primary、primaryForeground、secondary、secondaryForeground、muted、mutedForeground、accent、accentForeground、destructive、destructiveForeground、success、warning、border、input、ring。
每个键名与 --color-<kebab> CSS 变量一一对应(例如:primaryForeground → --color-primary-foreground)。在此设置的任意键都会覆盖当前激活主题的调色板级联规则——切换到其他主题后,这些覆盖将被清除。
原生 customCSS
用于表达 componentStyles 无法涵盖的选择器级别外壳样式——伪元素、动画、媒体查询、主题范围内的覆盖——可将原始 CSS 写入 customCSS:
customCSS: |
/* Scanline overlay — only visible when cockpit variant is active. */
:root[data-layout-variant="cockpit"] body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 100;
background: repeating-linear-gradient(to bottom,
transparent 0px, transparent 2px,
rgba(64, 200, 255, 0.035) 3px, rgba(64, 200, 255, 0.035) 4px);
mix-blend-mode: screen;
}
该 CSS 将作为单个作用域化的 <style data-hermes-theme-css> 标签注入主题应用时,并在主题切换时清理。每主题上限 32 KiB。
内置主题
每个内置主题自带其调色板、排版和布局——切换时不仅改变颜色,还会产生可见的视觉变化。
| 主题 | 调色板 | 排版 | 布局 |
|---|---|---|---|
Hermes Teal(default) | 深青绿 + 乳白色 | 系统字体栈,15px | 0.5rem 圆角,舒适 |
Hermes Teal(大号)(default-large) | 同默认 | 系统字体栈,18px,行高 1.65 | 0.5rem 圆角,宽松 |
Midnight(midnight) | 深蓝紫色 | Inter + JetBrains Mono,14px | 0.75rem 圆角,舒适 |
Ember(ember) | 温暖深红 + 青铜色 | Spectral(衬线体)+ IBM Plex Mono,15px | 0.25rem 圆角,舒适 |
Mono(mono) | 灰度 | IBM Plex Sans + IBM Plex Mono,13px | 0 圆角,紧凑 |
Cyberpunk(cyberpunk) | 黑底霓虹绿 | 全局 Share Tech Mono,14px | 0 圆角,紧凑 |
Rosé(rose) | 粉色 + 象牙白 | Fraunces(衬线体)+ DM Mono,16px | 1rem 圆角,宽敞 |
引用 Google Fonts 的主题(除 Hermes Teal 外的所有主题)会在首次切换时按需加载样式表——第一次切换至该主题时,会向 <head> 注入一个 <link> 标签。
完整主题 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 包以及可选的 CSS 文件和 FastAPI 路由 Python 文件的目录。插件位于 ~/.hermes/plugins/<name>/ 中与其他 Hermes 插件并列——仪表盘扩展是该插件目录内一个 dashboard/ 子文件夹,因此一个插件可从单一安装同时扩展 CLI/网关和仪表盘。
插件不打包 React 或 UI 组件。它们使用暴露在 window.__HERMES_PLUGIN_SDK__ 上的 插件 SDK。这使得插件包体积极小(通常仅几 KB),并避免版本冲突。
快速入门 —— 您的第一个插件
创建目录结构:
mkdir -p ~/.hermes/plugins/my-plugin/dashboard/dist
编写清单文件:
// ~/.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 —— 无需构建步骤):
// ~/.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);
})();
刷新仪表盘——您的标签页将出现在导航栏中,位于 技能 之后。
如果您更喜欢 JSX,可使用任意打包工具(esbuild、Vite、rollup),将 React 设为外部依赖并输出 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/网关插件(参见插件页面)。dashboard/manifest.json+dashboard/dist/index.js—— 仪表盘 UI 插件。dashboard/plugin_api.py—— 仪表盘后端路由。
以上均非必需;仅包含您需要的层级。
清单参考
{
"name": "my-plugin",
"label": "My Plugin",
"description": "What this plugin does",
"icon": "Sparkles",
"version": "1.0.0",
"tab": {
"path": "/my-plugin",
"position": "after:skills",
"override": "/",
"hidden": false
},
"slots": ["sidebar", "header-left"],
"entry": "dist/index.js",
"css": "dist/style.css",
"api": "plugin_api.py"
}
| 字段 | 是否必需 | 说明 |
|---|---|---|
name | 是 | 唯一的插件标识符。小写,允许使用连字符。用于 URL 和注册。 |
label | 是 | 导航标签中显示的名称。 |
description | 否 | 简短描述(在仪表盘管理界面中显示)。 |
icon | 否 | Lucide 图标名称。默认为 Puzzle。未知名称将回退至 Puzzle。 |
version | 否 | Semver 版本字符串。默认为 0.0.0。 |
tab.path | 是 | 标签的 URL 路径(例如 /my-plugin)。 |
tab.position | 否 | 插入标签的位置。"end"(默认)、"after:<path>" 或 "before:<path>"——冒号后的值为目标标签的路径片段(无前导斜杠)。示例:"after:skills"、"before:config"。 |
tab.override | 否 | 设置为内置路由路径("/"、"/sessions"、"/config",……)以替换该页面,而非添加新标签。详见 替换内置页面。 |
tab.hidden | 否 | 当为 true 时,注册组件及任何插槽但不在导航栏添加标签。用于仅插槽插件。详见 仅插槽插件。 |
slots | 否 | 此插件填充的命名外壳插槽。仅作文档辅助——实际注册由 JS 包通过 registerSlot() 完成。在此列出插槽有助于提升发现界面的信息量。 |
entry | 是 | JS 包相对于 dashboard/ 的路径。默认为 dist/index.js。 |
css | 否 | 要注入为 <link> 标签的 CSS 文件路径。 |
api | 否 | 包含 FastAPI 路由的 Python 文件路径。挂载于 /api/plugins/<name>/。 |
可用图标
插件使用 Lucide 图标名称。仪表盘根据名称映射图标——未知名称会静默回退至 Puzzle。当前已映射:Activity, BarChart3, Clock, Code, Database, Eye, FileText, Globe, Heart, KeyRound, MessageSquare, Package, Puzzle, Settings, Shield, Sparkles, Star, Terminal, Wrench, Zap。
需要不同的图标?请提交 PR 到 web/src/App.tsx 的 ICON_MAP —— 纯增量变更。
插件 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 插槽(Slots)
插槽允许插件将组件注入应用外壳的命名位置——如驾驶舱侧边栏、顶部栏、底部栏或覆盖层——而无需占据整个标签页。多个插件可填充同一插槽;它们按注册顺序堆叠渲染。
在插件包内部进行注册:
window.__HERMES_PLUGINS__.registerSlot("my-plugin", "sidebar", MySidebar);
window.__HERMES_PLUGINS__.registerSlot("my-plugin", "header-left", MyCrest);
插槽目录
全局插槽(在应用界面任意位置渲染):
| 插槽 | 位置 |
|---|---|
backdrop | 在 <Backdrop /> 层栈内部,位于噪声层之上。 |
header-left | 顶部栏中 Hermes 品牌之前。 |
header-right | 顶部栏中的主题/语言切换器之前。 |
header-banner | 导航下方的全宽条带。 |
sidebar | 驾驶舱侧边栏轨道 —— 仅当 layoutVariant === "cockpit" 时渲染。 |
pre-main | 路由出口上方(位于 <main> 内部)。 |
post-main | 路由出口下方(位于 <main> 内部)。 |
footer-left | 底部单元内容(替换默认内容)。 |
footer-right | 底部单元内容(替换默认内容)。 |
overlay | 固定位置层,位于所有内容之上。适用于实现 chrome 效果(如扫描线、暗角),这些效果 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(密钥)页面的顶部 / 底部。 |
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 包中的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"
}
尽管包内仍会调用 register() 传入占位组件(以防有人直接访问 URL),随后再通过 registerSlot() 执行实际工作。
后端 API 路由
插件可通过在清单中设置 api 来注册 FastAPI 路由。创建文件并导出一个 router:
# ~/.hermes/plugins/my-plugin/dashboard/plugin_api.py
from fastapi import APIRouter
router = APIRouter()
@router.get("/data")
async def get_data():
return {"items": ["one", "two", "three"]}
@router.post("/action")
async def do_action(body: dict):
return {"ok": True, "received": body}
路由将挂载于 /api/plugins/<name>/ 下,因此上述代码生成如下路由:
GET /api/plugins/my-plugin/dataPOST /api/plugins/my-plugin/action
插件 API 路由绕过会话令牌认证,因为仪表板服务器默认绑定到 localhost。若你运行不受信任的插件,请勿将仪表板暴露在公网接口上,否则其路由也将可被访问。
访问 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 文件,并在清单(manifest)中引用它:
```json
{
"css": "dist/style.css"
}
该文件会在插件加载时作为 ``<link>`` 标签注入。请使用特定的类名以避免与仪表板样式发生冲突,并通过引用仪表板的 CSS 变量来保持主题感知:
```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``)。引用这些变量后,您的插件将自动随当前主题进行重着色。
---
### 插件发现与重新加载 \{#plugin-discovery--reload}
仪表板会扫描以下三个目录中的 ``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
```bash
# Force a rescan without restart
curl http://127.0.0.1:9119/api/dashboard/plugins/rescan
或重启 ``hermes dashboard``。
#### 插件加载生命周期
1. 仪表板启动。``main.tsx`` 在 ``window.__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 中异常),仪表板将在浏览器控制台记录警告并继续运行而不包含该插件。
---
## 主题 + 插件联合演示 \{#combined-theme--plugin-demo}
[``strike-freedom-cockpit``](https://github.com/NousResearch/hermes-example-plugins/tree/main/strike-freedom-cockpit) 插件(配套仓库 ``hermes-example-plugins``)是一个完整的主题重制演示。它结合一个主题 YAML 和一个仅含槽位的插件,无需分叉仪表板即可实现机舱风格的 HUD。
**它展示了:**
- 一个完整主题,包含调色板、排版、``fontUrl``、``layoutVariant: cockpit``、``assets``、``componentStyles``(带凹角卡片、渐变背景)、``colorOverrides`` 和 ``customCSS``(扫描线叠加)。
- 一个仅槽位插件(``tab.hidden: true``),注册到三个槽位:
- ``sidebar`` — 带实时遥测条的 MS-STATUS 面板,由 ``SDK.api.getStatus()`` 驱动。
- ``header-left`` — 读取活动主题中 ``--theme-asset-crest`` 的派系徽章。
- ``footer-right`` — 替换默认组织行的自定义标语。
- 插件通过 CSS 变量读取主题提供的艺术素材,因此切换主题时英雄图/徽章自动变化,无需修改插件代码。
**安装方法:**
```bash
```bash
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 变量、防范旧版仪表板不支持槽位的情况,以及如何从一个包中注册三个槽位。
---
## API 参考 \{#api-reference}
### 主题端点
| 端点 | 方法 | 描述 |
|------|------|------|
| ``/api/dashboard/themes`` | GET | 列出可用主题及当前激活名称。内置主题返回 ``{name, label, description}``;用户主题还包含一个 ``definition`` 字段,其中包含完整标准化的主题对象。 |
| ``/api/dashboard/theme`` | PUT | 设置激活主题。请求体:``{"name": "midnight"}``。持久化至 ``config.yaml`` 下的 ``dashboard.theme``。 |
### 插件端点
| 端点 | 方法 | 描述 |
|------|------|------|
| ``/api/dashboard/plugins`` | GET | 列出已发现的插件(含清单,不含内部字段)。 |
| ``/api/dashboard/plugins/rescan`` | GET | 强制重新扫描插件目录,无需重启。 |
| ``/dashboard-plugins/<name>/<path>`` | GET | 从插件的 ``dashboard/`` 目录提供静态资源。路径遍历被阻止。 |
| ``/api/plugins/<name>/*`` | * | 插件注册的后端路由。 |
### SDK 在 ``window`` 上
| 全局 | 类型 | 提供者 |
|------|------|--------|
| ``window.__HERMES_PLUGIN_SDK__`` | object | ``registry.ts`` — React、钩子、UI 组件、API 客户端、工具函数。 |
| ``window.__HERMES_PLUGINS__.register(name, Component)`` | function | 注册插件主组件。 |
| ``window.__HERMES_PLUGINS__.registerSlot(name, slot, Component)`` | function | 注册到命名壳槽。 |
---
## 故障排除 \{#troubleshooting}
**我的主题未出现在选择器中。**
检查文件是否位于 ``~/.hermes/dashboard-themes/`` 并以 ``.yaml`` 或 ``.yml`` 结尾。刷新页面。运行 ``curl http://127.0.0.1:9119/api/dashboard/themes` — your theme should be in the response. If the YAML has a parse error, the dashboard logs to `errors.log` under `~/.hermes/logs/`。
**我的插件标签未显示。**
1. 检查清单是否位于 ``~/.hermes/plugins/<name>/dashboard/manifest.json``(注意 ``dashboard/`` 子目录)。
2. 使用 `curl http://127.0.0.1:9119/api/dashboard/plugins/rescan`` 强制重新发现。
3. 打开浏览器开发者工具 → 网络 —— 确认 ``manifest.json``、``index.js`` 和任何 CSS 均无 404 错误。
4. 打开浏览器开发者工具 → 控制台 —— 查看 IIFE 或 ``window.__HERMES_PLUGINS__ is undefined`` 是否报错(表明 SDK 未初始化,通常因早期 React 渲染崩溃导致)。
5. 确保你的包调用了 ``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 发布插件。**
仪表板插件通过目录结构安装,而非 pip 入口点。目前最清晰的分发路径是让用户将 git 仓库克隆至 ``~/.hermes/plugins/``。尚未配置基于 pip 的仪表板插件安装器。