Chuyển tới nội dung chính

Mở rộng Bảng điều khiển

#Mở rộng Bảng điều khiển

Bảng điều khiển web Hermes (bảng điều khiển Hermes) được xây dựng để thay đổi giao diện và mở rộng mà không cần phân tách cơ sở mã. Ba lớp được phơi bày:

  1. Chủ đề — Tệp YAML sơn lại bảng màu, kiểu chữ, bố cục và chrome theo từng thành phần của trang tổng quan. Thả một tập tin vào ~/.hermes/dashboard-themes/; nó xuất hiện trong trình chuyển đổi chủ đề.
  2. plugin giao diện người dùng — một thư mục có manifest.json + gói JavaScript đăng ký một tab, thay thế một trang tích hợp sẵn, bổ sung một trang thông qua các vị trí trong phạm vi trang hoặc đưa các thành phần vào các vị trí shell được đặt tên.
  3. Các plugin phụ trợ — một tệp Python bên trong thư mục plugin đó hiển thị bộ định tuyến FastAPI; các tuyến được gắn trong /api/plugins/<name>/ và được gọi từ giao diện người dùng của plugin.

Cả ba đều dùng trong thời gian chạy: không sao chép repo, không npm run build, không vá nguồn bảng điều khiển. Trang này là tài liệu tham khảo chuẩn cho cả ba.

Nếu bạn chỉ muốn sử dụng trang tổng quan, hãy xem Trang tổng quan web. Nếu bạn muốn thay đổi giao diện của CLI thiết bị đầu cuối (không phải trang tổng quan web), hãy xem Giao diện & Chủ đề — hệ thống giao diện CLI không liên quan đến chủ đề trang tổng quan.

Cách các mảnh ghép lại

Chủ đề và plugin độc lập nhưng có tác dụng hiệp đồng. Một chủ đề có thể độc lập (chỉ là một tệp YAML). Một plugin có thể độc lập (chỉ một tab). Chúng cùng nhau cho phép bạn xây dựng một giao diện trực quan hoàn chỉnh với HUD tùy chỉnh — bản demo tấn công-tự do-buồng lái đi kèm thực hiện chính xác điều đó. Xem Bản demo chủ đề + plugin kết hợp.


Mục lục


Chủ đề

Chủ đề là các tệp YAML được lưu trữ trong ~/.hermes/dashboard-themes/. Tên tệp không quan trọng (trường name: ​​của chủ đề là tên hệ thống sử dụng), nhưng quy ước là <name>.yaml. Mọi trường đều là tùy chọn — các khóa bị thiếu sẽ quay trở lại chủ đề mặc định được tích hợp sẵn, do đó, chủ đề có thể chỉ có một màu.

Bắt đầu nhanh — chủ đề đầu tiên của bạn

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

bảng màu:
nền: "#000000"
ở giữa: "#ff00ff"

Làm mới bảng điều khiển. Nhấp vào biểu tượng bảng màu trong tiêu đề và chọn Neon. Nền chuyển sang màu đen, văn bản và các điểm nhấn chuyển sang màu đỏ tươi và mọi màu dẫn xuất (thẻ, đường viền, tắt tiếng, vòng, v.v.) được tính toán lại từ bộ ba 2 màu đó thông qua color-mix() trong CSS.

Đó là toàn bộ quá trình triển khai: một tệp, hai màu. Mọi thứ bên dưới là sàng lọc tùy chọn.

Bảng màu, kiểu chữ, bố cục

Ba khối này là trung tâm của một chủ đề. Mỗi cái đều độc lập - ghi đè một cái, để lại những cái khác.

Bảng màu (3 lớp)

Bảng màu là bộ ba lớp màu cộng với màu họa tiết rực rỡ ấm áp và hệ số nhiễu hạt. Tầng hệ thống thiết kế của bảng thông tin lấy ra mọi mã thông báo tương thích với shadcn (thẻ, cửa sổ bật lên, tắt tiếng, đường viền, chính, phá hoại, đổ chuông, v.v.) từ bộ ba này thông qua CSS color-mix(). Ghi đè ba màu sẽ xếp tầng vào toàn bộ giao diện người dùng.

