feat(app): 初始化 OmniScheduler WPF 调度器
基于 PRD 搭建 .NET 8 WPF 桌面应用,包含主控制台、任务编辑、全局设置、系统托盘和应用图标集成。 实现本地任务模型、触发器规则、JSON 状态持久化、OmniNotify HTTP 推送、执行日志记录、动态变量替换以及基础 Cron 预览能力。 补充 .gitignore,排除构建产物和本地 IDE 文件。 BREAKING CHANGE: 首次提交,建立项目初始结构
This commit is contained in:
281
OmniScheduler/Models.cs
Normal file
281
OmniScheduler/Models.cs
Normal file
@@ -0,0 +1,281 @@
|
||||
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<ScheduledTask> Tasks { get; set; } = [];
|
||||
public ObservableCollection<ExecutionLog> 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<TaskTrigger> 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<TaskTrigger>(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<TaskTrigger>(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<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.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));
|
||||
}
|
||||
Reference in New Issue
Block a user