基于 PRD 搭建 .NET 8 WPF 桌面应用,包含主控制台、任务编辑、全局设置、系统托盘和应用图标集成。 实现本地任务模型、触发器规则、JSON 状态持久化、OmniNotify HTTP 推送、执行日志记录、动态变量替换以及基础 Cron 预览能力。 补充 .gitignore,排除构建产物和本地 IDE 文件。 BREAKING CHANGE: 首次提交,建立项目初始结构
282 lines
7.7 KiB
C#
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));
|
|
}
|