notification

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2026 License: AGPL-3.0 Imports: 4 Imported by: 0

README

notification

独立的系统通知接口,当前实现 macOS 14+、cgo、带 bundle identifier 的 .app,Linux 桌面 D-Bus 通知服务,以及 Windows 托盘气泡通知。WebAssembly、iOS/Android 和 macOS 无 cgo 构建返回 native.ErrUnsupported;macOS 普通 go run 同样不支持。Linux 不要求 X11,Wayland 下也通过会话总线通信。

notification.RequestPermission(func(err error) {
    if err != nil { return }
    notification.Post(notification.Message{
        ID: "download/report", Title: "下载完成", Body: "report.pdf 已保存。",
    }, func(err error) {
        // err == nil 表示系统接受请求,不代表横幅已显示。
    })
})
// 在 Post 完成后按同一 ID 撤回:
notification.Remove("download/report", func(err error) {})
  • Available() 只检查平台实现和应用包身份,不检查授权、签名信任或系统展示策略。
  • RequestPermission 申请 alert 权限;Post 不自动弹权限提示,未授权返回 ErrPermissionDenied。macOS 的 NSError 保留 domain、code 和本地化说明;UNErrorDomain 的 NotificationsNotAllowed 映射为 ErrPermissionDenied,其他系统错误包装 ErrFailed,用 errors.Is 判断分类,不要比较完整错误字符串。
  • ID 在整个应用内共享,非空且不能包含 NUL;正文或标题至少一个非空,所有字符串必须是合法 UTF-8。相同 ID 重新投递由系统替换已有请求。
  • 完成回调在独立 goroutine 执行,可传 nil。UI 修改应放进 core.Update;不要等待异步完成时阻塞主线程。
  • 同一 ID 的操作应等待前一次完成后再执行,避免异步投递和撤回交错。Remove 同时撤回待投递与已送达项;系统没有撤回完成确认,回调成功仅表示已发出撤回调用。
  • macOS 必须运行应用事件循环;权限对话框、专注模式、系统设置和签名信任影响实际显示。macOS 已注册通知中心 delegate,为本模块通知请求前台 Banner/List;系统设置仍可禁止展示。把窗口置前用 window.Window.Activate(token)(Linux 系统点击带来的激活令牌可以直接交给它,见 kit/notifier.md);跨启动的回调恢复未实现。
  • Windows 使用托盘气泡(Shell_NotifyIcon),Windows 10/11 将它显示为系统通知横幅,不需要 AppUserModelID、打包或开始菜单快捷方式,也不需要 cgo;RequestPermission 不弹授权,直接成功。同一时间只显示一条:投递新 ID 会替换当前通知,旧通知的点击回调随之失效;撤回已被替换的 ID 不做任何事。通知显示期间托盘里有应用图标(取可执行文件的第一个图标资源,没有则用系统默认图标),通知超时、被关闭、被点击或撤回后图标移除,操作中心里的这一条也随之消失。点击通知或托盘图标运行 OnClick;没有激活令牌。标题超过 63 个 UTF-16 单元、正文超过 255 个时截断并加省略号。专注助手/勿扰模式下系统可能不显示。
  • kit.Notifier 通过 NoticeSystemBackend 接入;examples/notification 提供适配器,处理仅系统/应用内加系统、同 ID 更新和撤回。macOS 示例通过 NoticeSystemInteractiveBackend 接入 kit 点击、关闭和 Window.Raise;Linux 示例也使用交互后端,要求通知服务声明 actions。

可手动运行 examples/notification。先构建应用包,再打开它:

mkdir -p /tmp/KeelNotification.app/Contents/MacOS
go build -o /tmp/KeelNotification.app/Contents/MacOS/KeelNotification ./examples/notification
cat > /tmp/KeelNotification.app/Contents/Info.plist <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleIdentifier</key><string>com.keel.notification-demo</string>
<key>CFBundleName</key><string>KeelNotification</string>
<key>CFBundleExecutable</key><string>KeelNotification</string>
<key>CFBundlePackageType</key><string>APPL</string>
</dict></plist>
PLIST
codesign --force --deep --sign - /tmp/KeelNotification.app
open /tmp/KeelNotification.app

此临时应用包用于开发验证;正式分发需要正常签名和可信安装位置。点击“申请通知权限”,允许后投递,切到其他应用观察通知中心;再次投递验证替换,再点“撤回任务通知”验证消失。Notifier 串行化系统请求,并显示真实错误;“应用内和系统通知”模式可验证应用内超时不撤回系统通知。-check 只打印平台/包身份检查结果,不申请权限、不发送通知。

示例保留最近 12 条事件,按发生顺序显示,窗口内容可滚动。点击两者模式的系统通知时,可检查“请求窗口置前 → 应用内通知已关闭 → 已打开对应任务”的顺序;若应用内通知已超时,则不重复产生关闭记录。后续撤回结果不会覆盖这些记录。仅系统模式没有应用内关闭回调;请求置前记录只证明发出了调用,仍需观察窗口是否真的出现在前台。

自动测试覆盖参数校验和未打包进程的拒绝路径,不弹出系统权限窗口。系统授权、真实通知展示/替换/撤回仍需上述手动验收。

接口依据:Apple UNUserNotificationCenter。

Linux 使用运行中的 org.freedesktop.Notifications 服务,Available() 会检查会话总线和服务能力,可能等待总线回应;RequestPermission 异步执行同样检查,不弹权限对话框。无服务返回 ErrUnsupported,协议调用失败返回 ErrFailed。Notify 使用系统默认超时、无图标/动作、应用名 Keel;正文根据 body-markup 能力转义,保留纯文本含义。

业务 ID 映射到服务返回的数值 ID,重复 Post 使用 replaces_id,Remove 调用 CloseNotification。收到 NotificationClosed 后删除映射;服务 owner 变化时清空旧映射,避免将旧 ID 发送给重启后的服务。映射仅保留在当前进程,不能跨进程启动撤回旧通知。Linux 已实现 ActionInvoked 默认动作回调;kit 示例连接窗口 Raise,已把 ActivationToken 传给 OnActivate;窗口后端消费令牌和实际置前仍待接入及桌面验收。真实桌面展示仍需运行示例验收。协议测试使用可控总线替身,不代表 Linux 桌面验收完成。

协议依据:Freedesktop Desktop Notifications。

macOS 可设置 Message.OnClick。收到系统默认打开动作后,回调在独立 goroutine 执行一次;UI 修改需 core.Update。示例“原生点击回调”可手动验证。Windows 的托盘通知和声明了 actions 的 Linux 服务也支持 OnClick;不支持 actions 的 Linux 服务及其他平台返回 ErrUnsupported,不会静默丢弃回调。普通通知仍按各平台能力投递。

首次 Post 在主队列安装本模块 delegate;已有其他 delegate 时返回 ErrConflict,不覆盖它。之后应用也不应另行替换 delegate。只有带本模块标记的通知会请求前台展示或触发回调,其他通知不处理。这里只接收当前进程投递后的响应,未实现冷启动/跨启动恢复。

同 ID 替换使用新回调;替换失败恢复旧回调,迟到的旧请求结果不会覆盖更新的注册。回调在点击、成功 Remove 或无回调替换后释放。仅在通知中心手动忽略/关闭横幅可能不会产生默认打开动作,应用应适时 Remove,释放仍保留的回调。回调已经开始执行后,Remove 不能取消其执行。

验证范围:编译、未打包进程拒绝路径、注册表替换/回退/并发消费和 race 测试;尚未完成真实通知横幅和系统点击验收。

Linux 默认动作使用 default 标识,监听 ActionInvoked 并消费对应业务 ID 的回调一次;仅接受当前服务 owner 的信号。NotificationClosed、成功 Remove、无回调替换和服务重启都会释放注册。信号使用顺序处理器,保留打开后关闭的处理顺序;回调在连接锁外的独立 goroutine 执行,可以继续投递或撤回。失败替换不丢失原有回调。ActivationToken 作为可选激活数据传给 OnActivate,详见下文;收到令牌不代表已成功置前。

2026-10-03 开发验收:临时签名 .app 的示例窗口和事件记录显示正常;权限请求返回 native: operation failed: status 7。因此本次未验证成功授权、系统横幅、替换/撤回或点击置前;后续保留系统错误详情后,复现为 UNErrorDomain (1): Notifications are not allowed for this application,已修正为权限错误分类。该信息不能单独区分系统设置、应用身份或签名问题,成功授权仍待验收。

错误分类依据:Apple notificationsNotAllowed。

激活令牌

Message.OnActivate(func(Activation)) 接收默认打开动作及可选的 Activation.Token;若同时设置 OnClick,则在同一个回调 goroutine 上先调用 OnActivate,再调用 OnClick。没有令牌的平台/通知服务传空字符串,仍正常响应点击。

Linux 按通知 ID 暂存 ActivationToken 信号,ActionInvoked 到达时取出并单次消费。仅接受当前服务 owner、指定对象路径及正确类型/长度的信号;关闭、撤回、成功替换和服务 owner 更新会释放令牌。失败替换/撤回保留原通知状态。未知动作会消费其前置令牌,但不会调用默认打开回调。

freedesktop 通知协议 规定令牌可在 ActionInvoked 前发送,也允许不发送;它可能是 X11 startup ID 或 Wayland xdg-activation token。应用应按窗口后端使用这个不透明值。该接口不改变进程环境变量,也不自行激活窗口;当前 Keel Window.Raise 尚不接收令牌。

协议替身测试覆盖隔离、可选令牌、信号验证、单次消费和生命周期;这些测试不代表已通过 Linux 通知桌面与 Wayland 置前验收。

Documentation

Overview

Package notification posts local operating-system notifications independently of Keel's UI. Implemented for bundled macOS applications with cgo, Linux desktop D-Bus and Windows tray balloons.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Available

func Available() bool

Available reports whether this process can use the platform implementation. It does not report notification permission or guarantee banner presentation. On Linux it queries the session bus and can wait for a service response.

func Post

func Post(m Message, done func(error))

Post submits a notification without requesting permission. done reports OS acceptance, not that a banner was shown or read. It runs once on a separate goroutine. RequestPermission must have completed successfully first.

func Remove

func Remove(id string, done func(error))

Remove requests removal of both pending and delivered notifications with id. Completion confirms the removal calls were issued; macOS provides no removal acknowledgment. Removing an unknown ID is harmless. done may be nil.

func RequestPermission

func RequestPermission(done func(error))

RequestPermission asks for alert authorization. done runs once on a separate goroutine. It may be nil. macOS needs a bundled app and a running event loop. Linux only checks the notification service and does not show a permission prompt.

Types

type Activation

type Activation struct{ Token string }

Activation carries optional platform data for opening a notification. Token is an opaque Linux X11 startup ID or Wayland xdg-activation token. It is empty when the server or platform does not supply one.

type Message

type Message struct {
	ID, Title, Body string
	// OnClick runs on a separate goroutine when the system notification is opened.
	// Supported on macOS, Windows and Linux servers advertising actions. It does not
	// raise a specific UI window.
	OnClick func()
	// OnActivate receives the platform's activation data on the callback goroutine.
	// If both callbacks are set, it runs before OnClick. No window is raised here.
	OnActivate func(Activation)
}

Message is a system notification. ID is an application-wide stable identifier; posting it again replaces the matching notification. Title or Body is required.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL