5ce2c8a982
为 Claude Code 提供原生 Windows toast 通知:点击跳回原窗口、切回 Windows Terminal 标签、跨虚拟桌面、调用方图标、非阻塞投递;NativeAOT 单文件分发。
3.7 KiB
3.7 KiB
架构
进程模型:CLI 子命令 + 常驻 Host
整个程序是单个 exe notify.exe,靠命令行第一个参数分流成两类角色:
| 角色 | 触发方式 | 是否加载 Avalonia | 生命周期 |
|---|---|---|---|
| CLI 子命令 | notify save|notify|input|cleanup |
否(纯互操作 / 落盘) | 即起即退(~100ms) |
| Host | notify 或 notify host |
是 | 常驻(单例,无主窗口) |
这样设计的原因:hook 在你每次发消息时都会被拉起,必须极快返回、绝不阻塞 Claude Code;而真正画 UI 的 Avalonia 较重,放在一个只初始化一次的常驻进程里。
flowchart TD
M["Main(args)"] --> SW{"args[0]"}
SW -->|save| CSAVE[CliRunner.Save]
SW -->|notify| CNOTIFY[CliRunner.Notify]
SW -->|input| CINPUT[CliRunner.Input]
SW -->|cleanup| CCLEAN[CliRunner.Cleanup]
SW -->|其它 / host| RUN[RunHost:单例互斥量 + Avalonia]
CSAVE -. 不加载 Avalonia .-> X1[退出]
CNOTIFY -. 不加载 Avalonia .-> X2[退出]
RUN --> APP[App:托盘 + SpoolWatcher + ToastManager]
- 单例:
RunHost用命名互斥量ClaudeCodeNotifyHost保证只有一个 Host;第二个实例直接退出。 - 保活:
ShutdownMode.OnExplicitShutdown,没有主窗口也不退,只有托盘"退出"才结束。
两条数据通道
- 状态文件(每会话)
%TEMP%\claude-notify-{session_id}.jsonsave写入:前台窗口句柄、prompt、WT 标签 RuntimeId、调用方 exe 路径。notify/input读取,拼成通知;cleanup删除。
- spool 队列
%TEMP%\claude-notify-spool\*.jsonnotify/input把一条NotifyMessage原子落盘,Host 用FileSystemWatcher消费。- 取代命名管道,让 CLI 写完即走、不等 Host(见 README 的「非阻塞投递」时序)。
组件 / 目录
Notify/
├── Program.cs 入口:子命令分流 + Host 单例
├── App.axaml(.cs) Avalonia 应用:托盘、SpoolWatcher、收到消息弹 toast
├── Cli/
│ ├── HookInput.cs stdin JSON 的 DTO(源生成反序列化)
│ └── CliRunner.cs save/notify/input/cleanup 实现 + 消息清洗
├── Ipc/
│ ├── NotifyMessage.cs 投递给 Host 的弹窗请求 DTO
│ ├── NotificationSpool.cs 落盘投递 + 拉起 Host(客户端)/ 消费(Host)
│ ├── SpoolWatcher.cs Host 侧目录监视
│ └── IpcConstants.cs 互斥量名等
├── Models/
│ ├── ToastSettings.cs 持久化设置 + 枚举(HEdge/VEdge)
│ ├── ToastRequest.cs Host 内部的一次弹窗请求
│ └── StateData.cs 每会话状态
├── Services/
│ ├── SettingsService.cs 设置读写
│ ├── StateStore.cs 状态文件读写
│ └── ToastManager.cs toast 创建 / 堆叠定位 / 排队
├── ViewModels/ ToastViewModel、SettingsViewModel(partial 源生成属性)
├── Views/ ToastWindow、SettingsWindow
├── Interop/ 原生互操作(见 interop.md)
└── Serialization/
└── AppJsonContext.cs System.Text.Json 源生成上下文
线程模型
- CLI 子命令在单线程跑完即退。
- Host 里:
FileSystemWatcher回调在线程池线程触发 →App.OnNotify里先做一次"是否前台"判断和提示音,再Dispatcher.UIThread.Post切回 UI 线程创建 toast。 - 所有 Avalonia 对象操作都在 UI 线程;GDI / COM 互操作可在任意线程,但本项目主要在 UI 线程调用。