Desktop 插件 SDK

桌面插件让你在 Hermes Agent 桌面应用中注册一等 UI、webview、持久化、快捷方式、工作区面板与运行时能力。该 SDK 面向在桌面中提供面向用户功能的外部或内置包。

包结构与加载

一个桌面插件包是一个导出 default 注册器的 ES 模块:

import { defineDesktopPlugin } from "@hermes-agent/desktop-sdk";

export default defineDesktopPlugin((ctx) => {
  ctx.registerRoute({
    path: "hello",
    nav: { label: "Hello", icon: "sparkles" },
    screen: async () => () => import("./screens/hello.js"),
  });
});

最小有效清单需要:

{
  "name": "@hermes-agent/desktop-plugin-hello",
  "version": "0.1.0",
  "desktopPlugin": {
    "id": "hello",
    "entry": "./index.js",
    "displayName": "Hello Plugin"
  }
}

插件运行时加载 desktopPlugin.entry,用来自插件包或运行时配置的描述调用注册器。

发现与启用

插件经配置清单发现,例如:

{
  "plugins": {
    "desktop": [
      {
        "id": "hello",
        "package": "@hermes-agent/desktop-plugin-hello",
        "version": "0.1.0",
        "enabled": true
      }
    ]
  }
}

开发模式可以指向本地入口路径。用户插件也可经桌面设置 UI 安装与切换。

UI 注册器

ctx.registerRoute 向应用导航注册一个屏幕。它接收:

  • path:一个稳定路由片段,例如 hello。
  • nav:标签、图标与可选的排序分组。
  • screen:异步组件加载器,通常为动态 import()。
  • details:可选的元数据,如描述、作者、徽章。

路由在应用主侧边栏中渲染。多个插件可贡献路由;它们按导航排序组合。

生命周期

defineDesktopPlugin 接收一个 DesktopPluginContext:

interface DesktopPluginContext {
  readonly plugin: DesktopPluginManifest;
  readonly runtime: DesktopRuntime;

  registerRoute(route: DesktopRouteRegistration): void;
  registerSidebarSection(section: DesktopSidebarSection): void;
  registerStatusBadge(badge: DesktopStatusBadge): void;
  registerCommand(command: DesktopCommandRegistration): void;
  registerTheme?(theme: DesktopThemeRegistration): void;
  registerStorage(namespace: string): DesktopPluginStorage;
  registerConnection<T>(connection: DesktopConnectionDefinition<T>): DesktopConnectionHandle<T>;
  registerPanel(panel: DesktopPanelRegistration): void;
  registerDevtool?(tool: DesktopDevtoolRegistration): void;
}

当应用就绪时,插件运行其注册器;导航、状态、存储与连接按需要可用。插件在应用生命周期内注册一次。

导航与屏幕

注册路由

ctx.registerRoute({
  path: "notes",
  nav: {
    label: "Notes",
    icon: "notebook",
    order: 30,
  },
  screen: async () => () => import("./screens/notes.js"),
  details: {
    description: "Per-profile notes workspace",
    author: "Hermes",
  },
});

屏幕是一个返回 React 组件的模块:

export default function NotesScreen() {
  return <main>Notes workspace</main>;
}

插件用经运行时或框架 provider 暴露的现有桌面设计系统组件。不要在插件中硬编码应用 shell 间距;优先使用共享布局组件。

侧边栏分组

用 registerSidebarSection 把多个路由归入一个有标签的组:

ctx.registerSidebarSection({
  id: "productivity",
  label: "Productivity",
  order: 40,
});

在路由的 nav.sectionId 中引用该 section id。

状态徽章

插件可在应用外壳中贡献一个小型状态指示器:

ctx.registerStatusBadge({
  id: "sync",
  label: "Sync",
  icon: "cloud",
  getState: () => ({ tone: "ok", text: "Up to date" }),
});

徽章应保持无内容——状态文本应简短且面向运维。

持久化与配置

registerStorage(namespace) 返回一个作用域化存储句柄:

const storage = ctx.registerStorage("notes");

await storage.set("lastView", { view: "grid" });
const lastView = await storage.get<{ view: string }>("lastView");

存储命名空间与插件 id 绑定,因此不同插件不会读取彼此的配置。该存储是异步持久化的;UI 状态可保留在组件局部内存中。

作用域级别

桌面存储后端按 profile 或安装存储键。插件通常应该经其命名空间使用默认作用域,而不是直接操作 localStorage。

对于用户配置,提供一个插件自己的设置屏幕并从 ctx.registerRoute({ path: "settings" }) 挂载它。

连接与 IPC

插件注册浏览器运行时与后端服务之间的类型化连接。连接用于受权限约束的桥,而不是把机密暴露给渲染器。