KeyDescription
palette.backgroundDeepest canvas color — typically near-black. Drives the page background and card fill.
palette.midgroundPrimary text and accent. Most UI chrome reads this (foreground text, button outlines, focus rings).
palette.foregroundTop-layer highlight. The default theme sets this to white at alpha 0 (invisible); themes that want a bright accent on top can raise its alpha.
palette.warmGlowrgba(...) string used as the vignette color by <Backdrop />.
palette.noiseOpacity0–1.2 multiplier on the grain overlay. Lower = softer, higher = grittier.

Mỗi lớp chấp nhận {hex: "#RRGGBB", alpha: 0,0–1,0} hoặc chuỗi hex trần (alpha mặc định là 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

Kiểu chữ

KeyTypeDescription
fontSansstringCSS font-family stack for body copy (applied to html, body).
fontMonostringCSS font-family stack for code blocks, <code>, .font-mono utilities.
fontDisplaystringOptional heading/display stack. Falls back to fontSans.
fontUrlstringOptional external stylesheet URL. Injected as <link rel="stylesheet"> in <head> on theme switch. Same URL is never injected twice. Works with Google Fonts, Bunny Fonts, self-hosted @font-face sheets — anything linkable.
baseSizestringRoot font size — controls the rem scale. E.g. "14px", "16px".
lineHeightstringDefault line-height. E.g. "1.5", "1.65".
letterSpacingstringDefault letter-spacing. E.g. "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"

Cách trình bày

KeyValuesDescription
radiusany CSS length ("0", "0.25rem", "0.5rem", "1rem", ...)Corner-radius token. Maps to --radius and cascades into --radius-sm/md/lg/xl — every rounded element shifts together.
densitycompact | comfortable | spaciousSpacing multiplier applied as the --spacing-mul CSS var. compact = 0.85×, comfortable = 1.0× (default), spacious = 1.2×. Scales Tailwind's base spacing, so padding, gap, and space-between utilities all shift proportionally.
layout:
radius: "0"
density: compact

Các biến thể của bố cục

layoutVariant chọn bố cục shell tổng thể. Mặc định là "tiêu chuẩn" khi vắng mặt.

VariantBehaviour
standardSingle column, 1600px max-width (default).
cockpitLeft sidebar rail (260px) + main content. Populated by plugins via the sidebar slot — see Shell slots. Without a plugin the rail shows a placeholder.
tiledDrops the max-width clamp so pages can use the full viewport width.
layoutVariant: cockpit

Biến thể hiện tại được hiển thị dưới dạng document.documentElement.dataset.layoutVariant, vì vậy CSS thô trong customCSS có thể nhắm mục tiêu nó thông qua :root[data-layout-variant="cockpit"] ....

Nội dung chủ đề (hình ảnh dưới dạng CSS)

Gửi URL tác phẩm nghệ thuật theo chủ đề. Mỗi vị trí được đặt tên sẽ trở thành một CSS var (--theme-asset-<name>) mà shell tích hợp và bất kỳ plugin nào đều có thể đọc được. Khe bg được tự động nối vào phông nền; các khe cắm khác hướng về phía plugin.

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

Giá trị chấp nhận:

  • URL trống — được gói tự động trong url(...).
  • Các biểu thức url(...), tuyến tính-gradient(...), radial-gradient(...) được gói sẵn — được sử dụng nguyên trạng.
  • "none" — từ chối rõ ràng.

Mọi nội dung cũng được phát ra dưới dạng --theme-asset-<name>-raw (URL chưa được gói), trong trường hợp plugin cần chuyển nó tới <img src> thay vì background-image.

Các plugin đọc chúng bằng CSS hoặc JS đơn giản:

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

Ghi đè thành phần chrome

ComponentStyles định kiểu lại các thành phần shell riêng lẻ mà không cần ghi bộ chọn CSS. Các mục nhập của mỗi nhóm trở thành các vars CSS (--comComponent-<bucket>-<kebab-property>) mà các thành phần chia sẻ của shell đọc. Vì vậy, phần ghi đè card: áp dụng cho mọi <Card>, header: đối với thanh ứng dụng, v.v.

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

Các nhóm được hỗ trợ: card, header, footer, sidebar, tab, progress, huy hiệu, backdrop, page.

Tên thuộc tính sử dụng CamelCase (clipPath) và được phát ra dưới dạng kebab (clip-path). Giá trị là các chuỗi CSS đơn giản — mọi thứ CSS chấp nhận (clip-path, border-image, background, box-shadow, animation, ...).

Ghi đè màu

Hầu hết các chủ đề sẽ không cần điều này — bảng màu 3 lớp lấy ra mọi mã thông báo shadcn. Sử dụng colorOverrides khi bạn muốn có một điểm nhấn cụ thể mà nguồn gốc sẽ không tạo ra (màu đỏ hủy diệt nhẹ nhàng hơn cho chủ đề màu phấn, màu xanh lá cây thành công cụ thể cho một thương hiệu).

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

Các khóa được hỗ trợ: card, cardForeground, popover, popoverForeground, primary, primaryForeground, secondary, secondaryForeground, muted, mutedForeground, accent, accentForeground, destroy, structiveForeground, success, cảnh báo, viền, đầu vào, đổ chuông.

Mỗi khóa ánh xạ 1:1 tới biến thể CSS --color-<kebab> (ví dụ: primaryForeground--color-primary-foreground). Bất kỳ khóa nào được đặt ở đây sẽ giành chiến thắng trong tầng bảng màu chỉ dành cho chủ đề đang hoạt động — việc chuyển sang chủ đề khác sẽ xóa phần ghi đè.

customCSS thô

Đối với chrome cấp bộ chọn mà comComponentStyles không thể biểu thị — phần tử giả, hoạt ảnh, truy vấn phương tiện, ghi đè theo phạm vi chủ đề — thả CSS thô vào 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 được đưa vào dưới dạng một thẻ <style data-hermes-theme-css> trong phạm vi duy nhất khi áp dụng chủ đề và được dọn dẹp khi chuyển đổi chủ đề. Giới hạn ở mức 32 KiB mỗi chủ đề.

Chủ đề tích hợp

Mỗi tính năng tích hợp đều có bảng màu, kiểu chữ và bố cục riêng — việc chuyển đổi sẽ tạo ra những thay đổi rõ ràng ngoài màu sắc.

ThemePaletteTypographyLayout
Hermes Teal (default)Dark teal + creamSystem stack, 15px0.5rem radius, comfortable
Hermes Teal (Large) (default-large)Same as defaultSystem stack, 18px, line-height 1.650.5rem radius, spacious
Midnight (midnight)Deep blue-violetInter + JetBrains Mono, 14px0.75rem radius, comfortable
Ember (ember)Warm crimson + bronzeSpectral (serif) + IBM Plex Mono, 15px0.25rem radius, comfortable
Mono (mono)GrayscaleIBM Plex Sans + IBM Plex Mono, 13px0 radius, compact
Cyberpunk (cyberpunk)Neon green on blackShare Tech Mono everywhere, 14px0 radius, compact
Rosé (rose)Pink + ivoryFraunces (serif) + DM Mono, 16px1rem radius, spacious

Các chủ đề tham chiếu đến Google Fonts (tất cả ngoại trừ Hermes Teal) tải biểu định kiểu theo yêu cầu — lần đầu tiên bạn chuyển sang chúng, thẻ <link> sẽ được đưa vào <head>.

Tham khảo đầy đủ chủ đề YAML

Mọi núm trong một tệp — sao chép và cắt bớt những gì bạn không cần:

# ~/.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 */

Làm mới bảng điều khiển sau khi tạo tệp. Chuyển chủ đề trực tiếp từ thanh tiêu đề — nhấp vào biểu tượng bảng màu. Lựa chọn vẫn tồn tại ở config.yaml trong dashboard.theme và được khôi phục khi tải lại.


Plugin

Plugin bảng điều khiển là một thư mục có manifest.json, gói JS dựng sẵn và tùy chọn là tệp CSS và tệp Python có các tuyến FastAPI. Các plugin nằm cạnh các plugin Hermes khác trong ~/.hermes/plugins/<name>/ — tiện ích mở rộng bảng thông tin là một thư mục con dashboard/ bên trong thư mục plugin đó, vì vậy một plugin có thể mở rộng cả CLI/gateway và bảng thông tin từ một lần cài đặt.

Các plugin không gói các thành phần React hoặc UI. Họ sử dụng SDK plugin hiển thị trên window.__HERMES_PLUGIN_SDK__. Điều này giữ cho các gói plugin có kích thước nhỏ (thường là vài KB) và tránh xung đột phiên bản.

Bắt đầu nhanh — plugin đầu tiên của bạn

Tạo cấu trúc thư mục:

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

Viết bảng kê khai:

// ~/.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"
}

