From 9d799510a28dc9a78c33b1c19a5924fa144e9717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=89=E9=B8=AD=E8=9B=8B?= Date: Fri, 26 Dec 2025 00:33:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AE=9A=E6=97=B6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E7=9A=84=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BetterGenshinImpact/App.xaml.cs | 5 +- .../Triggers/QuartzJob/QuartzGearTaskJob.cs | 89 ++++-- .../Gear/Triggers/TriggerExecutionRecord.cs | 48 +++ .../Service/GearTask/LogReaderService.cs | 61 ++++ .../Service/GearTask/Model/GearTaskPaths.cs | 3 +- .../Service/GearTask/TriggerHistoryService.cs | 147 +++++++++ .../View/Pages/GearTriggerPage.xaml | 297 ++++++++++++------ .../Pages/Component/GearTriggerViewModel.cs | 36 +++ .../Pages/GearTriggerPageViewModel.cs | 109 ++++++- 9 files changed, 675 insertions(+), 120 deletions(-) create mode 100644 BetterGenshinImpact/Model/Gear/Triggers/TriggerExecutionRecord.cs create mode 100644 BetterGenshinImpact/Service/GearTask/LogReaderService.cs create mode 100644 BetterGenshinImpact/Service/GearTask/TriggerHistoryService.cs diff --git a/BetterGenshinImpact/App.xaml.cs b/BetterGenshinImpact/App.xaml.cs index cfb4e21f..61d8aebe 100644 --- a/BetterGenshinImpact/App.xaml.cs +++ b/BetterGenshinImpact/App.xaml.cs @@ -65,9 +65,10 @@ public partial class App : Application services.AddSingleton(richTextBox); var loggerConfiguration = new LoggerConfiguration() + .Enrich.FromLogContext() // Enable LogContext .WriteTo.File(logFile, outputTemplate: - "[{Timestamp:HH:mm:ss.fff}] [{Level:u3}] {SourceContext}{NewLine}{Message}{NewLine}{Exception}{NewLine}", + "[{Timestamp:HH:mm:ss.fff}] [{Level:u3}] [{CorrelationId}] {SourceContext}{NewLine}{Message}{NewLine}{Exception}{NewLine}", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 31, retainedFileTimeLimit: TimeSpan.FromDays(21)) @@ -162,6 +163,8 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); diff --git a/BetterGenshinImpact/Model/Gear/Triggers/QuartzJob/QuartzGearTaskJob.cs b/BetterGenshinImpact/Model/Gear/Triggers/QuartzJob/QuartzGearTaskJob.cs index 9bdbc7d5..7a5ab148 100644 --- a/BetterGenshinImpact/Model/Gear/Triggers/QuartzJob/QuartzGearTaskJob.cs +++ b/BetterGenshinImpact/Model/Gear/Triggers/QuartzJob/QuartzGearTaskJob.cs @@ -1,8 +1,10 @@ using System; using System.Threading.Tasks; using BetterGenshinImpact.Service; +using BetterGenshinImpact.Service.GearTask; using Microsoft.Extensions.Logging; using Quartz; +using Serilog.Context; namespace BetterGenshinImpact.Model.Gear.Triggers.QuartzJob; @@ -19,34 +21,77 @@ public class QuartzGearTaskJob : IJob public async Task Execute(IJobExecutionContext context) { - try + var jobDataMap = context.MergedJobDataMap; + var triggerName = jobDataMap.GetString("TriggerName") ?? "Unknown"; + var triggerId = jobDataMap.GetString("TriggerId") ?? Guid.NewGuid().ToString(); + var taskDefinitionName = jobDataMap.GetString("TaskDefinitionName"); + var correlationId = Guid.NewGuid().ToString(); + + using (LogContext.PushProperty("CorrelationId", correlationId)) { - var jobDataMap = context.MergedJobDataMap; - var triggerName = jobDataMap.GetString("TriggerName") ?? "Unknown"; - var triggerId = jobDataMap.GetString("TriggerId") ?? Guid.NewGuid().ToString(); - var taskDefinitionName = jobDataMap.GetString("TaskDefinitionName"); - - if (string.IsNullOrWhiteSpace(taskDefinitionName)) + var historyService = App.GetService(); + var record = new TriggerExecutionRecord { - _logger.LogWarning("触发器 {TriggerName} 未配置任务定义名称", triggerName); - return; + TriggerName = triggerName, + TriggerId = triggerId, + TaskName = taskDefinitionName ?? "Unknown", + StartTime = DateTime.Now, + Status = TriggerExecutionStatus.Running, + CorrelationId = correlationId + }; + + if (historyService != null) + { + await historyService.AddRecordAsync(record); } - var shouldInterrupt = jobDataMap.GetBooleanValue("ShouldInterruptOthers"); - if (shouldInterrupt) + try { - await InterruptOtherJobs(context, triggerId); + if (string.IsNullOrWhiteSpace(taskDefinitionName)) + { + _logger.LogWarning("触发器 {TriggerName} 未配置任务定义名称", triggerName); + if (historyService != null) + { + record.EndTime = DateTime.Now; + record.Status = TriggerExecutionStatus.Failed; + record.Message = "未配置任务定义名称"; + await historyService.UpdateRecordAsync(record); + } + return; + } + + var shouldInterrupt = jobDataMap.GetBooleanValue("ShouldInterruptOthers"); + if (shouldInterrupt) + { + await InterruptOtherJobs(context, triggerId); + } + + var executor = App.GetRequiredService(); + await executor.ExecuteTaskDefinitionAsync(taskDefinitionName, context.CancellationToken); + + _logger.LogInformation("触发器 {TriggerName} 的任务定义执行完成", triggerName); + + if (historyService != null) + { + record.EndTime = DateTime.Now; + record.Status = TriggerExecutionStatus.Success; + record.Message = "执行成功"; + await historyService.UpdateRecordAsync(record); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "执行定时任务时发生错误"); + if (historyService != null) + { + record.EndTime = DateTime.Now; + record.Status = TriggerExecutionStatus.Failed; + record.Message = ex.Message; + record.LogDetails = ex.ToString(); + await historyService.UpdateRecordAsync(record); + } + throw; } - - var executor = App.GetRequiredService(); - await executor.ExecuteTaskDefinitionAsync(taskDefinitionName, context.CancellationToken); - - _logger.LogInformation("触发器 {TriggerName} 的任务定义执行完成", triggerName); - } - catch (Exception ex) - { - _logger.LogError(ex, "执行定时任务时发生错误"); - throw; } } diff --git a/BetterGenshinImpact/Model/Gear/Triggers/TriggerExecutionRecord.cs b/BetterGenshinImpact/Model/Gear/Triggers/TriggerExecutionRecord.cs new file mode 100644 index 00000000..16d3573b --- /dev/null +++ b/BetterGenshinImpact/Model/Gear/Triggers/TriggerExecutionRecord.cs @@ -0,0 +1,48 @@ +using System; +using Newtonsoft.Json; + +namespace BetterGenshinImpact.Model.Gear.Triggers; + +public enum TriggerExecutionStatus +{ + Running, + Success, + Failed, + Skipped +} + +public class TriggerExecutionRecord +{ + [JsonProperty("id")] + public string Id { get; set; } = Guid.NewGuid().ToString(); + + [JsonProperty("trigger_name")] + public string TriggerName { get; set; } = string.Empty; + + [JsonProperty("trigger_id")] + public string TriggerId { get; set; } = string.Empty; + + [JsonProperty("task_name")] + public string TaskName { get; set; } = string.Empty; + + [JsonProperty("start_time")] + public DateTime StartTime { get; set; } = DateTime.Now; + + [JsonProperty("end_time")] + public DateTime? EndTime { get; set; } + + [JsonProperty("status")] + public TriggerExecutionStatus Status { get; set; } = TriggerExecutionStatus.Running; + + [JsonProperty("message")] + public string Message { get; set; } = string.Empty; + + [JsonProperty("log_details")] + public string LogDetails { get; set; } = string.Empty; + + [JsonProperty("correlation_id")] + public string CorrelationId { get; set; } = string.Empty; + + [JsonIgnore] + public TimeSpan Duration => EndTime.HasValue ? EndTime.Value - StartTime : DateTime.Now - StartTime; +} diff --git a/BetterGenshinImpact/Service/GearTask/LogReaderService.cs b/BetterGenshinImpact/Service/GearTask/LogReaderService.cs new file mode 100644 index 00000000..366ab0a6 --- /dev/null +++ b/BetterGenshinImpact/Service/GearTask/LogReaderService.cs @@ -0,0 +1,61 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Text.RegularExpressions; +using System.Collections.Generic; + +namespace BetterGenshinImpact.Service.GearTask; + +public class LogReaderService +{ + private readonly string _logDirectory; + + public LogReaderService() + { + _logDirectory = Path.Combine(AppContext.BaseDirectory, "log"); + } + + public async Task GetLogsForCorrelationIdAsync(string correlationId, DateTime date) + { + if (string.IsNullOrWhiteSpace(correlationId)) + { + return "No correlation ID provided."; + } + + var logFileName = $"better-genshin-impact{date:yyyyMMdd}.log"; + var logFilePath = Path.Combine(_logDirectory, logFileName); + + if (!File.Exists(logFilePath)) + { + return $"Log file not found: {logFilePath}"; + } + + try + { + // 读取文件内容 + // 由于日志文件可能很大,这里简单起见读取全部,生产环境可能需要更高效的方式(如流式读取) + // 考虑到文件可能被占用,使用 FileShare.ReadWrite + using var fileStream = new FileStream(logFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var streamReader = new StreamReader(fileStream); + var content = await streamReader.ReadToEndAsync(); + + var lines = content.Split(new[] { Environment.NewLine }, StringSplitOptions.None); + + // 筛选包含 CorrelationId 的行 + // 日志格式: [Timestamp] [Level] [CorrelationId] SourceContext ... + var relatedLines = lines.Where(line => line.Contains(correlationId)).ToList(); + + if (relatedLines.Count == 0) + { + return "No logs found for this execution."; + } + + return string.Join(Environment.NewLine, relatedLines); + } + catch (Exception ex) + { + return $"Error reading log file: {ex.Message}"; + } + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/Service/GearTask/Model/GearTaskPaths.cs b/BetterGenshinImpact/Service/GearTask/Model/GearTaskPaths.cs index 79115d9a..2ea0312d 100644 --- a/BetterGenshinImpact/Service/GearTask/Model/GearTaskPaths.cs +++ b/BetterGenshinImpact/Service/GearTask/Model/GearTaskPaths.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using BetterGenshinImpact.Core.Config; namespace BetterGenshinImpact.Service.GearTask.Model; @@ -14,4 +14,5 @@ public class GearTaskPaths public static readonly string TaskTriggerPath = Path.Combine(TaskV2Path, "trigger"); + public static readonly string TaskHistoryPath = Path.Combine(TaskV2Path, "history"); } \ No newline at end of file diff --git a/BetterGenshinImpact/Service/GearTask/TriggerHistoryService.cs b/BetterGenshinImpact/Service/GearTask/TriggerHistoryService.cs new file mode 100644 index 00000000..1714bbab --- /dev/null +++ b/BetterGenshinImpact/Service/GearTask/TriggerHistoryService.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using BetterGenshinImpact.Model.Gear.Triggers; +using BetterGenshinImpact.Service.GearTask.Model; +using Newtonsoft.Json; +using Microsoft.Extensions.Logging; + +namespace BetterGenshinImpact.Service.GearTask; + +public interface ITriggerHistoryService +{ + event EventHandler HistoryChanged; + Task AddRecordAsync(TriggerExecutionRecord record); + Task UpdateRecordAsync(TriggerExecutionRecord record); + Task> GetHistoryAsync(); + Task ClearHistoryAsync(); +} + +public class TriggerHistoryService : ITriggerHistoryService +{ + private readonly string _historyFilePath; + private readonly ILogger _logger; + private readonly JsonSerializerSettings _jsonSettings; + private List _cachedHistory; + private const int MaxHistoryCount = 200; + + public event EventHandler? HistoryChanged; + + public TriggerHistoryService(ILogger logger) + { + _logger = logger; + // 使用 GearTaskPaths 定义的路径 + var historyDir = GearTaskPaths.TaskHistoryPath; + _historyFilePath = Path.Combine(historyDir, "trigger_history.json"); + + // 确保目录存在 + if (!Directory.Exists(historyDir)) + { + Directory.CreateDirectory(historyDir); + } + + _jsonSettings = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + DateFormatString = "yyyy-MM-dd HH:mm:ss", + NullValueHandling = NullValueHandling.Ignore + }; + + _cachedHistory = new List(); + // 初始化时加载 + _ = LoadHistoryFromFileAsync(); + } + + private async Task LoadHistoryFromFileAsync() + { + try + { + if (File.Exists(_historyFilePath)) + { + var json = await File.ReadAllTextAsync(_historyFilePath); + var history = JsonConvert.DeserializeObject>(json, _jsonSettings); + if (history != null) + { + lock (_cachedHistory) + { + _cachedHistory = history; + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "加载触发器历史记录失败"); + } + } + + private async Task SaveHistoryAsync() + { + try + { + string json; + lock (_cachedHistory) + { + // 保持最大记录数 + if (_cachedHistory.Count > MaxHistoryCount) + { + _cachedHistory = _cachedHistory.OrderByDescending(x => x.StartTime).Take(MaxHistoryCount).ToList(); + } + json = JsonConvert.SerializeObject(_cachedHistory, _jsonSettings); + } + await File.WriteAllTextAsync(_historyFilePath, json); + HistoryChanged?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + _logger.LogError(ex, "保存触发器历史记录失败"); + } + } + + public async Task AddRecordAsync(TriggerExecutionRecord record) + { + lock (_cachedHistory) + { + _cachedHistory.Insert(0, record); + } + await SaveHistoryAsync(); + } + + public async Task UpdateRecordAsync(TriggerExecutionRecord record) + { + lock (_cachedHistory) + { + var index = _cachedHistory.FindIndex(r => r.Id == record.Id); + if (index != -1) + { + _cachedHistory[index] = record; + } + } + await SaveHistoryAsync(); + } + + public async Task> GetHistoryAsync() + { + // 如果缓存为空但文件存在,尝试重新加载 + if (_cachedHistory.Count == 0 && File.Exists(_historyFilePath)) + { + await LoadHistoryFromFileAsync(); + } + + lock (_cachedHistory) + { + return _cachedHistory.OrderByDescending(x => x.StartTime).ToList(); + } + } + + public async Task ClearHistoryAsync() + { + lock (_cachedHistory) + { + _cachedHistory.Clear(); + } + await SaveHistoryAsync(); + } +} diff --git a/BetterGenshinImpact/View/Pages/GearTriggerPage.xaml b/BetterGenshinImpact/View/Pages/GearTriggerPage.xaml index 20e102a7..0704debf 100644 --- a/BetterGenshinImpact/View/Pages/GearTriggerPage.xaml +++ b/BetterGenshinImpact/View/Pages/GearTriggerPage.xaml @@ -8,6 +8,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:pages="clr-namespace:BetterGenshinImpact.ViewModel.Pages" xmlns:component="clr-namespace:BetterGenshinImpact.ViewModel.Pages.Component" + xmlns:triggers="clr-namespace:BetterGenshinImpact.Model.Gear.Triggers" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" d:DataContext="{d:DesignInstance Type=pages:GearTriggerPageViewModel}" d:DesignHeight="850" @@ -277,104 +278,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Appearance="Secondary" + Padding="8,4" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/BetterGenshinImpact/ViewModel/Pages/Component/GearTriggerViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/Component/GearTriggerViewModel.cs index 9fe17a85..8764c1f6 100644 --- a/BetterGenshinImpact/ViewModel/Pages/Component/GearTriggerViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/Component/GearTriggerViewModel.cs @@ -6,6 +6,7 @@ using BetterGenshinImpact.Model.Gear.Triggers; using BetterGenshinImpact.Model; using BetterGenshinImpact.Helpers.Extensions; using CommunityToolkit.Mvvm.ComponentModel; +using Quartz; namespace BetterGenshinImpact.ViewModel.Pages.Component; @@ -26,6 +27,11 @@ public partial class GearTriggerViewModel : ObservableObject [ObservableProperty] private string? _cronExpression; + partial void OnCronExpressionChanged(string? value) + { + UpdateNextRunTime(); + } + [ObservableProperty] private DateTime _createdTime = DateTime.Now; @@ -60,6 +66,36 @@ public partial class GearTriggerViewModel : ObservableObject /// public string HotkeyTypeName => HotkeyType.ToChineseName(); + [ObservableProperty] + private DateTime? _nextRunTime; + + [ObservableProperty] + private DateTime? _lastRunTime; + + [ObservableProperty] + private TriggerExecutionStatus? _lastRunStatus; + + public void UpdateNextRunTime() + { + if (TriggerType == TriggerType.Timed && !string.IsNullOrWhiteSpace(CronExpression)) + { + try + { + var cron = new CronExpression(CronExpression); + var next = cron.GetNextValidTimeAfter(DateTimeOffset.Now); + NextRunTime = next?.LocalDateTime; + } + catch + { + NextRunTime = null; + } + } + else + { + NextRunTime = null; + } + } + public GearTriggerViewModel() { } diff --git a/BetterGenshinImpact/ViewModel/Pages/GearTriggerPageViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/GearTriggerPageViewModel.cs index dfce0a19..041a746c 100644 --- a/BetterGenshinImpact/ViewModel/Pages/GearTriggerPageViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/GearTriggerPageViewModel.cs @@ -29,16 +29,70 @@ public partial class GearTriggerPageViewModel : ViewModel [ObservableProperty] private GearTaskDefinitionViewModel? _selectedTaskDefinition; + + [ObservableProperty] + private ObservableCollection _executionHistory = new(); - public GearTriggerPageViewModel(ILogger logger, GearTriggerStorageService storageService) + private readonly ITriggerHistoryService _historyService; + private readonly LogReaderService _logReaderService; + + public GearTriggerPageViewModel(ILogger logger, GearTriggerStorageService storageService, ITriggerHistoryService historyService, LogReaderService logReaderService) { _logger = logger; _storageService = storageService; + _historyService = historyService; + _logReaderService = logReaderService; + + _historyService.HistoryChanged += OnHistoryChanged; + } + + private void OnHistoryChanged(object? sender, EventArgs e) + { + System.Windows.Application.Current.Dispatcher.Invoke(async () => + { + await LoadHistoryAsync(); + UpdateTriggerStatus(); + }); + } + + private async Task LoadHistoryAsync() + { + try + { + var history = await _historyService.GetHistoryAsync(); + ExecutionHistory.Clear(); + foreach (var record in history) + { + ExecutionHistory.Add(record); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "加载历史记录失败"); + } + } + + private void UpdateTriggerStatus() + { + var history = ExecutionHistory.ToList(); + + foreach (var trigger in TimedTriggers) + { + var lastRun = history.FirstOrDefault(x => x.TriggerName == trigger.Name); + if (lastRun != null) + { + trigger.LastRunTime = lastRun.StartTime; + trigger.LastRunStatus = lastRun.Status; + } + // 确保更新下次运行时间 + trigger.UpdateNextRunTime(); + } } public override void OnNavigatedTo() { _ = LoadTriggersAsync(); + _ = LoadHistoryAsync(); } /// @@ -55,6 +109,7 @@ public partial class GearTriggerPageViewModel : ViewModel foreach (var trigger in timedTriggers) { + trigger.UpdateNextRunTime(); TimedTriggers.Add(trigger); } @@ -63,6 +118,9 @@ public partial class GearTriggerPageViewModel : ViewModel HotkeyTriggers.Add(trigger); } + // 加载完触发器后更新状态 + UpdateTriggerStatus(); + _logger.LogInformation("已加载 {TimedCount} 个定时触发器和 {HotkeyCount} 个快捷键触发器", TimedTriggers.Count, HotkeyTriggers.Count); } @@ -71,6 +129,55 @@ public partial class GearTriggerPageViewModel : ViewModel _logger.LogError(ex, "加载触发器数据时发生错误"); } } + + [RelayCommand] + private async Task ClearHistory() + { + await _historyService.ClearHistoryAsync(); + } + + [RelayCommand] + private async Task ViewHistoryDetails(TriggerExecutionRecord? record) + { + if (record == null) return; + + var logContent = "正在加载日志..."; + if (!string.IsNullOrEmpty(record.CorrelationId)) + { + try + { + logContent = await _logReaderService.GetLogsForCorrelationIdAsync(record.CorrelationId, record.StartTime); + } + catch (Exception ex) + { + logContent = $"读取日志失败: {ex.Message}"; + } + } + else + { + logContent = string.IsNullOrEmpty(record.LogDetails) ? "无关联的日志记录 (可能是旧版本数据)" : record.LogDetails; + } + + var message = $""" + 触发器: {record.TriggerName} + 任务: {record.TaskName} + 开始时间: {record.StartTime} + 结束时间: {record.EndTime} + 耗时: {record.Duration.TotalSeconds:F2} 秒 + 状态: {record.Status} + CorrelationId: {record.CorrelationId} + + 简述: {record.Message} + + === 详细日志 === + {logContent} + """; + + // 这里可以使用更高级的弹窗,目前先用 MessageBox + // 为了更好的体验,建议后续改用专门的日志查看窗口 + System.Windows.MessageBox.Show(message, "执行详情"); + } + /// /// 保存触发器数据