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

74 lines
3.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 架构
## 进程模型:CLI 子命令 + 常驻 Host
整个程序是**单个 exe `notify.exe`**,靠命令行第一个参数分流成两类角色:
| 角色 | 触发方式 | 是否加载 Avalonia | 生命周期 |
|------|----------|------------------|----------|
| **CLI 子命令** | `notify save\|notify\|input\|cleanup` | 否(纯互操作 / 落盘) | 即起即退(~100ms |
| **Host** | `notify``notify host` | 是 | 常驻(单例,无主窗口) |
这样设计的原因:hook 在你每次发消息时都会被拉起,必须**极快返回、绝不阻塞 Claude Code**;而真正画 UI 的 Avalonia 较重,放在一个**只初始化一次**的常驻进程里。
```mermaid
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 线程调用。