Viết gói JS (IIFE đơn giản - không cần bước xây dựng):

// ~/.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);
})();

Làm mới trang tổng quan — tab của bạn sẽ xuất hiện trong thanh điều hướng, sau Kỹ năng.

Bỏ qua React.createElement

Nếu bạn thích JSX, hãy sử dụng bất kỳ trình đóng gói nào (esbuild, Vite, rollup) với React làm đầu ra bên ngoài và IIFE. Yêu cầu khó khăn duy nhất là tệp cuối cùng phải là một tệp JS duy nhất có thể tải qua <script>. React không bao giờ được đóng gói; nó đến từ SDK.React.

Bố cục thư mục

~/.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)

Một thư mục plugin có thể chứa ba phần mở rộng trực giao:

  • plugin.yaml + __init__.py — plugin CLI/gateway (xem trang plugin).
  • dashboard/manifest.json + dashboard/dist/index.js — plugin giao diện người dùng bảng điều khiển.
  • dashboard/plugin_api.py — các tuyến phụ trợ của bảng điều khiển.

Không ai trong số họ được yêu cầu; chỉ bao gồm các lớp bạn cần.

Tài liệu tham khảo kê khai

{
"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"
}
FieldRequiredDescription
nameYesUnique plugin identifier. Lowercase, hyphens ok. Used in URLs and registration.
labelYesDisplay name shown in the nav tab.
descriptionNoShort description (shown in dashboard admin surfaces).
iconNoLucide icon name. Defaults to Puzzle. Unknown names fall back to Puzzle.
versionNoSemver string. Defaults to 0.0.0.
tab.pathYesURL path for the tab (e.g. /my-plugin).
tab.positionNoWhere to insert the tab. "end" (default), "after:<path>", or "before:<path>" — value after the colon is the path segment of the target tab (no leading slash). Examples: "after:skills", "before:config".
tab.overrideNoSet to a built-in route path ("/", "/sessions", "/config", ...) to replace that page instead of adding a new tab. See Replacing built-in pages.
tab.hiddenNoWhen true, register the component and any slots without adding a tab to the nav. Used by slot-only plugins. See Slot-only plugins.
slotsNoNamed shell slots this plugin populates. Documentation aid only — actual registration happens from the JS bundle via registerSlot(). Listing slots here makes discovery surfaces more informative.
entryYesPath to the JS bundle relative to dashboard/. Defaults to dist/index.js.
cssNoPath to a CSS file to inject as a <link> tag.
apiNoPath to a Python file with FastAPI routes. Mounted at /api/plugins/<name>/.

