using System.IO; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; namespace AxCopilot.Handlers; /// /// L29-2: 명령 체인(워크플로우) 핸들러. "flow" 프리픽스로 사용합니다. /// /// 예: flow → 등록된 플로우 목록 /// flow add 출근준비 "remind 09:00 회의" > "today" > "todo list" → 플로우 추가 /// flow 출근준비 → 플로우 실행 /// flow del 출근준비 → 플로우 삭제 /// flow edit 출근준비 → 플로우 명령 목록 표시 (클립보드 복사) /// Enter → 저장된 명령들을 순서대로 런처에 실행 (클립보드에 명령 목록 복사). /// Alfred 워크플로우 경량 대응. /// 저장: %APPDATA%\AxCopilot\flows.json /// public class FlowHandler : IActionHandler { public string? Prefix => "flow"; public PluginMetadata Metadata => new( "명령 체인", "여러 명령을 묶어 순서대로 실행 (워크플로우)", "1.0", "AX"); private sealed record FlowEntry( [property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("commands")] List Commands, [property: JsonPropertyName("created")] DateTime Created); private static readonly string DataPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "flows.json"); private static readonly JsonSerializerOptions JsonOpt = new() { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); var flows = Load(); // ── add 명령 ────────────────────────────────────────────────────────── if (q.StartsWith("add ", StringComparison.OrdinalIgnoreCase)) { var rest = q[4..].Trim(); var spaceIdx = rest.IndexOf(' '); if (spaceIdx < 1) { items.Add(new LauncherItem("사용법: flow add {이름} {명령1} > {명령2} > ...", "예: flow add 출근 \"today\" > \"todo list\" > \"remind 09:00 회의\"", null, null, Symbol: "\uE710")); return Task.FromResult>(items); } var name = rest[..spaceIdx]; var cmdStr = rest[(spaceIdx + 1)..].Trim(); var commands = ParseCommands(cmdStr); if (commands.Count == 0) { items.Add(new LauncherItem("명령을 > 로 구분해 입력하세요", "예: \"today\" > \"todo list\"", null, null, Symbol: Themes.Symbols.Warning)); } else { items.Add(new LauncherItem( $"플로우 저장: {name} ({commands.Count}개 명령)", string.Join(" → ", commands), null, ("add", name, commands), Symbol: "\uE710")); } return Task.FromResult>(items); } // ── del 명령 ────────────────────────────────────────────────────────── if (q.StartsWith("del ", StringComparison.OrdinalIgnoreCase)) { var name = q[4..].Trim(); var found = flows.FirstOrDefault(f => f.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (found != null) { items.Add(new LauncherItem($"플로우 삭제: {found.Name}", $"{found.Commands.Count}개 명령 · {string.Join(" → ", found.Commands)}", null, ("del", found.Name), Symbol: "\uE74D")); } else { items.Add(new LauncherItem($"'{name}' 플로우를 찾을 수 없습니다", "flow del {이름}", null, null, Symbol: "\uE783")); } return Task.FromResult>(items); } // ── 빈 쿼리 → 전체 목록 ────────────────────────────────────────────── if (string.IsNullOrWhiteSpace(q)) { if (flows.Count == 0) { items.Add(new LauncherItem("등록된 명령 체인이 없습니다", "flow add {이름} {명령1} > {명령2} > ... 로 추가하세요", null, null, Symbol: "\uE8A0")); items.Add(new LauncherItem("예시: flow add 출근 \"today\" > \"todo list\"", "오늘 업무 뷰 → 할일 목록 순서대로 실행", null, null, Symbol: Themes.Symbols.Info)); return Task.FromResult>(items); } items.Add(new LauncherItem($"명령 체인 {flows.Count}개", "Enter: 명령 목록 클립보드 복사 · flow add/del 로 관리", null, null, Symbol: "\uE8A0")); foreach (var f in flows) { items.Add(new LauncherItem( $"▶ {f.Name} ({f.Commands.Count}단계)", string.Join(" → ", f.Commands), null, ("run", f), Symbol: "\uE768")); } return Task.FromResult>(items); } // ── 이름 검색 → 실행 ────────────────────────────────────────────────── var match = flows.FirstOrDefault(f => f.Name.Equals(q, StringComparison.OrdinalIgnoreCase)); if (match != null) { items.Add(new LauncherItem( $"▶ {match.Name} 실행", string.Join(" → ", match.Commands), null, ("run", match), Symbol: "\uE768")); for (int i = 0; i < match.Commands.Count; i++) items.Add(new LauncherItem($" {i + 1}. {match.Commands[i]}", "", null, null, Symbol: Themes.Symbols.Terminal)); return Task.FromResult>(items); } // 부분 매칭 var searched = flows.Where(f => f.Name.Contains(q, StringComparison.OrdinalIgnoreCase) || f.Commands.Any(c => c.Contains(q, StringComparison.OrdinalIgnoreCase))).ToList(); if (searched.Count > 0) { foreach (var f in searched) items.Add(new LauncherItem($"▶ {f.Name} ({f.Commands.Count}단계)", string.Join(" → ", f.Commands), null, ("run", f), Symbol: "\uE768")); } else { items.Add(new LauncherItem($"'{q}' 플로우를 찾을 수 없습니다", "flow add {이름} {명령} > {명령} 으로 추가하세요", null, null, Symbol: "\uE783")); } return Task.FromResult>(items); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { if (item.Data is ("add", string name, List commands)) { var flows = Load(); flows.RemoveAll(f => f.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); flows.Add(new FlowEntry(name, commands, DateTime.Now)); Save(flows); NotificationService.Notify("flow", $"'{name}' 플로우가 저장되었습니다. ({commands.Count}단계)"); } else if (item.Data is ("del", string delName)) { var flows = Load(); flows.RemoveAll(f => f.Name.Equals(delName, StringComparison.OrdinalIgnoreCase)); Save(flows); NotificationService.Notify("flow", $"'{delName}' 플로우가 삭제되었습니다."); } else if (item.Data is ("run", FlowEntry flow)) { // 명령 목록을 클립보드에 복사 (사용자가 순서대로 런처에 입력) var text = string.Join("\n", flow.Commands.Select((c, i) => $"{i + 1}. {c}")); try { Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text)); NotificationService.Notify("flow", $"'{flow.Name}' 명령 {flow.Commands.Count}개가 클립보드에 복사되었습니다."); } catch { } } return Task.CompletedTask; } // ─── 명령 파싱 ──────────────────────────────────────────────────────────── private static List ParseCommands(string input) { // "cmd1" > "cmd2" > "cmd3" 또는 cmd1 > cmd2 > cmd3 return input.Split('>') .Select(s => s.Trim().Trim('"').Trim()) .Where(s => !string.IsNullOrWhiteSpace(s)) .ToList(); } // ─── JSON I/O ───────────────────────────────────────────────────────────── private static List Load() { try { if (!File.Exists(DataPath)) return []; var json = File.ReadAllText(DataPath); return JsonSerializer.Deserialize>(json) ?? []; } catch { return []; } } private static void Save(List list) { try { var dir = Path.GetDirectoryName(DataPath)!; if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); File.WriteAllText(DataPath, JsonSerializer.Serialize(list, JsonOpt)); } catch { } } }