Files
notify/docs/architecture.md
T
chuan 79992680c9 docs: 重写 README 并新增 docs 调用时序文档
- README 对齐当前 C# / Avalonia 架构与安装方式
- docs: architecture / sequences / hooks-and-cli / interop / build-and-install
2026-06-22 16:40:27 +08:00

3.6 KiB
Raw Blame History

架构

进程模型:瘦客户端 + 常驻 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(详见 sequences.md 的"非阻塞投递")。

组件 / 目录

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 线程调用。