Biểu tượng có sẵn

Các plugin sử dụng tên biểu tượng Lucide. Trang tổng quan ánh xạ những tên này theo tên - những cái tên không xác định âm thầm quay trở lại Puzzle.

Hiện được ánh xạ: Activity, BarChart3, Clock, Code, Database, Eye, FileText, Globe, Heart, KeyRound, MessageSquare, Package, Puzzle, Settings, Shield, Sparkles, Star, Thiết bị đầu cuối, Cờ lê, Zap.

Cần một biểu tượng khác? Mở PR tới ICON_MAP của web/src/App.tsx — thay đổi phụ gia thuần túy.

SDK plugin

Mọi thứ mà một plugin cần đều có trên window.__HERMES_PLUGIN_SDK__. Các plugin không bao giờ nên nhập React trực tiếp.

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

// Móc
SDK.useI18n // hook i18n cho plugin đa ngôn ngữ

Gọi phần phụ trợ của plugin của bạn

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

fetchJSON chèn mã thông báo xác thực phiên, hiển thị các lỗi dưới dạng ngoại lệ được gửi và tự động phân tích cú pháp JSON.

Gọi các điểm cuối Hermes tích hợp

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

// Phiên gần đây
SDK.api.getSessions(10).then((resp) => console.log(resp.sessions.length));

Xem Trang tổng quan web → API REST để biết danh sách đầy đủ.

Khe vỏ

Các vị trí cho phép plugin đưa các thành phần vào các vị trí được đặt tên của vỏ ứng dụng — thanh bên buồng lái, đầu trang, chân trang, lớp phủ — mà không yêu cầu toàn bộ tab. Nhiều plugin có thể nằm trong cùng một vị trí; chúng hiển thị xếp chồng lên nhau theo thứ tự đăng ký.

Đăng ký từ bên trong gói plugin:

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

Danh mục máy đánh bạc

