diff --git a/AGENTS.md b/AGENTS.md index ca9aa16c..7914956f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,4 +83,7 @@ 编译指令参考,如果出现程序占用场景,直接放弃编译验证即可 ``` dotnet build BetterGenshinImpact.sln -c Debug -``` \ No newline at end of file +``` +###其他要求 + +1. 改动时候请不要删除已有的注释,你可以修改,但是不要删! \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/TaskContext.cs b/BetterGenshinImpact/GameTask/TaskContext.cs index ba346ea4..3dfa3bf7 100644 --- a/BetterGenshinImpact/GameTask/TaskContext.cs +++ b/BetterGenshinImpact/GameTask/TaskContext.cs @@ -39,7 +39,7 @@ namespace BetterGenshinImpact.GameTask GameHandle = hWnd; PostMessageSimulator = Simulation.PostMessage(GameHandle); SystemInfo = new SystemInfo(hWnd); - DpiScale = DpiHelper.ScaleY; + DpiScale = DpiHelper.GetScale(hWnd).Y; //MaskWindowHandle = new WindowInteropHelper(MaskWindow.Instance()).Handle; IsInitialized = true; } @@ -108,4 +108,4 @@ namespace BetterGenshinImpact.GameTask } } } -} \ No newline at end of file +} diff --git a/BetterGenshinImpact/Helpers/DpiHelper.cs b/BetterGenshinImpact/Helpers/DpiHelper.cs index 58226bf7..dc9b7c01 100644 --- a/BetterGenshinImpact/Helpers/DpiHelper.cs +++ b/BetterGenshinImpact/Helpers/DpiHelper.cs @@ -16,8 +16,7 @@ public class DpiHelper private static float GetScaleY() { - if (Environment.OSVersion.Version >= new Version(6, 3) - && UIDispatcherHelper.MainWindow != null) + if (Environment.OSVersion.Version >= new Version(6, 3)) { HWND hWnd = HWND.NULL; if (TaskContext.Instance().IsInitialized) @@ -26,12 +25,15 @@ public class DpiHelper } else { - hWnd = new WindowInteropHelper(Application.Current?.MainWindow).Handle; + hWnd = GetMainWindowHandle(); } - HMONITOR hMonitor = User32.MonitorFromWindow(hWnd, User32.MonitorFlags.MONITOR_DEFAULTTONEAREST); - SHCore.GetDpiForMonitor(hMonitor, SHCore.MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out _, out uint dpiY); - return dpiY / 96f; + if (hWnd != HWND.NULL) + { + HMONITOR hMonitor = User32.MonitorFromWindow(hWnd, User32.MonitorFlags.MONITOR_DEFAULTTONEAREST); + SHCore.GetDpiForMonitor(hMonitor, SHCore.MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out _, out uint dpiY); + return dpiY / 96f; + } } HDC hdc = User32.GetDC(HWND.NULL); @@ -40,6 +42,27 @@ public class DpiHelper return scaleY / 96f; } + private static HWND GetMainWindowHandle() + { + var application = Application.Current; + if (application?.Dispatcher == null) + { + return HWND.NULL; + } + + if (application.Dispatcher.CheckAccess()) + { + return application.MainWindow == null + ? HWND.NULL + : new WindowInteropHelper(application.MainWindow).Handle; + } + + return application.Dispatcher.Invoke(() => + application.MainWindow == null + ? HWND.NULL + : new WindowInteropHelper(application.MainWindow).Handle); + } + public static DpiScaleF GetScale(nint hWnd = 0) { if (hWnd != IntPtr.Zero) diff --git a/BetterGenshinImpact/Service/QuartzSchedulerService.cs b/BetterGenshinImpact/Service/QuartzSchedulerService.cs index d98832b5..fc9884cf 100644 --- a/BetterGenshinImpact/Service/QuartzSchedulerService.cs +++ b/BetterGenshinImpact/Service/QuartzSchedulerService.cs @@ -4,10 +4,12 @@ using System.Threading; using System.Threading.Tasks; using BetterGenshinImpact.Model.Gear.Triggers; using BetterGenshinImpact.Model.Gear.Triggers.QuartzJob; +using BetterGenshinImpact.ViewModel.Pages.Component; using BetterGenshinImpact.Service.GearTask; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Quartz; +using Quartz.Impl.Matchers; namespace BetterGenshinImpact.Service; @@ -19,11 +21,38 @@ public class QuartzSchedulerService(ILogger logger, GearTriggerStorageService triggerStorageService) : IHostedService { private readonly ILogger _logger = logger; + private const string GearGroupName = "gear"; public async Task StartAsync(CancellationToken cancellationToken) { var (timedTriggers, _) = await triggerStorageService.LoadTriggersAsync(); + await SyncTimedTriggersAsync(timedTriggers, cancellationToken); + } + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public async Task SyncTimedTriggersAsync(IEnumerable timedTriggers, CancellationToken cancellationToken = default) + { + var scheduler = await schedulerFactory.GetScheduler(cancellationToken); + var existingJobKeys = await scheduler.GetJobKeys(GroupMatcher.GroupEquals(GearGroupName), cancellationToken); + + if (existingJobKeys.Count > 0) + { + await scheduler.DeleteJobs(existingJobKeys.ToList(), cancellationToken); + } + + var jobsDictionary = BuildJobsDictionary(timedTriggers); + if (jobsDictionary.Count > 0) + { + await scheduler.ScheduleJobs(jobsDictionary, replace: true, cancellationToken); + } + } + + private static Dictionary> BuildJobsDictionary(IEnumerable timedTriggers) + { var allData = timedTriggers .Where(t => t.IsEnabled && !string.IsNullOrWhiteSpace(t.CronExpression)) .Select(t => t.ToTrigger()) @@ -48,12 +77,12 @@ public class QuartzSchedulerService(ILogger logger, }; var job = JobBuilder.Create() - .WithIdentity($"job:{data.Name}", "gear") + .WithIdentity($"job:{data.Name}", GearGroupName) .UsingJobData(jobDataMap) .Build(); var trigger = TriggerBuilder.Create() - .WithIdentity($"trigger:{data.Name}", "gear") + .WithIdentity($"trigger:{data.Name}", GearGroupName) .WithCronSchedule(data.CronExpression) .ForJob(job) .Build(); @@ -61,16 +90,7 @@ public class QuartzSchedulerService(ILogger logger, jobsDictionary.Add(job, new HashSet { trigger }); } - var scheduler = await schedulerFactory.GetScheduler(cancellationToken); - if (jobsDictionary.Count > 0) - { - await scheduler.ScheduleJobs(jobsDictionary, replace: true, cancellationToken); - } + return jobsDictionary; } - public Task StopAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - -} \ No newline at end of file +} diff --git a/BetterGenshinImpact/Service/ScriptService.cs b/BetterGenshinImpact/Service/ScriptService.cs index ee679546..621c9ffd 100644 --- a/BetterGenshinImpact/Service/ScriptService.cs +++ b/BetterGenshinImpact/Service/ScriptService.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using System.Windows; using BetterGenshinImpact.Core.Script; using BetterGenshinImpact.Core.Script.Dependence; using BetterGenshinImpact.Core.Script.Group; @@ -559,7 +560,19 @@ public partial class ScriptService : IScriptService var homePageViewModel = App.GetService(); if (!homePageViewModel!.TaskDispatcherEnabled) { - await homePageViewModel.OnStartTriggerAsync(); + var dispatcher = Application.Current?.Dispatcher; + if (dispatcher?.CheckAccess() == true) + { + await homePageViewModel.OnStartTriggerAsync(); + } + else if (dispatcher != null) + { + await dispatcher.InvokeAsync(homePageViewModel.OnStartTriggerAsync).Task.Unwrap(); + } + else + { + await homePageViewModel.OnStartTriggerAsync(); + } if (waitForMainUi) { diff --git a/BetterGenshinImpact/ViewModel/Pages/GearTriggerPageViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/GearTriggerPageViewModel.cs index e7eeca01..07edbad7 100644 --- a/BetterGenshinImpact/ViewModel/Pages/GearTriggerPageViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/GearTriggerPageViewModel.cs @@ -1,14 +1,18 @@ +using System; using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using BetterGenshinImpact.ViewModel.Pages.Component; -using BetterGenshinImpact.Model.Gear.Triggers; using BetterGenshinImpact.Model; -using BetterGenshinImpact.View.Windows.GearTask; +using BetterGenshinImpact.Model.Gear.Triggers; +using BetterGenshinImpact.Service; using BetterGenshinImpact.Service.GearTask; +using BetterGenshinImpact.View.Windows; +using BetterGenshinImpact.View.Windows.GearTask; +using BetterGenshinImpact.ViewModel.Pages.Component; using Microsoft.Extensions.Logging; -using System; -using System.Threading.Tasks; +using Quartz; namespace BetterGenshinImpact.ViewModel.Pages; @@ -16,6 +20,7 @@ public partial class GearTriggerPageViewModel : ViewModel { private readonly ILogger _logger; private readonly GearTriggerStorageService _storageService; + private readonly QuartzSchedulerService _quartzSchedulerService; [ObservableProperty] private ObservableCollection _timedTriggers = new(); @@ -26,18 +31,27 @@ public partial class GearTriggerPageViewModel : ViewModel [ObservableProperty] private GearTriggerViewModel? _selectedTrigger; + [ObservableProperty] + private GearTaskDefinitionViewModel? _selectedTaskDefinition; + + public GearTriggerPageViewModel( + ILogger logger, + GearTriggerStorageService storageService, + QuartzSchedulerService quartzSchedulerService) + { + _logger = logger; + _storageService = storageService; + _quartzSchedulerService = quartzSchedulerService; + } + partial void OnSelectedTriggerChanged(GearTriggerViewModel? value) { EditTriggerCommand.NotifyCanExecuteChanged(); } - [ObservableProperty] - private GearTaskDefinitionViewModel? _selectedTaskDefinition; - - public GearTriggerPageViewModel(ILogger logger, GearTriggerStorageService storageService) + public override void OnNavigatedTo() { - _logger = logger; - _storageService = storageService; + _ = LoadTriggersAsync(); } private void UpdateTimedTriggersNextRunTime() @@ -48,107 +62,155 @@ public partial class GearTriggerPageViewModel : ViewModel } } - public override void OnNavigatedTo() - { - _ = LoadTriggersAsync(); - } - - /// - /// 异步加载触发器数据 - /// private async Task LoadTriggersAsync() { try { var (timedTriggers, hotkeyTriggers) = await _storageService.LoadTriggersAsync(); - + TimedTriggers.Clear(); HotkeyTriggers.Clear(); - + foreach (var trigger in timedTriggers) { trigger.UpdateNextRunTime(); TimedTriggers.Add(trigger); } - + foreach (var trigger in hotkeyTriggers) { HotkeyTriggers.Add(trigger); } - + UpdateTimedTriggersNextRunTime(); - - _logger.LogInformation("已加载 {TimedCount} 个定时触发器和 {HotkeyCount} 个快捷键触发器", - TimedTriggers.Count, HotkeyTriggers.Count); + _logger.LogInformation("已加载 {TimedCount} 个定时触发器和 {HotkeyCount} 个热键触发器", TimedTriggers.Count, HotkeyTriggers.Count); } catch (Exception ex) { _logger.LogError(ex, "加载触发器数据时发生错误"); } } - - /// - /// 保存触发器数据 - /// - private async Task SaveTriggersAsync() + + private async Task SaveTriggersAsync() { + if (!ValidateTimedTriggers()) + { + return false; + } + try { await _storageService.SaveTriggersAsync(TimedTriggers, HotkeyTriggers); - _logger.LogInformation("触发器数据已保存"); + await _quartzSchedulerService.SyncTimedTriggersAsync(TimedTriggers); + _logger.LogInformation("触发器数据已保存,并已同步到 Quartz 调度器"); + return true; } catch (Exception ex) { _logger.LogError(ex, "保存触发器数据时发生错误"); + ThemedMessageBox.Error($"保存触发器失败:{ex.Message}", "保存失败"); + return false; } } + private bool ValidateTimedTriggers() + { + var invalidTrigger = TimedTriggers.FirstOrDefault(t => + t.TriggerType == TriggerType.Timed && + t.IsEnabled && + !string.IsNullOrWhiteSpace(t.CronExpression) && + !IsValidCronExpression(t.CronExpression, out _)); + + if (invalidTrigger == null) + { + return true; + } + + IsValidCronExpression(invalidTrigger.CronExpression, out var errorMessage); + ThemedMessageBox.Error($"触发器“{invalidTrigger.Name}”的 Cron 表达式无效。\n{errorMessage}", "保存失败"); + return false; + } + + private static bool IsValidCronExpression(string? cronExpression, out string errorMessage) + { + errorMessage = string.Empty; + + if (string.IsNullOrWhiteSpace(cronExpression)) + { + errorMessage = "Cron 表达式不能为空"; + return false; + } + + try + { + _ = new CronExpression(cronExpression); + return true; + } + catch (FormatException ex) + { + errorMessage = ex.Message; + return false; + } + catch (Exception ex) + { + errorMessage = ex.Message; + return false; + } + } [RelayCommand] - private void AddTimedTrigger() + private async Task AddTimedTrigger() { var dialog = AddTriggerDialog.ShowAddTriggerDialog(TriggerType.Timed); - if (dialog != null) + if (dialog == null) { - var newTrigger = new GearTriggerViewModel(dialog.TriggerName, TriggerType.Timed) - { - CronExpression = dialog.CronExpression, - TaskDefinitionName = dialog.SelectedTaskDefinitionName, - IsEnabled = true - }; - TimedTriggers.Add(newTrigger); - SelectedTrigger = newTrigger; - - // 保存数据 - _ = SaveTriggersAsync(); + return; } + + var newTrigger = new GearTriggerViewModel(dialog.TriggerName, TriggerType.Timed) + { + CronExpression = dialog.CronExpression, + TaskDefinitionName = dialog.SelectedTaskDefinitionName, + IsEnabled = dialog.IsEnabled + }; + + TimedTriggers.Add(newTrigger); + SelectedTrigger = newTrigger; + newTrigger.UpdateNextRunTime(); + + await SaveTriggersAsync(); } [RelayCommand] - private void AddHotkeyTrigger() + private async Task AddHotkeyTrigger() { var dialog = AddTriggerDialog.ShowAddTriggerDialog(TriggerType.Hotkey); - if (dialog != null) + if (dialog == null) { - var newTrigger = new GearTriggerViewModel(dialog.TriggerName, TriggerType.Hotkey) - { - Hotkey = dialog.SelectedHotkey, - HotkeyType = dialog.HotkeyType, - TaskDefinitionName = dialog.SelectedTaskDefinitionName, - IsEnabled = true - }; - HotkeyTriggers.Add(newTrigger); - SelectedTrigger = newTrigger; - - // 保存数据 - _ = SaveTriggersAsync(); + return; } + + var newTrigger = new GearTriggerViewModel(dialog.TriggerName, TriggerType.Hotkey) + { + Hotkey = dialog.SelectedHotkey, + HotkeyType = dialog.HotkeyType, + TaskDefinitionName = dialog.SelectedTaskDefinitionName, + IsEnabled = dialog.IsEnabled + }; + + HotkeyTriggers.Add(newTrigger); + SelectedTrigger = newTrigger; + + await SaveTriggersAsync(); } [RelayCommand] - private void DeleteTrigger() + private async Task DeleteTrigger() { - if (SelectedTrigger == null) return; + if (SelectedTrigger == null) + { + return; + } switch (SelectedTrigger.TriggerType) { @@ -161,9 +223,7 @@ public partial class GearTriggerPageViewModel : ViewModel } SelectedTrigger = null; - - // 保存数据 - _ = SaveTriggersAsync(); + await SaveTriggersAsync(); } private bool CanEditTrigger() @@ -172,7 +232,7 @@ public partial class GearTriggerPageViewModel : ViewModel } [RelayCommand(CanExecute = nameof(CanEditTrigger))] - private void EditTrigger() + private async Task EditTrigger() { if (SelectedTrigger is not { } selectedTrigger) { @@ -204,6 +264,6 @@ public partial class GearTriggerPageViewModel : ViewModel selectedTrigger.ModifiedTime = DateTime.Now; selectedTrigger.UpdateNextRunTime(); - _ = SaveTriggersAsync(); + await SaveTriggersAsync(); } } diff --git a/BetterGenshinImpact/ViewModel/Windows/GearTask/AddTriggerDialogViewModel.cs b/BetterGenshinImpact/ViewModel/Windows/GearTask/AddTriggerDialogViewModel.cs index e0019735..6854ce98 100644 --- a/BetterGenshinImpact/ViewModel/Windows/GearTask/AddTriggerDialogViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Windows/GearTask/AddTriggerDialogViewModel.cs @@ -1,24 +1,26 @@ using System; using System.Collections.ObjectModel; -using System.Linq; +using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using BetterGenshinImpact.ViewModel.Pages.Component; -using BetterGenshinImpact.Model.Gear.Triggers; -using BetterGenshinImpact.Model; -using BetterGenshinImpact.Service; -using Microsoft.Extensions.Logging; -using Wpf.Ui.Violeta.Controls; using BetterGenshinImpact.Helpers.Extensions; -using System.ComponentModel; +using BetterGenshinImpact.Model; +using BetterGenshinImpact.Model.Gear.Triggers; +using BetterGenshinImpact.Service; +using BetterGenshinImpact.View.Windows; +using BetterGenshinImpact.ViewModel.Pages.Component; +using Microsoft.Extensions.Logging; +using Quartz; namespace BetterGenshinImpact.ViewModel.Windows.GearTask; /// -/// 新增触发器对话框 ViewModel +/// 新增/编辑触发器对话框 ViewModel。 /// public partial class AddTriggerDialogViewModel : ObservableObject { + private const string DefaultCronExpression = "1 0 4 * * ?"; + private readonly GearTaskStorageService _storageService; private readonly ILogger _logger; @@ -32,7 +34,7 @@ public partial class AddTriggerDialogViewModel : ObservableObject private TriggerType _selectedTriggerType = TriggerType.Timed; [ObservableProperty] - private string _cronExpression = "1 0 4 * * ?"; // 默认每天 04:00:01 + private string _cronExpression = DefaultCronExpression; [ObservableProperty] private CronInputMode _selectedCronInputMode = CronInputMode.Preset; @@ -106,33 +108,25 @@ public partial class AddTriggerDialogViewModel : ObservableObject LoadAvailableTaskDefinitions(); } - /// - /// 构造函数,用于指定触发器类型 - /// - public AddTriggerDialogViewModel(GearTaskStorageService storageService, ILogger logger, TriggerType? predefinedType = null) + public AddTriggerDialogViewModel( + GearTaskStorageService storageService, + ILogger logger, + TriggerType? predefinedType = null) + : this(storageService, logger) { - _storageService = storageService; - _logger = logger; - - // 如果指定了预定义类型,则设置并禁用选择 if (predefinedType.HasValue) { SelectedTriggerType = predefinedType.Value; IsTriggerTypeSelectionEnabled = false; } - - // 生成默认名称 - GenerateDefaultName(); - - // 加载可用的任务定义 - LoadAvailableTaskDefinitions(); } - public AddTriggerDialogViewModel(GearTaskStorageService storageService, ILogger logger, GearTriggerViewModel existingTrigger) + public AddTriggerDialogViewModel( + GearTaskStorageService storageService, + ILogger logger, + GearTriggerViewModel existingTrigger) + : this(storageService, logger) { - _storageService = storageService; - _logger = logger; - DialogTitle = "编辑触发器"; IsTriggerTypeSelectionEnabled = false; @@ -142,21 +136,16 @@ public partial class AddTriggerDialogViewModel : ObservableObject SelectedTaskDefinitionName = existingTrigger.TaskDefinitionName; CronExpression = existingTrigger.TriggerType == TriggerType.Timed - ? (existingTrigger.CronExpression ?? CronExpression) - : CronExpression; + ? existingTrigger.CronExpression ?? DefaultCronExpression + : DefaultCronExpression; SelectedCronInputMode = existingTrigger.TriggerType == TriggerType.Timed ? CronInputMode.Manual : CronInputMode.Preset; - SelectedHotkey = existingTrigger.TriggerType == TriggerType.Hotkey - ? existingTrigger.Hotkey - : null; - + SelectedHotkey = existingTrigger.TriggerType == TriggerType.Hotkey ? existingTrigger.Hotkey : null; HotkeyType = existingTrigger.TriggerType == TriggerType.Hotkey ? existingTrigger.HotkeyType : HotKeyTypeEnum.KeyboardMonitor; - - LoadAvailableTaskDefinitions(); } /// @@ -188,9 +177,9 @@ public partial class AddTriggerDialogViewModel : ObservableObject AvailableTaskDefinitions.Add(taskDefinition.Name); } } - + _logger.LogInformation("已加载 {Count} 个可用的任务定义", AvailableTaskDefinitions.Count); - + // 如果有任务定义,默认选择第一个 if (AvailableTaskDefinitions.Count > 0 && string.IsNullOrWhiteSpace(SelectedTaskDefinitionName)) { @@ -229,44 +218,42 @@ public partial class AddTriggerDialogViewModel : ObservableObject partial void OnSelectedCronInputModeChanged(CronInputMode value) { - if (SelectedTriggerType != TriggerType.Timed) + if (SelectedTriggerType == TriggerType.Timed && string.IsNullOrWhiteSpace(CronExpression)) { - return; - } - - if (string.IsNullOrWhiteSpace(CronExpression)) - { - CronExpression = "1 0 4 * * ?"; + CronExpression = DefaultCronExpression; } } - /// - /// 确认创建触发器 - /// [RelayCommand] private void Confirm() { if (string.IsNullOrWhiteSpace(TriggerName)) { - Toast.Error("请输入触发器名称"); + ThemedMessageBox.Error("请输入触发器名称", "保存失败"); return; } if (SelectedTriggerType == TriggerType.Timed && string.IsNullOrWhiteSpace(CronExpression)) { - Toast.Error(SelectedCronInputMode == CronInputMode.Manual + var message = SelectedCronInputMode == CronInputMode.Manual ? "请输入 Cron 表达式" - : "请先完成定时选择"); + : "请先完成定时选择"; + ThemedMessageBox.Error(message, "保存失败"); + return; + } + + if (SelectedTriggerType == TriggerType.Timed && !IsValidCronExpression(CronExpression, out var cronErrorMessage)) + { + ThemedMessageBox.Error(cronErrorMessage, "Cron 表达式错误"); return; } if (SelectedTriggerType == TriggerType.Hotkey && SelectedHotkey == null) { - Toast.Error("请选择热键"); + ThemedMessageBox.Error("请选择热键", "保存失败"); return; } - // 创建触发器 ViewModel CreatedTrigger = new GearTriggerViewModel(TriggerName, SelectedTriggerType) { IsEnabled = IsEnabled, @@ -294,7 +281,9 @@ public partial class AddTriggerDialogViewModel : ObservableObject [RelayCommand] private void SwitchHotKeyType() { - HotkeyType = HotkeyType == HotKeyTypeEnum.GlobalRegister ? HotKeyTypeEnum.KeyboardMonitor : HotKeyTypeEnum.GlobalRegister; + HotkeyType = HotkeyType == HotKeyTypeEnum.GlobalRegister + ? HotKeyTypeEnum.KeyboardMonitor + : HotKeyTypeEnum.GlobalRegister; } /// @@ -303,8 +292,33 @@ public partial class AddTriggerDialogViewModel : ObservableObject [RelayCommand] private void SelectHotkey() { - // 移除旧的示例代码,现在使用HotKeyTextBox直接设置 - // HotKeyTextBox会直接绑定到SelectedHotkey属性 + } + + private static bool IsValidCronExpression(string? cronExpression, out string errorMessage) + { + errorMessage = string.Empty; + + if (string.IsNullOrWhiteSpace(cronExpression)) + { + errorMessage = "Cron 表达式不能为空"; + return false; + } + + try + { + _ = new CronExpression(cronExpression); + return true; + } + catch (FormatException ex) + { + errorMessage = $"Cron 表达式格式无效:{ex.Message}"; + return false; + } + catch (Exception ex) + { + errorMessage = $"Cron 表达式校验失败:{ex.Message}"; + return false; + } } } @@ -312,6 +326,7 @@ public enum CronInputMode { [Description("可视化选择")] Preset, + [Description("手动 Cron")] Manual }