Files
notify/docs/architecture.md
chuan 5ce2c8a982 feat: Claude Code 原生 Windows 通知(C# / .NET 10 + Avalonia 12)
为 Claude Code 提供原生 Windows toast 通知:点击跳回原窗口、切回 Windows
Terminal 标签、跨虚拟桌面、调用方图标、非阻塞投递;NativeAOT 单文件分发。
2026-06-22 18:05:15 +08:00

3.7 KiB
Raw Permalink Blame History

架构

进程模型:CLI 子命令 + 常驻 Host

整个程序是单个 exe notify.exe,靠命令行第一个参数分流成两类角色:

角色 触发方式 是否加载 Avalonia 生命周期
CLI 子命令 notify save|notify|input|cleanup 否(纯互操作 / 落盘) 即起即退(~100ms
Host notifynotify 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,没有主窗口也不退,只有托盘"退出"才结束。

两条数据通道

  1. 状态文件(每会话)%TEMP%\claude-notify-{session_id}.json
    • save 写入:前台窗口句柄、prompt、WT 标签 RuntimeId、调用方 exe 路径。
    • notify/input 读取,拼成通知;cleanup 删除。
  2. spool 队列 %TEMP%\claude-notify-spool\*.json
    • notify/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、SettingsViewModelpartial 源生成属性)
├── 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 线程调用。