using System; using System.IO; using System.Text; using System.Text.Json; using Notify.Interop; using Notify.Ipc; using Notify.Models; using Notify.Serialization; using Notify.Services; namespace Notify.Cli; /// /// 钩子子命令实现:纯互操作 / IPC,绝不加载 Avalonia,做完即退出 /// public static class CliRunner { // UserPromptSubmit:记录前台窗口与 prompt public static int Save() { var input = ReadStdin(); if (string.IsNullOrEmpty(input?.SessionId)) { return 0; } var hwnd = Win32.GetForegroundWindow(); // 前台是 Windows Terminal 时,记录当前标签的 RuntimeId var wtRuntimeId = WinTerminalTabs.IsWindowsTerminal(hwnd) ? WinTerminalTabs.GetSelectedTabRuntimeId(hwnd) : ""; StateStore.Save(input.SessionId, new StateData { Hwnd = hwnd.ToInt64(), Prompt = input.Prompt ?? "", WtRuntimeId = wtRuntimeId, CallerExePath = ProcessTree.FindCallerExePath(), }); return 0; } // Stop:任务完成通知,正文取本次 prompt public static int Notify() { var input = ReadStdin(); if (string.IsNullOrEmpty(input?.SessionId)) { return 0; } var state = StateStore.Load(input.SessionId); var message = !string.IsNullOrWhiteSpace(state?.Prompt) ? state!.Prompt : "Task completed"; NotificationSpool.Deliver(new NotifyMessage { SessionId = input.SessionId, Title = "Claude Code", Message = Sanitize(message), InputMode = false, Sticky = false, TargetHwnd = state?.Hwnd ?? 0, WtRuntimeId = state?.WtRuntimeId, IconPath = state?.CallerExePath, }); return 0; } // Notification / PreToolUse:需要输入,常驻显示 public static int Input() { var input = ReadStdin(); if (string.IsNullOrEmpty(input?.SessionId)) { return 0; } // 过滤无需打扰的类型 if (input.NotificationType is "auth_success" or "elicitation_complete" or "elicitation_response") { return 0; } var (title, message) = Resolve(input); var state = StateStore.Load(input.SessionId); NotificationSpool.Deliver(new NotifyMessage { SessionId = input.SessionId, Title = title, Message = Sanitize(message), InputMode = true, Sticky = true, TargetHwnd = state?.Hwnd ?? 0, WtRuntimeId = state?.WtRuntimeId, IconPath = state?.CallerExePath, }); return 0; } // SessionEnd:清理会话状态 public static int Cleanup() { var input = ReadStdin(); if (!string.IsNullOrEmpty(input?.SessionId)) { StateStore.Delete(input.SessionId); } return 0; } // 按 tool_name / notification_type 决定标题与正文,对齐原版语义 private static (string Title, string Message) Resolve(HookInput input) { if (input.ToolName == "AskUserQuestion") { var msg = string.IsNullOrEmpty(input.Message) ? "Claude 在向你提问" : input.Message!; return ("Claude is Asking", msg); } if (input.ToolName == "ExitPlanMode") { return ("Plan Ready for Approval", "Claude 提交了一份计划,待批准"); } var title = input.NotificationType switch { "permission_prompt" => "Permission Required", "idle_prompt" => "Claude is Waiting", "elicitation_dialog" => "MCP Asks", _ => "Input Required", }; var message = string.IsNullOrEmpty(input.Message) ? "Claude needs your input" : input.Message!; return (title, message); } // 折叠换行/制表/多余空白为单行,避免撑乱 toast 布局(截断交给 toast 的省略号) private static string Sanitize(string s) { if (string.IsNullOrEmpty(s)) { return s; } s = s.Replace('\r', ' ').Replace('\n', ' ').Replace('\t', ' '); while (s.Contains(" ")) { s = s.Replace(" ", " "); } return s.Trim(); } // 直接读原始字节并按 UTF-8 解码:WinExe 下 Console.In 不可靠,且其代码页 // 会把中文解成乱码(GBK),这里绕开 private static HookInput? ReadStdin() { try { using var stdin = Console.OpenStandardInput(); using var ms = new MemoryStream(); stdin.CopyTo(ms); var bytes = ms.ToArray(); if (bytes.Length == 0) { return null; } var text = Encoding.UTF8.GetString(bytes).TrimStart(''); if (string.IsNullOrWhiteSpace(text)) { return null; } return JsonSerializer.Deserialize(text, AppJsonContext.Default.HookInput); } catch { return null; } } }