Skip to content

OpenTray · 桌面状态运行时

给 CLI 与 AI 技能一个桌面上的家——不必背上应用框架的重量。

CLI 工具与 AI 技能做着漫长而安静的工作——构建、agent、监听器——却在没人盯着的终端里结束。OpenTray 是面向 Node、Deno 与 Bun 的桌面状态运行时:一次 createTray() 自动启动本地 broker,把一个常驻的状态原子放上托盘——Rust 内核、公开的托盘/会话协议、没有 Node addon、没有应用框架。

Node · Deno · BunRust 内核 · 无 addon一次 createTray()MIT
GitHub ↗
opentray — first-app

$node first-app.mjs

broker: 已自动启动——Rust 内核

托盘: com.example.first-app · 图标 "OT" 已常驻

菜单: Quit (primaryEvent)

会话: 活跃——事件正在流入

快速开始

一次安装,一次 createTray()

打包好的运行时在首次使用时自动启动本地 broker——可见托盘文本属于图标投影,运行时身份(appId/appName)与之分离,primaryEvent 是普通菜单项上的一个角色,触发的是通常的 menuClick。

first-app.mjs
import {
  createTray,
  type CreateTrayHandle,
  type CreateTrayOptions,
  type TrayIcon,
} from "opentray";

const icon: TrayIcon = { "text-only": "OT" };
let tray: CreateTrayHandle;
const options: CreateTrayOptions = {
  id: "com.example.first-app",
  icon,
  menu: {
    items: [
      {
        title: "Quit",
        primaryEvent: true,
        onMenuClick: () => void tray.destroy(),
      },
    ],
  },
};

tray = await createTray(options, {
  appId: "com.example.first-app",
  appName: "First App",
});

已经有一个在本地提供 HTTP 服务的命令? npx create-opentray 可以把它包装成一个 OpenTray 托管的应用。

内置能力

存在的原因

不是托盘图标,是入口

目标从来不是「显示一个托盘图标」:轻量工具值得一个系统级入口,而不必被迫背上 Electron 或整套桌面应用框架。模型只有四个原子:App 是调用方持有的运行时身份与隔离边界,Tray 是单个桌面状态原子,Session 是托盘事件与变更的实时权威来源,Extension 是可选的原生能力原子。

App · Tray · Session · Extension

SDK

一次调用,自持生命周期

不该为了显示状态去引导一个应用外壳。应用代码直接调用 createTray(),并自行持有前台/后台生命周期。顶层 createTray(...) 与其返回的 setMenu(...) 接受面向应用的菜单简写;更底层的 createClient(...) 保持仅协议形态,供需要精确线上报文结构的工具使用。

opentray · 菜单简写 · 类型化句柄

Rust 内核

是 broker,不是 addon

为什么敢用 npm 装来的托盘?原生侧是一个以独立可执行文件交付的 Rust 内核:默认的 createTray() 传输面向本地运行时宿主,并在首次使用需要时自动启动它;普通应用代码通过公开的托盘/会话协议与它通信。没有 Node addon,托盘工作不需要拆 worker。

托盘/会话协议 · 自动启动的 Rust 宿主

上手

带上你自己的命令

不该为了一个托盘重写一个能用的工具。已经有一个在本地提供 HTTP 服务的命令?npx create-opentray 把它包装成一个 OpenTray 托管的应用——通过浏览器向导交互式完成(create-opentray web)、完全非交互式完成(create-opentray create --app-id … --app-name … --exec …),或把内置 AI 技能交给你的 agent(npx create-opentray skill)。

npx create-opentray · web · create · skill

扩展家族

原生能力,按需原子

当一个状态原子需要的比菜单更多时,扩展在不改变模型的前提下补充原生能力:@opentray/ext-webview 是富弹出层门面——它的窗口默认是托盘持有的工具窗口,style.appMode: true 让其中一个表现得像普通桌面窗口(任务栏/Alt-Tab、Dock/Command-Tab)。@opentray/ext-badge 增加徽标/进度/覆盖层 API;@opentray/ext-island 是路线图上的灵动岛扩展。

ext-webview · ext-badge · ext-island

打包层

与打包器无关,四个适配器

交付不应被打包器绑死。@opentray/packaging 将运行时可执行文件、原生 sidecar 与伴随资产装配到按 app-id 推导的输出路径,并写入 opentray-app-manifest.json 清单;Vite、tsdown、esbuild 与 webpack 的适配器均已发布,四者写入相同的清单结构。

opentray-app-manifest.json · 4 个适配器

平台运行时

六个平台,朴素二进制

运行时以最朴素的方式到达:@opentray/darwin-arm64 这类平台包为三个系统 × 两种架构(darwin、linux、windows × arm64、x64)携带 bin/opentray 或 bin/opentray.exe。打包始终是构建层的关注点——它装配工件并产出清单事实;托盘生命周期、会话权威与扩展分发留在运行时。

@opentray/<os>-<arch> · bin/opentray

发布

协议线 dist-tags

混装的包集不该彼此漂移。当应用使用官方扩展时,请在这组包上锁定同一个 OpenTray 协议线标签:pnpm add opentray@stable-A-B @opentray/ext-webview@stable-A-B。alpha-A-B 承载同一协议线上的 alpha 包;标签由 @opentray/spec 发布。

stable-A-B · alpha-A-B

工作区

一个仓库,一个包家族

npm 表面与仓库布局互为镜像。Lynx 扩展维护在独立的 jixoai/opentray-ext-lynx 仓库中;OpenTray 核心不构建、不装配、也不发布 Lynx 工件。

目录npm 包用途
packages/cliopentray面向开发者的托盘优先 SDK 与 CLI 包。
packages/spec@opentray/specTypeScript 协议与共享契约包。
packages/packaging@opentray/packaging与打包器无关的运行时工件装配(staging)契约。
packages/vite-plugin@opentray/vite-plugin构建在 packaging 契约之上的第一个 Vite 适配器。
packages/ext-webview@opentray/ext-webview富弹出层扩展门面。
packages/ext-webview-*@opentray/ext-webview-*平台 WebView 动态库包。
packages/ext-badge@opentray/ext-badge平台徽标/进度/覆盖层 API 扩展。
packages/ext-island@opentray/ext-island路线图上的灵动岛/实时活动扩展。
packages/<os>-<arch>@opentray/<os>-<arch>平台运行时工件包。

生态

一切所在

源码、包与公开指南——与 README 相同的一组链接。