OpenTray v0.29~0.32:补齐系统交互,改进未签名应用的呈现
0.29.0 到 0.32.0 新增 dialog/sound/clipboard/opener/notification 五个原生扩展,补齐桌面工具最常用的系统交互。macOS 未签名应用的通知与对话框都经 osascript 兜底,messageDialog 支持自定义图标,扩展命令协议升到 ABI v2。
$ npm view opentray version
0.32.0
OpenTray 0.29.0 到 0.32.0 补齐了桌面工具最常用的系统交互:问用户与选文件、发提示音、读写剪贴板、打开链接与定位文件、发系统通知,各成一个原生扩展。macOS 26 对未签名应用的呈现限制也有了完整的兜底方案。上一篇讲到 0.28.0,本篇按主题归组讲完这九个版本的全部变更,链接栏给出每个版本的 release 永链。
五个包在同一条版本线发布,都是 embedded 形态:macOS 与 Windows 四个目标架构的二进制内嵌在包里,正常包管理器安装即完整,没有平台子包要装,也没有原生库要找。实测打包体积 0.93 到 1.35 MiB,全部低于 2 MB 预警线。Linux 上每个方法都以各自的类型化错误(如 clipboard_platform_unsupported)在任何派发之前拒绝。
安装一个扩展只要一行:
npm i opentray @opentray/ext-clipboard
问用户与选文件走原生对话框
托盘菜单现在可以直接发起原生对话框:向用户问一个问题,或让用户挑文件、挑目录与选保存位置。选择器取消返回 null,不抛错;alert 与 confirm 是单按钮/双按钮的糖。
import { attachDialog } from "@opentray/ext-dialog";
import { createTray } from "opentray";
const tray = await createTray(
{ id: "com.example.tool", icon: { "text-only": "OT" } },
{ appId: "com.example.tool", appName: "Tool" }
);
const dialog = attachDialog(tray);
const picked = await dialog.pickFile(); // string | null
const saveTo = await dialog.pickSavePath(); // string | null
const choice = await dialog.messageDialog({
message: "导出完成。",
buttons: ["打开文件夹", "关闭"],
defaultId: 0, // Enter 聚焦的按钮
});
细节:平台引擎与长命令结算
macOS 用 NSAlert 与 NSSavePanel 的 modal-session 步进驱动,不跑阻塞主循环的 runModal;Windows 在有界的专用 STA worker 上跑 TaskDialogIndirect,老系统回退 MessageBox。这类长命令经 DeferredOperation 事务结算:先回 Accepted 帧,结束时再投出唯一一帧终态。
未签名 macOS 应用上的呈现(0.31.2-0.32.0)
macOS 26 按签名类别门控的不仅是通知,还有交互式对话框:ad-hoc 或未签名载具的进程内 NSAlert 会渲染成退化形态,任何点击都到不了窗口。真机矩阵验证了七种激活策略全部无效,0.31.1 的激活修复随之退役。
处理方式与通知同律:每个 show 命令先探测进程签名,正式签名走进程内完整体验,未签名经 /usr/bin/osascript 的 display dialog / choose file / choose folder / choose file name 兜底,结果契约两条路径完全一致(按钮索引、取消返回 null)。降级均已记录:detail 并入正文,suppressed 恒为 false,按钮最多三个,不支持混合选择。
图标槽默认归属 osascript 宿主,标题栏显示载具应用名(0.31.3 起)。0.32.0 新增 darwin.icon 选项:传入图片文件路径即可替换图标槽,自定义 png/icns 与系统图标文件都支持;不可读的路径在弹窗前就类型化拒绝。
细节:退化形态与隔离矩阵
退化形态是竖排空白按钮与右上问号圆钮。2026-09-19 的隔离矩阵覆盖五种载具形态与七种激活策略(含 runModal、modal-session 步进、激活前置),全部无效;activateIgnoringOtherApps 改变不了签名类别。签名探测用 SecCode 家族,osascript 路径的负载全部走 run 参数,不做 AppleScript 字面量拼接。
发出提示音,播放本地音频
三个方法,每个在播放被系统接受时即返回,没有完成事件。beep() 播系统默认提示音;playSystemSound 的常用名表冻结为 notification/warning/error 加平台原生名,名字查不到是类型化错误,不静默、也不回落默认音;playSound 播本地音频文件。
const sound = attachSound(tray);
await sound.beep(); // 系统默认提示音
await sound.playSystemSound("notification"); // 冻结的常用名表
await sound.playSound("/path/to/ding.wav");
细节:Windows 的缺失判定与 WAV 校验
Windows 判定系统别名缺失读注册表的声音方案目录,因为 winmm 的 BOOL 返回值在这种查询模式下证明不了缺失;playSound 做精确 RIFF 校验,上限 64 MiB。
剪贴板读写逐字一致
null 是一等值:剪贴板上没有文本时读回 null,不是错误也不是空字符串。契约按 UTF-16 计,写入上限 1,048,576 个单元;孤立的代理对以类型化错误拒绝,不做替换字符,读回与写入逐字一致。
const clipboard = attachClipboard(tray);
await clipboard.writeText("copied from the tray");
const text = await clipboard.readText(); // string | null
await clipboard.clear();
细节:Windows 剪贴板占用的重试纪律
剪贴板被占用时只在 ACCESS_DENIED 重试,2 秒总预算从命令派发起算,耗尽报 clipboard_locked 并带尝试次数与耗时;其他打开失败立即类型化拒绝。
打开链接与定位文件走系统默认
open 把链接和文件交给系统默认应用,revealInFolder 在 Finder 或资源管理器里定位。scheme 允许列表冻结为 http/https/file/mailto;目标按字面分发不做规范化,C:x 这类盘符相对路径因语义歧义直接拒绝。revealInFolder 拒绝含引号与 C0 控制字符的路径,根路径不会被裁掉尾部分隔符。
const opener = attachOpener(tray);
await opener.open("https://example.com"); // 默认浏览器
await opener.open("/tmp/report.txt"); // 该文件类型的默认应用
await opener.revealInFolder("/tmp/report.txt"); // Finder / 资源管理器定位
细节:各平台的接受标准
Windows 侧接受标准是 ShellExecuteW 返回值大于 32;darwin 的 open 以 NSWorkspace.open 布尔值为准,revealInFolder 的 Finder 定位调用无返回值、按定义接受。
系统通知未签名应用也能发
标题/正文/副标题的 UTF-16 上限冻结为 64/256/64 个单元,超限类型化拒绝,不静默截断。Windows 投影走应用自己已注册的托盘图标通道,一次 Shell_NotifyIcon 气泡修改,不新建窗口也不新建图标;两个授权查询在 Windows 上恒为已授予。(对话框从 0.31.2 起遵循同一条签名门控律,见 ext-dialog 节。)
const notification = attachNotification(tray);
const granted = await notification.requestAuthorization(); // darwin 首次调用触发系统授权提示
await notification.notify({ title: "Export finished", body: "report.pdf is ready." });
未签名应用的授权与兜底(0.31.0)
macOS 26 对 ad-hoc 与未签名载体在所有启动形态下都拒绝 UNUserNotificationCenter 授权:直接执行、LaunchServices 启动、LSUIElement、accessory AppKit 四种形态都试过,无提示、无横幅,系统设置里也不出现条目。变量是签名类别,不是 plist。
0.31.0 起 darwin 的 notify 先探测当前进程的代码签名类别。正式签名的应用走完整 UN 体验;未签名载体经 /usr/bin/osascript 的 display notification 兜底,宿主是 Apple 签名的,恒被允许。标题/正文/副标题作为 run 参数传入,不拼接 AppleScript 字面量。
兜底横幅归属到 osascript 的图标,这是已记录的降级;授权命令保持 UN 语义,被拒快照仍然类型化拒绝、零投递。能用 Developer ID 重签 carrier 的开发者拿回完整的 UN 体验。
细节:兜底的接受语义
兜底路径的接受语义是 spawn 成功(resolve-on-acceptance),非 silent 时带默认提示音;签名探测与 ext-dialog 的桥同源,同为 SecCode 家族。
真机验收面板补上测试盲区
examples 里新增 host-atoms 验收面板:一个托盘菜单跑完五个扩展的 43 个场景,覆盖正常路径、类型化拒绝与边界输入;--self-test 跳过 5 个交互对话框场景,非交互自测 38 个。这个面板在真机上抓到一个脚本测试抓不到的 bug:传输层测试全绿,真机的首个 notify 却被门面拒绝,因为 darwin 首个无快照通知合法走延迟结算而门面只接受立即结算(0.30.2 修复)。
长命令有了事务结算
扩展命令 ABI 升到 v2:长命令经 DeferredOperation 事务结算,broker 发出不透明句柄,扩展恰投一帧终态;v1 完全互操作,旧扩展零改动。broker 协议同步升到版本 2。npm 发布改走 OIDC trusted publishing 加 provenance,包页可见构建来源。
其余变更
修复:dialog 命令线格式。0.29.0 与 0.30.0 的四个弹窗命令把负载包在嵌套
options对象里,原生解码器从不接受,每个 show 命令对真机扩展都失败(issue #8,d4b56bb)。修复:首个
notify的延迟结算路径(7b226d3)。修复:未激活宿主上的对话框点击。macOS accessory 宿主未激活时点击被激活过程消费,模态会话永不结束;现在每个模态会话开始前激活宿主(issue #10)。
文档:skills/opentray 五篇消费者参考与各包 README 随版本发布。
0.28.0 之后 82 个提交(不含合并,口径为 conventional 提交前缀计数):15 个 feat / 26 个 fix / 19 个 docs,其余 22 个是 chore、测试与发布工程。本区间无破坏性变更。
升级
npm i opentray@0.32.0
npm i @opentray/ext-dialog @opentray/ext-sound @opentray/ext-clipboard @opentray/ext-opener @opentray/ext-notification
链接
Changelog:opentray@0.32.0 · opentray@0.31.3 · opentray@0.31.2 · opentray@0.31.1 · opentray@0.31.0 · opentray@0.30.2 · opentray@0.30.1 · opentray@0.30.0 · opentray@0.29.0 · packages/cli/CHANGELOG.md
文档:opentray.jixoai.com · ext-dialog README · ext-sound README · ext-clipboard README · ext-opener README · ext-notification README
升级:见上一节
npm:opentray · @opentray/ext-dialog · @opentray/ext-sound · @opentray/ext-clipboard · @opentray/ext-opener · @opentray/ext-notification
本站相关:OpenTray v0.24~0.28(2026-09-16)
English version: /blog/2026-09-18-opentray-v0-29-v0-32/