Các khe toàn Shell (hiển thị ở mọi nơi trong ứng dụng chrome):

SlotLocation
backdropInside the <Backdrop /> layer stack, above the noise layer.
header-leftBefore the Hermes brand in the top bar.
header-rightBefore the theme/language switchers in the top bar.
header-bannerFull-width strip below the nav.
sidebarCockpit sidebar rail — only rendered when layoutVariant === "cockpit".
pre-mainAbove the route outlet (inside <main>).
post-mainBelow the route outlet (inside <main>).
footer-leftFooter cell content (replaces default).
footer-rightFooter cell content (replaces default).
overlayFixed-position layer above everything else. Useful for chrome (scanlines, vignettes) customCSS can't achieve alone.

Vị trí trong phạm vi trang (chỉ hiển thị trên trang tích hợp có tên — sử dụng những vị trí này để đưa tiện ích, thẻ hoặc thanh công cụ vào trang hiện có mà không ghi đè toàn bộ tuyến):

SlotWhere it renders
sessions:top / sessions:bottomTop / bottom of the /sessions page.
analytics:top / analytics:bottomTop / bottom of the /analytics page.
logs:top / logs:bottomTop (above filter toolbar) / bottom (below log viewer) of /logs.
cron:top / cron:bottomTop / bottom of the /cron page.
skills:top / skills:bottomTop / bottom of the /skills page.
config:top / config:bottomTop / bottom of the /config page.
env:top / env:bottomTop / bottom of the /env (Keys) page.
docs:top / docs:bottomTop (above the iframe) / bottom of /docs.
chat:top / chat:bottomTop / bottom of /chat (only active when embedded chat is enabled).

Ví dụ - thêm thẻ biểu ngữ vào đầu trang Phiên:

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);

Kết hợp các vị trí trong phạm vi trang với tab.hidden: true nếu plugin của bạn chỉ tăng cường các trang hiện có và không cần tab thanh bên của riêng nó.

Shell chỉ hiển thị <PluginSlot name="..." /> cho các vị trí ở trên. Tên bổ sung được cơ quan đăng ký chấp nhận cho giao diện người dùng plugin lồng nhau — một plugin có thể hiển thị các vị trí riêng của nó thông qua SDK.comComponents.PluginSlot.

####Đăng ký lại và HMR

Nếu cùng một cặp (plugin, slot) được đăng ký hai lần, lệnh gọi sau sẽ thay thế lệnh gọi trước đó - điều này khớp với cách React HMR mong đợi việc gắn lại plugin sẽ hoạt động.

Thay thế các trang cài sẵn (tab.override)

Việc đặt tab.override thành đường dẫn định tuyến tích hợp sẽ khiến thành phần của plugin thay thế trang đó thay vì thêm tab mới. Hữu ích khi một chủ đề muốn có một trang chủ tùy chỉnh (/) nhưng muốn giữ nguyên phần còn lại của trang tổng quan.

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

Với bộ ghi đè:

  • Thành phần trang gốc tại / bị xóa khỏi bộ định tuyến.
  • Thay vào đó, plugin của bạn hiển thị tại /.
  • Không có tab điều hướng nào được thêm vào cho tab.path (phần ghi đè là điểm).

Chỉ một plugin có thể ghi đè một đường dẫn nhất định. Nếu hai plugin yêu cầu ghi đè giống nhau, thì plugin đầu tiên sẽ thắng và plugin thứ hai sẽ bị bỏ qua với cảnh báo ở chế độ nhà phát triển.

Thay vào đó, nếu bạn chỉ cần thêm thẻ hoặc thanh công cụ vào một trang hiện có mà không tiếp quản trang đó, hãy sử dụng các vị trí có phạm vi trang.

Tăng cường các trang tích hợp (các vị trí trong phạm vi trang)

Việc thay thế hoàn toàn thông qua tab.override rất nặng — plugin của bạn hiện sở hữu toàn bộ trang, bao gồm mọi bản cập nhật trong tương lai mà chúng tôi gửi tới trang đó. Hầu hết bạn chỉ muốn thêm biểu ngữ, thẻ hoặc thanh công cụ vào trang hiện có. Đó chính là mục đích của các vị trí trong phạm vi trang.

Mỗi trang tích hợp hiển thị các vị trí <page>:top<page>:bottom được hiển thị ở đầu và cuối khu vực nội dung của nó. Plugin của bạn sẽ điền một plugin bằng cách gọi registerSlot() — trang tích hợp sẵn tiếp tục hoạt động bình thường và thành phần của bạn hiển thị cùng với nó.

