添加定时任务的日志

This commit is contained in:
辉鸭蛋
2025-12-26 00:33:06 +08:00
parent 8e292fcda3
commit 9d799510a2
9 changed files with 675 additions and 120 deletions

View File

@@ -65,9 +65,10 @@ public partial class App : Application
services.AddSingleton<IRichTextBox>(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<OcrFactory>();
services.AddSingleton<GearTaskStorageService>();
services.AddSingleton<GearTriggerStorageService>();
services.AddSingleton<ITriggerHistoryService, TriggerHistoryService>();
services.AddSingleton<LogReaderService>();

View File

@@ -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<ITriggerHistoryService>();
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<GearTaskExecutor>();
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<GearTaskExecutor>();
await executor.ExecuteTaskDefinitionAsync(taskDefinitionName, context.CancellationToken);
_logger.LogInformation("触发器 {TriggerName} 的任务定义执行完成", triggerName);
}
catch (Exception ex)
{
_logger.LogError(ex, "执行定时任务时发生错误");
throw;
}
}

View File

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

View File

@@ -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<string> 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}";
}
}
}

View File

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

View File

@@ -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<List<TriggerExecutionRecord>> GetHistoryAsync();
Task ClearHistoryAsync();
}
public class TriggerHistoryService : ITriggerHistoryService
{
private readonly string _historyFilePath;
private readonly ILogger<TriggerHistoryService> _logger;
private readonly JsonSerializerSettings _jsonSettings;
private List<TriggerExecutionRecord> _cachedHistory;
private const int MaxHistoryCount = 200;
public event EventHandler? HistoryChanged;
public TriggerHistoryService(ILogger<TriggerHistoryService> 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<TriggerExecutionRecord>();
// 初始化时加载
_ = LoadHistoryFromFileAsync();
}
private async Task LoadHistoryFromFileAsync()
{
try
{
if (File.Exists(_historyFilePath))
{
var json = await File.ReadAllTextAsync(_historyFilePath);
var history = JsonConvert.DeserializeObject<List<TriggerExecutionRecord>>(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<List<TriggerExecutionRecord>> 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();
}
}

View File

@@ -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 @@
</Grid>
</Border>
</DataTemplate>
<!-- 历史记录表格头部模板 -->
<DataTemplate x:Key="HistoryTableHeaderTemplate">
<Border Padding="10,12" Background="Transparent">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80" /> <!-- 时间 -->
<ColumnDefinition Width="80" /> <!-- 任务 -->
<ColumnDefinition Width="50" /> <!-- 状态 -->
<ColumnDefinition Width="*" /> <!-- 信息 -->
</Grid.ColumnDefinitions>
<ui:TextBlock Grid.Column="0" Text="时间" FontWeight="SemiBold" HorizontalAlignment="Center" />
<ui:TextBlock Grid.Column="1" Text="任务" FontWeight="SemiBold" HorizontalAlignment="Center" />
<ui:TextBlock Grid.Column="2" Text="状态" FontWeight="SemiBold" HorizontalAlignment="Center" />
<ui:TextBlock Grid.Column="3" Text="信息" FontWeight="SemiBold" HorizontalAlignment="Center" />
</Grid>
</Border>
</DataTemplate>
<!-- 历史记录数据项模板 -->
<DataTemplate x:Key="HistoryItemTemplate" DataType="{x:Type triggers:TriggerExecutionRecord}">
<Border BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="1"
Padding="10,12"
Background="Transparent">
<Border.InputBindings>
<MouseBinding MouseAction="LeftDoubleClick"
Command="{Binding DataContext.ViewHistoryDetailsCommand, RelativeSource={RelativeSource AncestorType=ui:ListView}}"
CommandParameter="{Binding}" />
</Border.InputBindings>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80" />
<ColumnDefinition Width="80" />
<ColumnDefinition Width="50" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:TextBlock Grid.Column="0" Text="{Binding StartTime, StringFormat='HH:mm:ss'}" HorizontalAlignment="Center" VerticalAlignment="Center" />
<ui:TextBlock Grid.Column="1" Text="{Binding TaskName}" HorizontalAlignment="Center" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" />
<ui:TextBlock Grid.Column="2" Text="{Binding Status}" HorizontalAlignment="Center" VerticalAlignment="Center" />
<ui:TextBlock Grid.Column="3" Text="{Binding Message}" HorizontalAlignment="Left" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" ToolTip="{Binding Message}" Padding="5,0"/>
</Grid>
</Border>
</DataTemplate>
</ResourceDictionary>
</UserControl.Resources>
<!-- 简化的双Tab布局 -->
<ui:Border Background="{DynamicResource CardBackground}"
BorderBrush="{DynamicResource CardBorderBrush}"
BorderThickness="1"
CornerRadius="8"
Margin="10">
<TabControl Style="{StaticResource ConsistentTabControlStyle}">
<TabItem Header="定时触发" Style="{StaticResource ConsistentTabItemStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- 工具栏 -->
<Border Grid.Row="0"
Background="{DynamicResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
Padding="12,8">
<StackPanel Orientation="Horizontal">
<ui:Button Content="新增定时触发器"
Command="{Binding AddTimedTriggerCommand}"
Icon="{ui:SymbolIcon Add24}"
Appearance="Primary"
Margin="0,0,8,0" />
<ui:Button Content="删除选中项"
Command="{Binding DeleteTriggerCommand}"
<!-- 左右分栏布局 -->
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" /> <!-- 左侧触发器设置 -->
<ColumnDefinition Width="10" /> <!-- 分隔 -->
<ColumnDefinition Width="2*" /> <!-- 右侧执行历史 -->
</Grid.ColumnDefinitions>
<!-- 左侧:触发器设置 -->
<ui:Border Grid.Column="0"
Background="{DynamicResource CardBackground}"
BorderBrush="{DynamicResource CardBorderBrush}"
BorderThickness="1"
CornerRadius="8">
<TabControl Style="{StaticResource ConsistentTabControlStyle}">
<TabItem Header="定时触发" Style="{StaticResource ConsistentTabItemStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- 工具栏 -->
<Border Grid.Row="0"
Background="{DynamicResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
Padding="12,8">
<StackPanel Orientation="Horizontal">
<ui:Button Content="新增定时触发器"
Command="{Binding AddTimedTriggerCommand}"
Icon="{ui:SymbolIcon Add24}"
Appearance="Primary"
Margin="0,0,8,0" />
<ui:Button Content="删除选中项"
Command="{Binding DeleteTriggerCommand}"
Icon="{ui:SymbolIcon Delete24}"
Appearance="Secondary" />
</StackPanel>
</Border>
<!-- 固定表格头部 -->
<ContentPresenter Grid.Row="1"
ContentTemplate="{StaticResource TimedTableHeaderTemplate}" />
<!-- 可滚动的列表内容 -->
<ScrollViewer Grid.Row="2"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<ui:ListView ItemsSource="{Binding TimedTriggers}"
SelectedItem="{Binding SelectedTrigger, Mode=TwoWay}"
ItemTemplate="{StaticResource TimedTriggerItemTemplate}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
BorderThickness="0"
Margin="0" />
</ScrollViewer>
</Grid>
</TabItem>
<TabItem Header="快捷键触发" Style="{StaticResource ConsistentTabItemStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- 工具栏 -->
<Border Grid.Row="0"
Background="{DynamicResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
Padding="12,8">
<StackPanel Orientation="Horizontal">
<ui:Button Content="新增快捷键触发器"
Command="{Binding AddHotkeyTriggerCommand}"
Icon="{ui:SymbolIcon Add24}"
Appearance="Primary"
Margin="0,0,8,0" />
<ui:Button Content="删除选中项"
Command="{Binding DeleteTriggerCommand}"
Icon="{ui:SymbolIcon Delete24}"
Appearance="Secondary" />
</StackPanel>
</Border>
<!-- 固定表格头部 -->
<ContentPresenter Grid.Row="1"
ContentTemplate="{StaticResource TableHeaderTemplate}" />
<!-- 可滚动的列表内容 -->
<ScrollViewer Grid.Row="2"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<ui:ListView ItemsSource="{Binding HotkeyTriggers}"
SelectedItem="{Binding SelectedTrigger, Mode=TwoWay}"
ItemTemplate="{StaticResource TriggerItemTemplate}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
BorderThickness="0"
Margin="0" />
</ScrollViewer>
</Grid>
</TabItem>
</TabControl>
</ui:Border>
<!-- 右侧:执行历史 -->
<ui:Border Grid.Column="2"
Background="{DynamicResource CardBackground}"
BorderBrush="{DynamicResource CardBorderBrush}"
BorderThickness="1"
CornerRadius="8">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- 标题栏 -->
<Border Grid.Row="0"
Background="{DynamicResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
Padding="12,8">
<Grid>
<ui:TextBlock Text="执行历史"
FontWeight="SemiBold"
VerticalAlignment="Center"
FontSize="14"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<ui:Button Content="清空"
Command="{Binding ClearHistoryCommand}"
Icon="{ui:SymbolIcon Delete24}"
Appearance="Secondary" />
Appearance="Secondary"
Padding="8,4" />
</StackPanel>
</Border>
<!-- 固定表格头部 -->
<ContentPresenter Grid.Row="1"
ContentTemplate="{StaticResource TimedTableHeaderTemplate}" />
<!-- 可滚动的列表内容 -->
<ScrollViewer Grid.Row="2"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<ui:ListView ItemsSource="{Binding TimedTriggers}"
SelectedItem="{Binding SelectedTrigger, Mode=TwoWay}"
ItemTemplate="{StaticResource TimedTriggerItemTemplate}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
BorderThickness="0"
Margin="0" />
</ScrollViewer>
</Grid>
</TabItem>
<TabItem Header="快捷键触发" Style="{StaticResource ConsistentTabItemStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- 工具栏 -->
<Border Grid.Row="0"
Background="{DynamicResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
Padding="12,8">
<StackPanel Orientation="Horizontal">
<ui:Button Content="新增快捷键触发器"
Command="{Binding AddHotkeyTriggerCommand}"
Icon="{ui:SymbolIcon Add24}"
Appearance="Primary"
Margin="0,0,8,0" />
<ui:Button Content="删除选中项"
Command="{Binding DeleteTriggerCommand}"
Icon="{ui:SymbolIcon Delete24}"
Appearance="Secondary" />
</StackPanel>
</Border>
<!-- 固定表格头部 -->
<ContentPresenter Grid.Row="1"
ContentTemplate="{StaticResource TableHeaderTemplate}" />
<!-- 可滚动的列表内容 -->
<ScrollViewer Grid.Row="2"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<ui:ListView ItemsSource="{Binding HotkeyTriggers}"
SelectedItem="{Binding SelectedTrigger, Mode=TwoWay}"
ItemTemplate="{StaticResource TriggerItemTemplate}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
BorderThickness="0"
Margin="0" />
</ScrollViewer>
</Grid>
</TabItem>
</TabControl>
</ui:Border>
</Grid>
</Border>
<!-- 固定表格头部 -->
<ContentPresenter Grid.Row="1"
ContentTemplate="{StaticResource HistoryTableHeaderTemplate}" />
<!-- 可滚动的列表内容 -->
<ScrollViewer Grid.Row="2"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<ui:ListView ItemsSource="{Binding ExecutionHistory}"
ItemTemplate="{StaticResource HistoryItemTemplate}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
BorderThickness="0"
Margin="0" />
</ScrollViewer>
</Grid>
</ui:Border>
</Grid>
</UserControl>

View File

@@ -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
/// </summary>
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()
{
}

View File

@@ -29,16 +29,70 @@ public partial class GearTriggerPageViewModel : ViewModel
[ObservableProperty]
private GearTaskDefinitionViewModel? _selectedTaskDefinition;
[ObservableProperty]
private ObservableCollection<TriggerExecutionRecord> _executionHistory = new();
public GearTriggerPageViewModel(ILogger<GearTriggerPageViewModel> logger, GearTriggerStorageService storageService)
private readonly ITriggerHistoryService _historyService;
private readonly LogReaderService _logReaderService;
public GearTriggerPageViewModel(ILogger<GearTriggerPageViewModel> 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();
}
/// <summary>
@@ -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, "执行详情");
}
/// <summary>
/// 保存触发器数据