Extending the Dashboard(扩展仪表盘)

Hermes Web 仪表盘(hermes dashboard)设计为无需 fork 代码库即可重新换肤和扩展。有三个可扩展的层面:

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

三者均为运行时即插即用:无需克隆仓库、无需 npm run build、无需修改仪表盘源码。本页是所有三者的规范参考。

如果您只想使用仪表盘,请参见 Web Dashboard。如果您想重新换肤终端 CLI(而非 Web 仪表盘),请参见 Skins & Themes——CLI 皮肤系统与仪表盘主题完全独立。


主题

主题是一个 YAML 文件,放在 ~/.hermes/dashboard-themes/<name>.yaml 中。它无需对应插件——仅仅是一个文件。仪表盘在启动时扫描该目录,并在主题切换器中显示每个主题。

快速开始

mkdir -p ~/.hermes/dashboard-themes

创建 ~/.hermes/dashboard-themes/midnight.yaml

# ~/.hermes/dashboard-themes/midnight.yaml
name: "Midnight"
label: "Midnight"
description: "A dark, low-contrast theme"
 
palette:
  background: "#0a1628"
  foreground: "#c8d6e5"
  card: "#111d35"
  cardForeground: "#e0e7ff"
  popover: "#111d35"
  popoverForeground: "#e0e7ff"
  primary: "#4f8cff"
  primaryForeground: "#ffffff"
  secondary: "#1e2d50"
  secondaryForeground: "#94a3b8"
  muted: "#1a2744"
  mutedForeground: "#7f8ea3"
  accent: "#2a3d6b"
  accentForeground: "#e0e7ff"
  border: "#233458"
  input: "#1e2d50"
 
# ...

刷新仪表盘。点击顶部栏中的调色板图标——您会看到 Midnight 与内置主题并列。选择后,它会立即重新涂抹仪表盘,并将选择持久化到 config.yaml 中的 dashboard.theme

示例:完整主题

name: "Strike Freedom"
label: "Strike Freedom"
description: "Cockpit-style HUD for power users"
 
palette:
  background: "#0a0f1e"
  foreground: "#d4d4e8"
  card: "#121827"
  cardForeground: "#e2e2f0"
  popover: "#121827"
  popoverForeground: "#e2e2f0"
  primary: "#f59e0b"
  primaryForeground: "#0a0f1e"
  secondary: "#1e293b"
  secondaryForeground: "#94a3b8"
  muted: "#1a2332"
  mutedForeground: "#7e8b9e"
  accent: "#f59e0b"
  accentForeground: "#0a0f1e"
  destructive: "#ef4444"
  border: "#1e293b"
  input: "#1e293b"
  ring: "#f59e0b"
 
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.yaml 中的 dashboard.theme,刷新后恢复。


插件

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

插件不捆绑 React 或 UI 组件。它们使用在 window.__HERMES_PLUGIN_SDK__ 上暴露的 Plugin 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);
})();

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

:::tip 跳过 React.createElement 如果您更喜欢 JSX,可以使用任何打包工具(esbuild、Vite、rollup),将 React 设为外部依赖并输出 IIFE。唯一硬性要求是最终文件是一个可通过 <script> 加载的单个 JS 文件。React 从不被捆绑;它来自 SDK.React。 :::

目录布局

~/.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——仪表盘后端路由

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

清单参考

{
  "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() 进行。在此列出插槽使发现界面更丰富。
entryJS 包相对于 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__ 上。插件绝不应直接导入 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 客户端 + 原始取回器
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                  // 多语言插件的国际化 hook

调用插件后端

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

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

调用内置 Hermes 端点

// Agent 状态
SDK.api.getStatus().then((s) => console.log("Version:", s.version));
 
// 最近的会话
SDK.api.getSessions(10).then((resp) => console.log(resp.sessions.length));

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

外壳插槽(Shell slots)

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

从插件包内注册:

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 单独无法实现的效果有用(扫描线、暗角)

页面作用域插槽(仅在命名的内置页面上渲染——使用这些插槽向现有页面注入小部件、卡片或工具栏,无需覆盖整个路由):

插槽渲染位置
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 结合使用,如果您的插件仅增强现有页面且不需要自己的侧边栏标签页。

外壳仅为上述插槽渲染 <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)。参见外壳插槽 → 插槽目录中的完整目录。

最小示例——将横幅固定到 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 清单字段仅作为文档辅助。实际绑定在 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/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 令牌作为 --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.tsxwindow.__HERMES_PLUGIN_SDK__ 上暴露 SDK,在 window.__HERMES_PLUGINS__ 上暴露注册表。
  2. App.tsx 调用 usePlugins() → 获取 GET /api/dashboard/plugins
  3. 对每个清单:注入 CSS <link>(如果声明),然后通过 <script> 标签加载 JS 包。
  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,无需 fork 仪表盘。

它演示了:

  • 使用调色板、排版、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
 
# 主题
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.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注册到命名外壳插槽

故障排除

我的主题没有出现在选择器中。 检查文件是否在 ~/.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. 验证您的包是否使用与 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 入口点。目前最干净的发布路径是一个 git 仓库,用户克隆到 ~/.hermes/plugins/。基于 pip 的仪表盘插件安装程序尚未接入。