OpenTray · 桌面状态运行时
给 CLI 与 AI 技能一个桌面上的家——不必背上应用框架的重量。
CLI 工具与 AI 技能做着漫长而安静的工作——构建、agent、监听器——却在没人盯着的终端里结束。OpenTray 是面向 Node、Deno 与 Bun 的桌面状态运行时:一次 createTray() 自动启动本地 broker,把一个常驻的状态原子放上托盘——Rust 内核、公开的托盘/会话协议、没有 Node addon、没有应用框架。
$node first-app.mjs
broker: 已自动启动——Rust 内核
托盘: com.example.first-app · 图标 "OT" 已常驻
菜单: Quit (primaryEvent)
会话: 活跃——事件正在流入
快速开始
一次安装,一次 createTray()
打包好的运行时在首次使用时自动启动本地 broker——可见托盘文本属于图标投影,运行时身份(appId/appName)与之分离,primaryEvent 是普通菜单项上的一个角色,触发的是通常的 menuClick。
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/cli | opentray | 面向开发者的托盘优先 SDK 与 CLI 包。 |
| packages/spec | @opentray/spec | TypeScript 协议与共享契约包。 |
| 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 相同的一组链接。
- GitHub · jixoai/opentray ↗ 源码、issue 与发布历史。
- npm · opentray ↗ 面向开发者的托盘优先 SDK 与 CLI 包。
- create-app 指南 ↗ 用 npx create-opentray 包装一条提供 HTTP 服务的命令。
- app-mode 决策指南 ↗ @opentray/ext-webview 上的普通应用、托盘工具与混合窗口产品。
- opentray-ext-lynx ↗ Lynx 扩展,维护在独立仓库中。
