Files
AX-Copilot-Codex/src/AxCopilot/Services/AgentTriggerService.cs

179 lines
6.0 KiB
C#

using System.IO;
using System.Text.Json;
using System.Windows.Threading;
namespace AxCopilot.Services;
/// <summary>
/// 이벤트 기반 에이전트 트리거 서비스.
/// 파일 변경, 스케줄, Git 이벤트를 감지하여 에이전트를 자동 실행합니다.
/// </summary>
public class AgentTriggerService : IDisposable
{
private readonly SettingsService _settings;
private FileSystemWatcher? _fileWatcher;
private DispatcherTimer? _scheduleTimer;
private readonly List<TriggerRule> _rules = new();
private bool _disposed;
/// <summary>트리거 발동 시 에이전트 실행 콜백. (triggerName, prompt) → void</summary>
public Action<string, string>? OnTriggerFired { get; set; }
public AgentTriggerService(SettingsService settings)
{
_settings = settings;
}
/// <summary>트리거 규칙을 로드하고 모니터링을 시작합니다.</summary>
public void Start()
{
LoadRules();
StartFileWatcher();
StartScheduleTimer();
LogService.Info($"에이전트 트리거 서비스 시작: {_rules.Count}개 규칙");
}
/// <summary>트리거 규칙을 다시 로드합니다.</summary>
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<List<TriggerRule>>(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;
}
/// <summary>수동으로 트리거를 실행합니다.</summary>
public void FireManual(string ruleName)
{
var rule = _rules.FirstOrDefault(r => r.Name == ruleName);
if (rule != null)
OnTriggerFired?.Invoke(rule.Name, rule.Prompt);
}
/// <summary>현재 로드된 트리거 규칙 목록.</summary>
public IReadOnlyList<TriggerRule> Rules => _rules;
public void Dispose()
{
if (_disposed) return;
_disposed = true;
Stop();
}
}
/// <summary>트리거 규칙 정의.</summary>
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; }
}