type NotesContract = {
  list(): Promise<Array<{ id: string; title: string }>>;
  create(input: { title: string }): Promise<{ id: string }>;
};

const notes = ctx.registerConnection<NotesContract>({
  id: "notes",
  methods: {
    list: { description: "List notes", input: "readonly" },
    create: { description: "Create note", input: "write" },
  },
});

UI 组件可经注入或运行时 helper 调用这些方法:

const notes = usePluginConnection<NotesContract>("notes");
const items = await notes.list();

连接实现住在桌面后端的受管插件宿主中。浏览器界面绝不直接访问 Node.js、文件系统、子进程或环境变量。

安全模型

  • 渲染器只看到类型化方法及其声明的访问级别。
  • 后端 handler 应用权限、路径规范化与输入校验。
  • 不透明 id 应在边界处验证;用户路径绝不直接流入 shell 命令。
  • 连接失败应在 UI 中优雅降级,绝不导致未捕获渲染错误。

运行时能力

ctx.runtime 暴露有限的环境:

interface DesktopRuntime {
  readonly platform: "darwin" | "win32" | "linux";
  readonly version: string;
  readonly hermesHome: string;
  readonly profileName: string;
  readonly notify(message: DesktopNotification): Promise<void>;
}

插件可用它适配平台行为,但不应为了绕过隔离直接读 process.env。

通知

await ctx.runtime.notify({
  title: "Notes synced",
  body: "Your notes are up to date.",
});

通知应简短、非侵入,且应可由用户禁用。

快捷键

经 registerCommand 注册命令:

ctx.registerCommand({
  id: "notes.new",
  label: "New note",
  shortcut: "CmdOrCtrl+Shift+N",
  run: () => openNotesEditor(),
});

快捷键应避免覆盖应用级编辑与导航命令。

工作区面板

桌面应用支持可停靠工作区面板。插件注册一个面板:

ctx.registerPanel({
  id: "notes-sidebar",
  label: "Notes",
  location: "right",
  icon: "notebook",
  panel: async () => () => import("./panels/notes.js"),
});

面板与全屏路由共享 UI 原语,但针对一个窄工作区表面设计。保持交互小且上下文相关。

主题与样式

若提供品牌化 UI,优先使用应用设计系统 token:

ctx.registerTheme?.({
  id: "notes",
  label: "Notes",
  tokens: {
    surface: "#1f2430",
    accent: "#8ab4ff",
  },
});

避免注入全局样式表或修改基础元素选择器。插件应只给自己的面板与路由加样式。

Devtools

开发插件可注册一个开发者工具面板:

ctx.registerDevtool?.({
  id: "notes-debug",
  label: "Notes Debug",
  panel: async () => () => import("./devtools/notes-debug.js"),
});

Devtools 只应出现在开发构建或显式调试模式中。

完整示例:一个小型 Notes 插件

import { defineDesktopPlugin } from "@hermes-agent/desktop-sdk";

export default defineDesktopPlugin((ctx) => {
  const storage = ctx.registerStorage("notes");
  const notes = ctx.registerConnection<{
    list(): Promise<Array<{ id: string; title: string }>>;
    create(input: { title: string }): Promise<{ id: string }>;
  }>({
    id: "notes",
    methods: {
      list: { description: "List notes", input: "readonly" },
      create: { description: "Create note", input: "write" },
    },
  });

  ctx.registerRoute({
    path: "notes",
    nav: { label: "Notes", icon: "notebook", order: 35 },
    screen: async () => () => import("./screens/notes.js"),
  });

  ctx.registerCommand({
    id: "notes.create",
    label: "Create note",
    shortcut: "CmdOrCtrl+Shift+N",
    run: async () => {
      const title = "New note";
      await notes.create({ title });
    },
  });

  ctx.registerStatusBadge({
    id: "notes-count",
    label: "Notes",
    icon: "notebook",
    getState: async () => {
      const items = await notes.list();
      return { tone: "neutral", text: `${items.length} notes` };
    },
  });
});

检查清单

  •  插件包声明含 id、entry、displayName 的 desktopPlugin 清单
  •  默认导出 defineDesktopPlugin
  •  UI 经 registerRoute 或 registerPanel 注册,而非 hack 进应用外壳
  •  受保护能力经 registerConnection,带类型化方法与访问级别
  •  持久化经 registerStorage(namespace),而非裸 localStorage
  •  命令使用描述性 id(domain.action)
  •  快捷键避免应用级编辑/导航冲突
  •  Devtools 只在调试模式中注册
  •  渲染器绝不直接访问 Node.js API、文件系统或 env

另见