Các vị trí có sẵn: sessions:*, analytics:*, logs:*, cron:*, skills:*, config:*, env:*, docs:*, chat:* (mỗi vị trí có :top:bottom). Xem danh mục đầy đủ trong Shell slot → Slot catalogue.

Ví dụ tối thiểu - ghim biểu ngữ lên đầu trang Phiên:

// ~/.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; });

// Công việc thực sự.
window.__HERMES_PLUGINS__.registerSlot("session-notes", "sessions:top", Banner);
})();

Những điểm chính:

  • tab.hidden: true giữ plugin nằm ngoài thanh bên — nó không có trang độc lập.
  • Trường bảng kê khai slots chỉ mang tính chất tài liệu. Liên kết thực tế xảy ra trong gói JS thông qua registerSlot().
  • Nhiều plugin có thể yêu cầu cùng một vị trí trong phạm vi trang. Họ kết xuất xếp chồng lên nhau theo thứ tự đăng ký.
  • Không có dấu chân khi không đăng ký plugin: trang tích hợp hiển thị chính xác như trước.

Một plugin tham chiếu (example-dashboard trong hermes-example-plugins) gửi một bản demo trực tiếp chèn biểu ngữ vào sessions:top — cài đặt nó để xem mẫu từ đầu đến cuối.

Các plugin chỉ có khe cắm (tab.hidden)

Khi tab.hidden: true, plugin sẽ đăng ký thành phần của nó (đối với các lượt truy cập URL trực tiếp) và bất kỳ vị trí nào, nhưng không bao giờ thêm tab vào điều hướng. Được sử dụng bởi các plugin chỉ tồn tại để chèn vào các vị trí - đỉnh tiêu đề, HUD thanh bên, lớp phủ.

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

Gói này vẫn gọi register() với thành phần giữ chỗ (cách thực hành tốt trong trường hợp ai đó truy cập trực tiếp vào URL) và sau đó registerSlot() để thực hiện công việc thực sự.

Các tuyến API phụ trợ

Các plugin có thể đăng ký các tuyến FastAPI bằng cách đặt api trong tệp kê khai. Tạo tệp và xuất bộ định tuyến:

# ~/.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):
trả về {"ok": Đúng, "đã nhận": nội dung}

Các tuyến đường được gắn kết dưới /api/plugins/<name>/, do đó, ở trên trở thành:

  • NHẬN /api/plugin/plugin của tôi/dữ liệu
  • POST /api/plugins/my-plugin/action

Các tuyến API plugin bỏ qua xác thực mã thông báo phiên vì máy chủ bảng thông tin liên kết với localhost theo mặc định. Không hiển thị trang tổng quan trên giao diện công cộng với --host 0.0.0.0 nếu bạn chạy các plugin không đáng tin cậy — các tuyến đường của chúng cũng có thể truy cập được.

Truy cập nội bộ của Hermes

Các tuyến phụ trợ chạy bên trong quy trình bảng điều khiển, vì vậy chúng có thể nhập trực tiếp từ cơ sở mã 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 = tải_config()
trả về {"model": cfg.get("model", {})}

CSS tùy chỉnh cho mỗi plugin

Nếu plugin của bạn cần các kiểu ngoài lớp Tailwind và style= nội tuyến, hãy thêm tệp CSS và tham chiếu tệp đó trong tệp kê khai:

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

Tệp được đưa vào dưới dạng thẻ <link> khi tải plugin. Sử dụng tên lớp cụ thể để tránh xung đột với kiểu của trang tổng quan và tham chiếu các biến CSS của trang tổng quan để luôn nhận biết chủ đề:

