using System.IO; using System.Text.Json; using System.Windows.Threading; namespace AxCopilot.Services; /// /// 이벤트 기반 에이전트 트리거 서비스. /// 파일 변경, 스케줄, Git 이벤트를 감지하여 에이전트를 자동 실행합니다. /// public class AgentTriggerService : IDisposable { private readonly SettingsService _settings; private FileSystemWatcher? _fileWatcher; private DispatcherTimer? _scheduleTimer; private readonly List _rules = new(); private bool _disposed; /// 트리거 발동 시 에이전트 실행 콜백. (triggerName, prompt) → void public Action? OnTriggerFired { get; set; } public AgentTriggerService(SettingsService settings) { _settings = settings; } /// 트리거 규칙을 로드하고 모니터링을 시작합니다. public void Start() { LoadRules(); StartFileWatcher(); StartScheduleTimer(); LogService.Info($"에이전트 트리거 서비스 시작: {_rules.Count}개 규칙"); } /// 트리거 규칙을 다시 로드합니다. public void Reload() { Stop(); Start(); } public void Stop() { _fileWatcher?.Dispose(); _fileWatcher = null; _scheduleTimer?.Stop(); } private void LoadRules() { _rules.Clear(); var triggerFile = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "triggers.json"); if (!File.Exists(triggerFile)) return; try { var json = File.ReadAllText(triggerFile); var loaded = JsonSerializer.Deserialize>(json); if (loaded != null) _rules.AddRange(loaded.Where(r => r.Enabled)); } catch (Exception ex) { LogService.Warn($"트리거 규칙 로드 실패: {ex.Message}"); } } private void StartFileWatcher() { var fileRules = _rules.Where(r => r.Type == "file_change").ToList(); if (fileRules.Count == 0) return; var workFolder = _settings.Settings.Llm.WorkFolder; if (string.IsNullOrEmpty(workFolder) || !Directory.Exists(workFolder)) return; _fileWatcher = new FileSystemWatcher(workFolder) { IncludeSubdirectories = true, NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName, EnableRaisingEvents = true, }; // 디바운스용 — 짧은 시간 내 여러 변경을 하나로 묶음 DateTime lastTrigger = DateTime.MinValue; _fileWatcher.Changed += (_, e) => { if ((DateTime.Now - lastTrigger).TotalSeconds < 5) return; foreach (var rule in fileRules) { if (MatchesPattern(e.FullPath, rule.Pattern)) { lastTrigger = DateTime.Now; var prompt = rule.Prompt.Replace("{file}", e.FullPath).Replace("{name}", e.Name ?? ""); System.Windows.Application.Current?.Dispatcher.BeginInvoke(() => OnTriggerFired?.Invoke(rule.Name, prompt)); break; } } }; } private void StartScheduleTimer() { var scheduleRules = _rules.Where(r => r.Type == "schedule").ToList(); if (scheduleRules.Count == 0) return; _scheduleTimer = new DispatcherTimer { Interval = TimeSpan.FromMinutes(1) }; _scheduleTimer.Tick += (_, _) => { var now = DateTime.Now; foreach (var rule in scheduleRules) { if (ShouldRunSchedule(rule, now)) { rule.LastRun = now; OnTriggerFired?.Invoke(rule.Name, rule.Prompt); } } }; _scheduleTimer.Start(); } private static bool MatchesPattern(string filePath, string? pattern) { if (string.IsNullOrEmpty(pattern)) return true; // 간단한 확장자/폴더 패턴 매칭 if (pattern.StartsWith("*.")) return filePath.EndsWith(pattern[1..], StringComparison.OrdinalIgnoreCase); return filePath.Contains(pattern, StringComparison.OrdinalIgnoreCase); } private static bool ShouldRunSchedule(TriggerRule rule, DateTime now) { if (rule.LastRun.HasValue && (now - rule.LastRun.Value).TotalMinutes < (rule.IntervalMinutes ?? 60)) return false; // 시간 범위 체크 (근무 시간만) if (rule.ActiveHourStart.HasValue && now.Hour < rule.ActiveHourStart.Value) return false; if (rule.ActiveHourEnd.HasValue && now.Hour >= rule.ActiveHourEnd.Value) return false; return true; } /// 수동으로 트리거를 실행합니다. public void FireManual(string ruleName) { var rule = _rules.FirstOrDefault(r => r.Name == ruleName); if (rule != null) OnTriggerFired?.Invoke(rule.Name, rule.Prompt); } /// 현재 로드된 트리거 규칙 목록. public IReadOnlyList Rules => _rules; public void Dispose() { if (_disposed) return; _disposed = true; Stop(); } } /// 트리거 규칙 정의. public class TriggerRule { public string Name { get; set; } = ""; public string Type { get; set; } = "file_change"; // file_change | schedule | git public string Prompt { get; set; } = ""; public string? Pattern { get; set; } // file_change: 확장자/경로 패턴 public int? IntervalMinutes { get; set; } // schedule: 실행 간격 (분) public int? ActiveHourStart { get; set; } // schedule: 활성 시작 시각 (0~23) public int? ActiveHourEnd { get; set; } // schedule: 활성 종료 시각 public bool Enabled { get; set; } = true; public DateTime? LastRun { get; set; } }