179 lines
6.0 KiB
C#
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; }
|
|
}
|