891128fec3
- P1b: 点击通知切回 Windows Terminal 原标签(源生成 COM UIAutomation,AOT 友好) - Toast 设为工具窗口(WS_EX_TOOLWINDOW),从任务栏与 Alt+Tab 隐藏 - 投递改为落盘队列 spool + FileSystemWatcher,CLI 毫秒级返回不阻塞 Claude Code - 移除命名管道(PipeServer/PipeClient/PipeMessage),新增 NotificationSpool/SpoolWatcher - NativeAOT 发布配置 + scripts/build.bat(vcvars 自动配置 MSVC 工具链) - .gitattributes 保证 .bat 用 CRLF
119 lines
3.1 KiB
C#
119 lines
3.1 KiB
C#
using System;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using Notify.Serialization;
|
|
|
|
namespace Notify.Ipc;
|
|
|
|
/// <summary>
|
|
/// 基于落盘队列的非阻塞投递:CLI 写文件后立即返回,Host 监视目录消费
|
|
///
|
|
/// 取代命名管道,避免 CLI 在 Host 冷启动时被阻塞而拖住 Claude Code
|
|
/// </summary>
|
|
public static class NotificationSpool
|
|
{
|
|
public static readonly string Dir =
|
|
Path.Combine(Path.GetTempPath(), "claude-notify-spool");
|
|
|
|
// CLI 侧:写入一条请求,必要时拉起 Host,全程不阻塞
|
|
public static void Deliver(NotifyMessage message)
|
|
{
|
|
try
|
|
{
|
|
Directory.CreateDirectory(Dir);
|
|
|
|
// 先写 .tmp 再原子改名为 .json,避免 Host 读到半截文件
|
|
var id = Guid.NewGuid().ToString("N");
|
|
var tmp = Path.Combine(Dir, id + ".tmp");
|
|
var final = Path.Combine(Dir, id + ".json");
|
|
File.WriteAllText(tmp, JsonSerializer.Serialize(message, AppJsonContext.Default.NotifyMessage));
|
|
File.Move(tmp, final);
|
|
}
|
|
catch
|
|
{
|
|
// 写入失败则放弃这条通知,绝不影响调用方
|
|
}
|
|
|
|
EnsureHostRunning();
|
|
}
|
|
|
|
// Host 未运行则拉起(不等待);运行中则什么都不做
|
|
private static void EnsureHostRunning()
|
|
{
|
|
try
|
|
{
|
|
if (Mutex.TryOpenExisting(IpcConstants.HostMutexName, out var existing))
|
|
{
|
|
existing.Dispose();
|
|
return;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// 打不开就当作未运行,继续尝试拉起
|
|
}
|
|
|
|
try
|
|
{
|
|
var exe = Environment.ProcessPath;
|
|
if (exe is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Process.Start(new ProcessStartInfo
|
|
{
|
|
FileName = exe,
|
|
Arguments = "host",
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
});
|
|
}
|
|
catch
|
|
{
|
|
// 拉起失败则该通知会在下次 Host 启动时由 DrainExisting 补弹
|
|
}
|
|
}
|
|
|
|
// Host 侧:消费单个 spool 文件并删除
|
|
public static NotifyMessage? ReadAndRemove(string path)
|
|
{
|
|
try
|
|
{
|
|
var json = File.ReadAllText(path);
|
|
File.Delete(path);
|
|
return JsonSerializer.Deserialize(json, AppJsonContext.Default.NotifyMessage);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Host 启动时把已有的 spool 文件补弹一遍
|
|
public static void DrainExisting(Action<NotifyMessage> handler)
|
|
{
|
|
try
|
|
{
|
|
if (!Directory.Exists(Dir))
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var path in Directory.GetFiles(Dir, "*.json"))
|
|
{
|
|
if (ReadAndRemove(path) is { } msg)
|
|
{
|
|
handler(msg);
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// 忽略
|
|
}
|
|
}
|
|
}
|