/* 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);
}

Trang tổng quan hiển thị mọi mã thông báo shadcn dưới dạng --color-* cộng với các phần bổ sung của chủ đề (--theme-asset-*, --comComponent-<bucket>-*, --radius, --spacing-mul). Hãy tham khảo những thứ đó và plugin của bạn sẽ tự động thay đổi giao diện với chủ đề đang hoạt động.

Khám phá và tải lại plugin

Trang tổng quan quét ba thư mục để tìm dashboard/manifest.json:

PriorityDirectorySource label
1 (wins on conflict)~/.hermes/plugins/<name>/dashboard/user
2<repo>/plugins/memory/<name>/dashboard/bundled
2<repo>/plugins/<name>/dashboard/bundled
3./.hermes/plugins/<name>/dashboard/project — only when HERMES_ENABLE_PROJECT_PLUGINS is set

Kết quả khám phá được lưu vào bộ đệm cho mỗi quy trình trên trang tổng quan. Sau khi thêm một plugin mới, hãy:

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

…hoặc khởi động lại bảng điều khiển Hermes.

Vòng đời tải plugin

  1. Tải bảng điều khiển. main.tsx hiển thị SDK trên window.__HERMES_PLUGIN_SDK__ và sổ đăng ký trên window.__HERMES_PLUGINS__.
  2. App.tsx gọi usePlugins() → tìm nạp GET /api/dashboard/plugins.
  3. Đối với mỗi tệp kê khai: CSS <link> được chèn vào (nếu được khai báo), sau đó thẻ <script> sẽ tải gói JS.
  4. IIFE của plugin chạy và gọi window.__HERMES_PLUGINS__.register(name, Component) — và tùy chọn .registerSlot(name, slot, Component) cho mỗi slot.
  5. Bảng thông tin phân giải thành phần đã đăng ký dựa trên bảng kê khai, thêm tab vào điều hướng (trừ khi hidden) và gắn thành phần đó dưới dạng tuyến đường.

Các plugin có tối đa 2 giây sau khi tải tập lệnh để gọi register(). Sau đó, bảng điều khiển ngừng chờ và hoàn tất kết xuất ban đầu. Nếu sau này plugin đăng ký, nó vẫn xuất hiện - điều hướng đang hoạt động.

Nếu tập lệnh của plugin không tải được (404, lỗi cú pháp, ngoại lệ trong IIFE), bảng thông tin sẽ ghi cảnh báo vào bảng điều khiển trình duyệt và tiếp tục mà không có cảnh báo đó.


Bản demo chủ đề + plugin kết hợp

Plugin strike-freedom-cockpit (repo đi kèm hermes-example-plugins) là một bản demo reskin hoàn chỉnh. Nó kết hợp chủ đề YAML với một plugin chỉ có khe cắm để tạo ra HUD kiểu buồng lái mà không cần chia bảng điều khiển.

Nó thể hiện điều gì:

  • Một chủ đề đầy đủ sử dụng bảng màu, kiểu chữ, fontUrl, layoutVariant: Cockpit, assets, ComponentStyles (góc thẻ có khía, nền chuyển màu), colorOverridescustomCSS (lớp phủ quét).
  • Một plugin chỉ dành cho vị trí (tab.hidden: true) đăng ký thành ba vị trí:
    • sidebar — bảng MS-STATUS với các thanh đo từ xa trực tiếp được điều khiển bởi SDK.api.getStatus().
    • header-left — một huy hiệu phe phái có nội dung --theme-asset-crest từ chủ đề đang hoạt động.
    • footer-right — dòng giới thiệu tùy chỉnh thay thế dòng tổ chức mặc định.
  • Plugin đọc tác phẩm nghệ thuật do chủ đề cung cấp thông qua các vars CSS, do đó, việc hoán đổi chủ đề sẽ thay đổi anh hùng/huy hiệu mà không thay đổi mã plugin.

Cài đặt:

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/

Mở bảng điều khiển, chọn Strike Freedom từ trình chuyển đổi chủ đề. Thanh bên buồng lái xuất hiện, biểu tượng hiển thị ở phần đầu trang, dòng tagline thay thế cho phần chân trang. Chuyển về Hermes Teal và plugin vẫn được cài đặt nhưng ẩn (khe sidebar chỉ hiển thị trong biến thể bố cục cockpit).

Đọc nguồn plugin (strike-freedom-cockpit/dashboard/dist/index.js trong kho lưu trữ đồng hành) để biết cách nó đọc các biến thể CSS, bảo vệ khỏi các trang tổng quan cũ hơn mà không hỗ trợ vị trí và đăng ký ba vị trí từ một gói.


Tham chiếu API

Điểm cuối của chủ đề

EndpointMethodDescription
/api/dashboard/themesGETList available themes + active name. Built-ins return {name, label, description}; user themes also include a definition field with the full normalised theme object.
/api/dashboard/themePUTSet active theme. Body: {"name": "midnight"}. Persists to config.yaml under dashboard.theme.

Điểm cuối của plugin

EndpointMethodDescription
/api/dashboard/pluginsGETList discovered plugins (with manifests, minus internal fields).
/api/dashboard/plugins/rescanGETForce re-scan the plugin directories without restarting.
/dashboard-plugins/<name>/<path>GETServe static assets from a plugin's dashboard/ directory. Path traversal is blocked.
/api/plugins/<name>/**Plugin-registered backend routes.

SDK trên window

GlobalTypeProvider
window.__HERMES_PLUGIN_SDK__objectregistry.ts — React, hooks, UI components, API client, utils.
window.__HERMES_PLUGINS__.register(name, Component)functionRegister a plugin's main component.
window.__HERMES_PLUGINS__.registerSlot(name, slot, Component)functionRegister into a named shell slot.

Khắc phục sự cố

Chủ đề của tôi không xuất hiện trong bộ chọn. Kiểm tra xem tệp có ở ~/.hermes/dashboard-themes/ và kết thúc bằng .yaml hoặc .yml hay không. Làm mới trang. Chạy curl http://127.0.0.1:9119/api/dashboard/themes — chủ đề của bạn phải nằm trong phản hồi. Nếu YAML có lỗi phân tích cú pháp, bảng thông tin sẽ ghi vào errors.log trong ~/.hermes/logs/.

Tab plugin của tôi không hiển thị.

  1. Kiểm tra tệp kê khai có tại ~/.hermes/plugins/<name>/dashboard/manifest.json (lưu ý thư mục con dashboard/).
  2. curl http://127.0.0.1:9119/api/dashboard/plugins/rescan để buộc khám phá lại.
  3. Mở công cụ phát triển trình duyệt → Mạng — xác nhận manifest.json, index.js và bất kỳ CSS nào được tải mà không có 404.
  4. Mở công cụ phát triển trình duyệt → Bảng điều khiển — tìm lỗi trong IIFE hoặc window.__HERMES_PLUGINS__ không được xác định (cho biết SDK không khởi chạy, thường là lỗi kết xuất React trước đó).
  5. Xác minh các lệnh gọi gói của bạn window.__HERMES_PLUGINS__.register(...)cùng tênmanifest.json:name.

Các thành phần đã đăng ký theo vị trí không hiển thị. Khe sidebar chỉ hiển thị khi chủ đề đang hoạt động có layoutVariant: Cockpit. Các vị trí khác luôn hiển thị. Nếu bạn đang đăng ký vào một vị trí không có lượt truy cập nào, hãy thêm console.log bên trong registerSlot để xác nhận gói plugin đã chạy.

Các tuyến phụ trợ plugin trả về 404.

  1. Xác nhận tệp kê khai có "api": "plugin_api.py" trỏ đến tệp hiện có bên trong dashboard/.
  2. Khởi động lại bảng điều khiển Hermes — các tuyến API của plugin được gắn kết một lần khi khởi động, không khi quét lại.
  3. Kiểm tra xem plugin_api.py có xuất router = APIRouter() cấp mô-đun hay không. Tên xuất khẩu khác không được chọn.
  4. Đuôi ~/.hermes/logs/errors.log cho Không thể tải plugin <tên> các tuyến API — lỗi nhập được ghi lại ở đó.

Thay đổi chủ đề sẽ loại bỏ phần ghi đè màu của tôi. colorOverrides nằm trong phạm vi chủ đề đang hoạt động và bị xóa khi chuyển đổi chủ đề — đó là do thiết kế. Nếu bạn muốn các phần ghi đè vẫn tồn tại, hãy đặt chúng vào YAML của chủ đề chứ không phải trong trình chuyển đổi trực tiếp.

CSS tùy chỉnh chủ đề bị cắt bớt. Khối customCSS được giới hạn ở mức 32 KiB cho mỗi chủ đề. Chia các biểu định kiểu lớn cho nhiều chủ đề hoặc chuyển sang một plugin chèn biểu định kiểu đầy đủ thông qua trường css của nó (không có giới hạn kích thước).

Tôi muốn gửi một plugin trên PyPI. Các plugin bảng điều khiển được cài đặt theo bố cục thư mục chứ không phải theo điểm vào pip. Đường dẫn phân phối sạch nhất hiện nay là git repo mà người dùng sao chép vào ~/.hermes/plugins/. Trình cài đặt dựa trên pip cho plugin bảng điều khiển hiện chưa được kết nối.