using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Text.Json.Serialization; namespace OmniScheduler; public enum TriggerKind { OneTime, Interval, Daily, Weekly, Monthly, Cron } public enum IntervalUnit { Seconds, Minutes, Hours, Days } public enum MisfireStrategy { DoNothing, FireOnceNow } public sealed class SchedulerState : INotifyPropertyChanged { public ObservableCollection Tasks { get; set; } = []; public ObservableCollection Logs { get; set; } = []; public AppSettings Settings { get; set; } = new(); public event PropertyChangedEventHandler? PropertyChanged; public void Notify([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public sealed class AppSettings : NotifyObject { private string _omniNotifyUrl = "http://127.0.0.1:19845/notify"; private int _logRetentionDays = 30; private int _maxLogRecords = 10000; private bool _startWithWindows; public string OmniNotifyUrl { get => _omniNotifyUrl; set => SetField(ref _omniNotifyUrl, value); } public int LogRetentionDays { get => _logRetentionDays; set => SetField(ref _logRetentionDays, Math.Max(1, value)); } public int MaxLogRecords { get => _maxLogRecords; set => SetField(ref _maxLogRecords, Math.Max(100, value)); } public bool StartWithWindows { get => _startWithWindows; set => SetField(ref _startWithWindows, value); } } public sealed class ScheduledTask : NotifyObject { private bool _isEnabled = true; private string _name = "新任务"; private string _description = ""; private string _channel = ""; private string _title = ""; private string _body = ""; private DateTime? _lastRunAt; private DateTime? _nextRunAt; private string _lastStatus = "Idle"; public Guid Id { get; set; } = Guid.NewGuid(); public bool IsEnabled { get => _isEnabled; set => SetField(ref _isEnabled, value); } public string Name { get => _name; set => SetField(ref _name, value); } public string Description { get => _description; set => SetField(ref _description, value); } public string Channel { get => _channel; set => SetField(ref _channel, value); } public string Title { get => _title; set => SetField(ref _title, value); } public string Body { get => _body; set => SetField(ref _body, value); } public MisfireStrategy MisfireStrategy { get; set; } = MisfireStrategy.DoNothing; public ObservableCollection Triggers { get; set; } = []; public DateTime? LastRunAt { get => _lastRunAt; set => SetField(ref _lastRunAt, value); } public DateTime? NextRunAt { get => _nextRunAt; set => SetField(ref _nextRunAt, value); } public string LastStatus { get => _lastStatus; set => SetField(ref _lastStatus, value); } [JsonIgnore] public string TriggerSummary => Triggers.Count == 0 ? "未配置触发器" : string.Join("; ", Triggers.Select(t => t.Summary).Take(3)); public ScheduledTask Clone() { return new ScheduledTask { Id = Guid.NewGuid(), IsEnabled = IsEnabled, Name = $"{Name} - 副本", Description = Description, Channel = Channel, Title = Title, Body = Body, MisfireStrategy = MisfireStrategy, Triggers = new ObservableCollection(Triggers.Select(t => t.Clone())) }; } public void CopyFrom(ScheduledTask source) { IsEnabled = source.IsEnabled; Name = source.Name; Description = source.Description; Channel = source.Channel; Title = source.Title; Body = source.Body; MisfireStrategy = source.MisfireStrategy; Triggers = new ObservableCollection(source.Triggers.Select(t => t.Clone())); Notify(nameof(TriggerSummary)); } } public sealed class TaskTrigger : NotifyObject { public Guid Id { get; set; } = Guid.NewGuid(); public TriggerKind Kind { get; set; } = TriggerKind.Interval; public DateTime OneTimeAt { get; set; } = DateTime.Now.AddMinutes(5); public int IntervalValue { get; set; } = 5; public IntervalUnit IntervalUnit { get; set; } = IntervalUnit.Minutes; public DateTime? StartsAt { get; set; } public DateTime? EndsAt { get; set; } public TimeSpan TimeOfDay { get; set; } = DateTime.Now.TimeOfDay; public DayOfWeekFlags WeekDays { get; set; } = DayOfWeekFlags.Monday | DayOfWeekFlags.Tuesday | DayOfWeekFlags.Wednesday | DayOfWeekFlags.Thursday | DayOfWeekFlags.Friday; public int MonthDay { get; set; } = 1; public bool LastBusinessDay { get; set; } public string CronExpression { get; set; } = "0 0/5 14 * * ?"; [JsonIgnore] public string Summary => Kind switch { TriggerKind.OneTime => $"单次 {OneTimeAt:yyyy-MM-dd HH:mm:ss}", TriggerKind.Interval => $"每 {IntervalValue} {IntervalUnitText}", TriggerKind.Daily => $"每日 {TimeOfDay:hh\\:mm\\:ss}", TriggerKind.Weekly => $"每周 {WeekDays} {TimeOfDay:hh\\:mm\\:ss}", TriggerKind.Monthly => LastBusinessDay ? $"每月最后工作日 {TimeOfDay:hh\\:mm\\:ss}" : $"每月 {MonthDay} 日 {TimeOfDay:hh\\:mm\\:ss}", TriggerKind.Cron => $"Cron {CronExpression}", _ => Kind.ToString() }; [JsonIgnore] public string IntervalUnitText => IntervalUnit switch { IntervalUnit.Seconds => "秒", IntervalUnit.Minutes => "分钟", IntervalUnit.Hours => "小时", IntervalUnit.Days => "天", _ => IntervalUnit.ToString() }; public TaskTrigger Clone() => new() { Id = Guid.NewGuid(), Kind = Kind, OneTimeAt = OneTimeAt, IntervalValue = IntervalValue, IntervalUnit = IntervalUnit, StartsAt = StartsAt, EndsAt = EndsAt, TimeOfDay = TimeOfDay, WeekDays = WeekDays, MonthDay = MonthDay, LastBusinessDay = LastBusinessDay, CronExpression = CronExpression }; } [Flags] public enum DayOfWeekFlags { None = 0, Sunday = 1, Monday = 2, Tuesday = 4, Wednesday = 8, Thursday = 16, Friday = 32, Saturday = 64 } public sealed class ExecutionLog { public DateTime Timestamp { get; set; } = DateTime.Now; public string Level { get; set; } = "INFO"; public string TaskName { get; set; } = ""; public string TriggerType { get; set; } = ""; public string TargetUrl { get; set; } = ""; public int? StatusCode { get; set; } public long ElapsedMs { get; set; } public string RequestJson { get; set; } = ""; public string Response { get; set; } = ""; public string Message { get; set; } = ""; } public abstract class NotifyObject : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; protected bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) { if (EqualityComparer.Default.Equals(field, value)) { return false; } field = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); return true; } public void Notify([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }