Initial commit to new repository
This commit is contained in:
178
src/AxCopilot/Services/AgentTriggerService.cs
Normal file
178
src/AxCopilot/Services/AgentTriggerService.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
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; }
|
||||
}
|
||||
Reference in New Issue
Block a user