Files
omni-scheduler/OmniScheduler/Models.cs
home-PC 2a669bdfe7 feat(app): 初始化 OmniScheduler WPF 调度器
基于 PRD 搭建 .NET 8 WPF 桌面应用,包含主控制台、任务编辑、全局设置、系统托盘和应用图标集成。

实现本地任务模型、触发器规则、JSON 状态持久化、OmniNotify HTTP 推送、执行日志记录、动态变量替换以及基础 Cron 预览能力。

补充 .gitignore,排除构建产物和本地 IDE 文件。

BREAKING CHANGE: 首次提交,建立项目初始结构
2026-05-20 00:12:17 +08:00

282 lines
7.7 KiB
C#

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));
}