Skip to content

OpenTray v0.29~0.32:补齐系统交互,改进未签名应用的呈现

jixoai v0.32.0 opentray

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

链接