feat: implement GearTaskStorageService for task definition persistence

This commit is contained in:
辉鸭蛋
2025-08-26 01:31:20 +08:00
parent d3768501b2
commit 7915140acb
5 changed files with 546 additions and 27 deletions

View File

@@ -136,6 +136,7 @@ public partial class App : Application
services.AddSingleton<HutaoNamedPipe>();
services.AddSingleton<BgiOnnxFactory>();
services.AddSingleton<OcrFactory>();
services.AddSingleton<GearTaskStorageService>();
// Configuration
//services.Configure<AppConfig>(context.Configuration.GetSection(nameof(AppConfig)));

View File

@@ -0,0 +1,65 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
namespace BetterGenshinImpact.Model.Gear;
/// <summary>
/// 任务定义数据模型,用于 JSON 序列化
/// </summary>
public class GearTaskDefinitionData
{
[JsonProperty("name")]
public string Name { get; set; } = string.Empty;
[JsonProperty("description")]
public string Description { get; set; } = string.Empty;
[JsonProperty("created_time")]
public DateTime CreatedTime { get; set; } = DateTime.Now;
[JsonProperty("modified_time")]
public DateTime ModifiedTime { get; set; } = DateTime.Now;
[JsonProperty("root_task")]
public GearTaskData? RootTask { get; set; }
}
/// <summary>
/// 任务数据模型,用于 JSON 序列化
/// </summary>
public class GearTaskData
{
[JsonProperty("name")]
public string Name { get; set; } = string.Empty;
[JsonProperty("description")]
public string Description { get; set; } = string.Empty;
[JsonProperty("task_type")]
public string TaskType { get; set; } = string.Empty;
[JsonProperty("is_enabled")]
public bool IsEnabled { get; set; } = true;
[JsonProperty("is_directory")]
public bool IsDirectory { get; set; } = false;
[JsonProperty("parameters")]
public string Parameters { get; set; } = "{}";
[JsonProperty("created_time")]
public DateTime CreatedTime { get; set; } = DateTime.Now;
[JsonProperty("modified_time")]
public DateTime ModifiedTime { get; set; } = DateTime.Now;
[JsonProperty("priority")]
public int Priority { get; set; } = 0;
[JsonProperty("tags")]
public string Tags { get; set; } = string.Empty;
[JsonProperty("children")]
public List<GearTaskData> Children { get; set; } = new();
}

View File

@@ -0,0 +1,304 @@
using BetterGenshinImpact.Model.Gear;
using BetterGenshinImpact.ViewModel.Pages.Component;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BetterGenshinImpact.Core.Config;
namespace BetterGenshinImpact.Service;
/// <summary>
/// 齿轮任务存储服务,负责任务定义的 JSON 持久化
/// </summary>
public class GearTaskStorageService
{
private readonly ILogger<GearTaskStorageService> _logger;
private readonly string _taskStoragePath;
private readonly JsonSerializerSettings _jsonSettings;
public GearTaskStorageService(ILogger<GearTaskStorageService> logger)
{
_logger = logger;
_taskStoragePath = Path.Combine(Global.Absolute("User"), "task_v2", "list");
// 确保目录存在
Directory.CreateDirectory(_taskStoragePath);
// 配置 JSON 序列化设置
_jsonSettings = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
DateFormatString = "yyyy-MM-dd HH:mm:ss",
NullValueHandling = NullValueHandling.Ignore
};
}
/// <summary>
/// 保存任务定义到 JSON 文件
/// </summary>
/// <param name="taskDefinition">任务定义</param>
/// <returns></returns>
public async Task SaveTaskDefinitionAsync(GearTaskDefinitionViewModel taskDefinition)
{
try
{
var data = ConvertToData(taskDefinition);
var fileName = GetSafeFileName(taskDefinition.Name) + ".json";
var filePath = Path.Combine(_taskStoragePath, fileName);
var json = JsonConvert.SerializeObject(data, _jsonSettings);
await File.WriteAllTextAsync(filePath, json);
_logger.LogInformation("任务定义 '{TaskName}' 已保存到 {FilePath}", taskDefinition.Name, filePath);
}
catch (Exception ex)
{
_logger.LogError(ex, "保存任务定义 '{TaskName}' 时发生错误", taskDefinition.Name);
throw;
}
}
/// <summary>
/// 从 JSON 文件加载任务定义
/// </summary>
/// <param name="fileName">文件名(不含扩展名)</param>
/// <returns></returns>
public async Task<GearTaskDefinitionViewModel?> LoadTaskDefinitionAsync(string fileName)
{
try
{
var filePath = Path.Combine(_taskStoragePath, fileName + ".json");
if (!File.Exists(filePath))
{
_logger.LogWarning("任务定义文件不存在: {FilePath}", filePath);
return null;
}
var json = await File.ReadAllTextAsync(filePath);
var data = JsonConvert.DeserializeObject<GearTaskDefinitionData>(json, _jsonSettings);
if (data == null)
{
_logger.LogWarning("无法反序列化任务定义文件: {FilePath}", filePath);
return null;
}
var viewModel = ConvertToViewModel(data);
_logger.LogInformation("任务定义 '{TaskName}' 已从 {FilePath} 加载", data.Name, filePath);
return viewModel;
}
catch (Exception ex)
{
_logger.LogError(ex, "加载任务定义文件 '{FileName}' 时发生错误", fileName);
throw;
}
}
/// <summary>
/// 加载所有任务定义
/// </summary>
/// <returns></returns>
public async Task<List<GearTaskDefinitionViewModel>> LoadAllTaskDefinitionsAsync()
{
var taskDefinitions = new List<GearTaskDefinitionViewModel>();
try
{
var jsonFiles = Directory.GetFiles(_taskStoragePath, "*.json");
foreach (var filePath in jsonFiles)
{
try
{
var json = await File.ReadAllTextAsync(filePath);
var data = JsonConvert.DeserializeObject<GearTaskDefinitionData>(json, _jsonSettings);
if (data != null)
{
var viewModel = ConvertToViewModel(data);
taskDefinitions.Add(viewModel);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "加载任务定义文件 '{FilePath}' 时发生错误", filePath);
}
}
_logger.LogInformation("已加载 {Count} 个任务定义", taskDefinitions.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "加载任务定义列表时发生错误");
}
return taskDefinitions;
}
/// <summary>
/// 删除任务定义文件
/// </summary>
/// <param name="taskName">任务名称</param>
/// <returns></returns>
public async Task DeleteTaskDefinitionAsync(string taskName)
{
try
{
var fileName = GetSafeFileName(taskName) + ".json";
var filePath = Path.Combine(_taskStoragePath, fileName);
if (File.Exists(filePath))
{
await Task.Run(() => File.Delete(filePath));
_logger.LogInformation("任务定义文件已删除: {FilePath}", filePath);
}
else
{
_logger.LogWarning("要删除的任务定义文件不存在: {FilePath}", filePath);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "删除任务定义 '{TaskName}' 时发生错误", taskName);
throw;
}
}
/// <summary>
/// 重命名任务定义文件
/// </summary>
/// <param name="oldName">旧名称</param>
/// <param name="newName">新名称</param>
/// <returns></returns>
public async Task RenameTaskDefinitionAsync(string oldName, string newName)
{
try
{
var oldFileName = GetSafeFileName(oldName) + ".json";
var newFileName = GetSafeFileName(newName) + ".json";
var oldFilePath = Path.Combine(_taskStoragePath, oldFileName);
var newFilePath = Path.Combine(_taskStoragePath, newFileName);
if (File.Exists(oldFilePath))
{
await Task.Run(() => File.Move(oldFilePath, newFilePath));
_logger.LogInformation("任务定义文件已重命名: {OldPath} -> {NewPath}", oldFilePath, newFilePath);
}
else
{
_logger.LogWarning("要重命名的任务定义文件不存在: {FilePath}", oldFilePath);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "重命名任务定义 '{OldName}' -> '{NewName}' 时发生错误", oldName, newName);
throw;
}
}
/// <summary>
/// 将 ViewModel 转换为数据模型
/// </summary>
/// <param name="viewModel">视图模型</param>
/// <returns></returns>
private GearTaskDefinitionData ConvertToData(GearTaskDefinitionViewModel viewModel)
{
return new GearTaskDefinitionData
{
Name = viewModel.Name,
Description = viewModel.Description,
CreatedTime = viewModel.CreatedTime,
ModifiedTime = viewModel.ModifiedTime,
RootTask = viewModel.RootTask != null ? ConvertTaskToData(viewModel.RootTask) : null
};
}
/// <summary>
/// 将任务 ViewModel 转换为数据模型
/// </summary>
/// <param name="viewModel">任务视图模型</param>
/// <returns></returns>
private GearTaskData ConvertTaskToData(GearTaskViewModel viewModel)
{
return new GearTaskData
{
Name = viewModel.Name,
Description = viewModel.Description,
TaskType = viewModel.TaskType,
IsEnabled = viewModel.IsEnabled,
IsDirectory = viewModel.IsDirectory,
Parameters = viewModel.Parameters,
CreatedTime = viewModel.CreatedTime,
ModifiedTime = viewModel.ModifiedTime,
Priority = viewModel.Priority,
Tags = viewModel.Tags,
Children = viewModel.Children.Select(ConvertTaskToData).ToList()
};
}
/// <summary>
/// 将数据模型转换为 ViewModel
/// </summary>
/// <param name="data">数据模型</param>
/// <returns></returns>
private GearTaskDefinitionViewModel ConvertToViewModel(GearTaskDefinitionData data)
{
var viewModel = new GearTaskDefinitionViewModel
{
Name = data.Name,
Description = data.Description,
CreatedTime = data.CreatedTime,
ModifiedTime = data.ModifiedTime,
RootTask = data.RootTask != null ? ConvertTaskToViewModel(data.RootTask) : null
};
return viewModel;
}
/// <summary>
/// 将任务数据模型转换为 ViewModel
/// </summary>
/// <param name="data">任务数据模型</param>
/// <returns></returns>
private GearTaskViewModel ConvertTaskToViewModel(GearTaskData data)
{
var viewModel = new GearTaskViewModel
{
Name = data.Name,
Description = data.Description,
TaskType = data.TaskType,
IsEnabled = data.IsEnabled,
IsDirectory = data.IsDirectory,
Parameters = data.Parameters,
CreatedTime = data.CreatedTime,
ModifiedTime = data.ModifiedTime,
Priority = data.Priority,
Tags = data.Tags
};
// 递归转换子任务
foreach (var childData in data.Children)
{
viewModel.Children.Add(ConvertTaskToViewModel(childData));
}
return viewModel;
}
/// <summary>
/// 获取安全的文件名(移除非法字符)
/// </summary>
/// <param name="name">原始名称</param>
/// <returns></returns>
private string GetSafeFileName(string name)
{
var invalidChars = Path.GetInvalidFileNameChars();
var safeName = string.Join("_", name.Split(invalidChars, StringSplitOptions.RemoveEmptyEntries));
return string.IsNullOrWhiteSpace(safeName) ? "unnamed_task" : safeName;
}
}

View File

@@ -7,6 +7,7 @@
xmlns:pages="clr-namespace:BetterGenshinImpact.ViewModel.Pages"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:component="clr-namespace:BetterGenshinImpact.ViewModel.Pages.Component"
xmlns:dd="urn:gong-wpf-dragdrop"
d:DataContext="{d:DesignInstance Type=pages:GearTaskListPageViewModel}"
d:DesignHeight="850"
d:DesignWidth="1200"
@@ -153,6 +154,9 @@
ItemTemplate="{StaticResource TaskDefinitionItemTemplate}"
Background="Transparent"
BorderThickness="0"
dd:DragDrop.IsDragSource="True"
dd:DragDrop.IsDropTarget="True"
dd:DragDrop.UseDefaultDragAdorner="True"
Padding="4">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
@@ -231,7 +235,11 @@
<ui:TreeListView BorderThickness="0"
ItemsSource="{Binding CurrentTaskTree}"
SelectedItem="{Binding SelectedTaskNode}">
SelectedItem="{Binding SelectedTaskNode}"
dd:DragDrop.IsDragSource="True"
dd:DragDrop.IsDropTarget="True"
dd:DragDrop.UseDefaultDragAdorner="True"
>
<ui:TreeListView.Columns>
<GridViewColumnCollection>
<ui:GridViewColumn Width="{Binding ActualWidth, ElementName=TreeColumnName}" Header="任务名称">

View File

@@ -10,6 +10,9 @@ using System.Windows;
using System.Linq;
using System;
using BetterGenshinImpact.ViewModel.Pages.Component;
using BetterGenshinImpact.Service;
using System.Threading.Tasks;
using System.Collections.Specialized;
namespace BetterGenshinImpact.ViewModel.Pages;
@@ -19,6 +22,7 @@ namespace BetterGenshinImpact.ViewModel.Pages;
public partial class GearTaskListPageViewModel : ViewModel
{
private readonly ILogger<GearTaskListPageViewModel> _logger;
private readonly GearTaskStorageService _storageService;
/// <summary>
/// 任务定义列表(左侧)
@@ -60,18 +64,61 @@ public partial class GearTaskListPageViewModel : ViewModel
"组合任务"
};
public GearTaskListPageViewModel(ILogger<GearTaskListPageViewModel> logger)
public GearTaskListPageViewModel(ILogger<GearTaskListPageViewModel> logger, GearTaskStorageService storageService)
{
_logger = logger;
_storageService = storageService;
InitializeData();
// 监听集合变化,实现自动保存
TaskDefinitions.CollectionChanged += OnTaskDefinitionsChanged;
}
/// <summary>
/// 任务定义集合变化时的处理
/// </summary>
private async void OnTaskDefinitionsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
// 当集合发生变化时,可以在这里处理自动保存等逻辑
// 例如:保存到配置文件等
}
/// <summary>
/// 初始化数据
/// </summary>
private void InitializeData()
private async void InitializeData()
{
try
{
// 从 JSON 文件加载任务定义
var loadedTasks = await _storageService.LoadAllTaskDefinitionsAsync();
foreach (var task in loadedTasks)
{
TaskDefinitions.Add(task);
// 为每个任务定义设置属性变化监听
SetupTaskDefinitionPropertyChanged(task);
}
// 如果没有加载到任何任务,创建一个示例任务
if (TaskDefinitions.Count == 0)
{
await CreateSampleTaskAsync();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "初始化任务数据时发生错误");
// 发生错误时创建示例任务
await CreateSampleTaskAsync();
}
}
/// <summary>
/// 创建示例任务
/// </summary>
private async Task CreateSampleTaskAsync()
{
// 创建示例数据
var sampleTask = new GearTaskDefinitionViewModel("示例任务组", "这是一个示例任务组");
if (sampleTask.RootTask != null)
{
@@ -85,6 +132,10 @@ public partial class GearTaskListPageViewModel : ViewModel
}
TaskDefinitions.Add(sampleTask);
SetupTaskDefinitionPropertyChanged(sampleTask);
// 保存示例任务到文件
await _storageService.SaveTaskDefinitionAsync(sampleTask);
}
/// <summary>
@@ -124,18 +175,22 @@ public partial class GearTaskListPageViewModel : ViewModel
/// 添加新的任务定义
/// </summary>
[RelayCommand]
private void AddTaskDefinition()
private async Task AddTaskDefinition()
{
var newTask = new GearTaskDefinitionViewModel($"新任务组 {TaskDefinitions.Count + 1}", "新创建的任务组");
TaskDefinitions.Add(newTask);
SetupTaskDefinitionPropertyChanged(newTask);
SelectedTaskDefinition = newTask;
// 自动保存到文件
await _storageService.SaveTaskDefinitionAsync(newTask);
}
/// <summary>
/// 删除任务定义
/// </summary>
[RelayCommand]
private void DeleteTaskDefinition(GearTaskDefinitionViewModel? taskDefinition)
private async Task DeleteTaskDefinition(GearTaskDefinitionViewModel? taskDefinition)
{
if (taskDefinition == null) return;
@@ -144,11 +199,15 @@ public partial class GearTaskListPageViewModel : ViewModel
if (result == MessageBoxResult.Yes)
{
var taskName = taskDefinition.Name;
TaskDefinitions.Remove(taskDefinition);
if (SelectedTaskDefinition == taskDefinition)
{
SelectedTaskDefinition = TaskDefinitions.FirstOrDefault();
}
// 删除对应的 JSON 文件
await _storageService.DeleteTaskDefinitionAsync(taskName);
}
}
@@ -156,21 +215,25 @@ public partial class GearTaskListPageViewModel : ViewModel
/// 重命名任务定义
/// </summary>
[RelayCommand]
private void RenameTaskDefinition(GearTaskDefinitionViewModel? taskDefinition)
private async Task RenameTaskDefinition(GearTaskDefinitionViewModel? taskDefinition)
{
if (taskDefinition == null) return;
// 这里可以弹出重命名对话框,暂时简单处理
var newName = "新名称"; // 简化处理,实际应该弹出输入框
if (!string.IsNullOrWhiteSpace(newName))
if (!string.IsNullOrWhiteSpace(newName) && newName != taskDefinition.Name)
{
var oldName = taskDefinition.Name;
taskDefinition.Name = newName;
taskDefinition.ModifiedTime = DateTime.Now;
if (taskDefinition.RootTask != null)
{
taskDefinition.RootTask.Name = newName;
}
// 重命名对应的 JSON 文件
await _storageService.RenameTaskDefinitionAsync(oldName, newName);
}
}
@@ -178,7 +241,7 @@ public partial class GearTaskListPageViewModel : ViewModel
/// 添加任务节点
/// </summary>
[RelayCommand]
private void AddTaskNode(string? taskType = null)
private async Task AddTaskNode(string? taskType = null)
{
if (SelectedTaskDefinition?.RootTask == null) return;
@@ -198,13 +261,16 @@ public partial class GearTaskListPageViewModel : ViewModel
}
SelectedTaskDefinition.ModifiedTime = DateTime.Now;
// 自动保存到文件
await _storageService.SaveTaskDefinitionAsync(SelectedTaskDefinition);
}
/// <summary>
/// 添加任务组
/// </summary>
[RelayCommand]
private void AddTaskGroup()
private async Task AddTaskGroup()
{
if (SelectedTaskDefinition?.RootTask == null) return;
@@ -223,13 +289,16 @@ public partial class GearTaskListPageViewModel : ViewModel
}
SelectedTaskDefinition.ModifiedTime = DateTime.Now;
// 自动保存到文件
await _storageService.SaveTaskDefinitionAsync(SelectedTaskDefinition);
}
/// <summary>
/// 删除任务节点
/// </summary>
[RelayCommand]
private void DeleteTaskNode(GearTaskViewModel? taskNode)
private async Task DeleteTaskNode(GearTaskViewModel? taskNode)
{
if (taskNode == null || SelectedTaskDefinition?.RootTask == null) return;
@@ -240,6 +309,9 @@ public partial class GearTaskListPageViewModel : ViewModel
{
RemoveTaskFromTree(SelectedTaskDefinition.RootTask, taskNode);
SelectedTaskDefinition.ModifiedTime = DateTime.Now;
// 自动保存到文件
await _storageService.SaveTaskDefinitionAsync(SelectedTaskDefinition);
}
}
@@ -266,46 +338,115 @@ public partial class GearTaskListPageViewModel : ViewModel
}
/// <summary>
/// 保存到JSON预留功能
/// 保存所有任务定义到JSON文件
/// </summary>
[RelayCommand]
private void SaveToJson()
private async Task SaveToJson()
{
try
{
var json = JsonSerializer.Serialize(TaskDefinitions, new JsonSerializerOptions
foreach (var taskDefinition in TaskDefinitions)
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
// 这里可以保存到配置文件
_logger.LogInformation("任务定义已序列化为JSON: {Json}", json);
await _storageService.SaveTaskDefinitionAsync(taskDefinition);
}
_logger.LogInformation("所有任务定义已保存到JSON文件");
MessageBox.Show("保存成功!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
_logger.LogError(ex, "保存任务定义到JSON时发生错误");
_logger.LogError(ex, "保存任务定义到JSON文件时发生错误");
MessageBox.Show($"保存失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 从JSON加载(预留功能)
/// 从JSON文件重新加载所有任务定义
/// </summary>
[RelayCommand]
private void LoadFromJson()
private async Task LoadFromJson()
{
try
{
// 这里可以从配置文件加载
_logger.LogInformation("从JSON加载任务定义功能待实现");
MessageBox.Show("加载功能待实现!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
TaskDefinitions.Clear();
var loadedTasks = await _storageService.LoadAllTaskDefinitionsAsync();
foreach (var task in loadedTasks)
{
TaskDefinitions.Add(task);
SetupTaskDefinitionPropertyChanged(task);
}
_logger.LogInformation("从JSON文件重新加载了 {Count} 个任务定义", loadedTasks.Count);
MessageBox.Show($"加载成功!共加载 {loadedTasks.Count} 个任务定义", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
_logger.LogError(ex, "从JSON加载任务定义时发生错误");
_logger.LogError(ex, "从JSON文件加载任务定义时发生错误");
MessageBox.Show($"加载失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 为任务定义设置属性变化监听器,实现自动保存
/// </summary>
private void SetupTaskDefinitionPropertyChanged(GearTaskDefinitionViewModel taskDefinition)
{
taskDefinition.PropertyChanged += async (sender, e) =>
{
if (sender is GearTaskDefinitionViewModel task)
{
try
{
await _storageService.SaveTaskDefinitionAsync(task);
}
catch (Exception ex)
{
_logger.LogError(ex, "自动保存任务定义 {TaskName} 时发生错误", task.Name);
}
}
};
// 为根任务及其所有子任务设置监听器
if (taskDefinition.RootTask != null)
{
SetupTaskPropertyChangeListener(taskDefinition.RootTask, taskDefinition);
}
}
/// <summary>
/// 递归为任务及其子任务设置属性变化监听器
/// </summary>
private void SetupTaskPropertyChangeListener(GearTaskViewModel task, GearTaskDefinitionViewModel parentDefinition)
{
task.PropertyChanged += async (sender, e) =>
{
try
{
parentDefinition.ModifiedTime = DateTime.Now;
await _storageService.SaveTaskDefinitionAsync(parentDefinition);
}
catch (Exception ex)
{
_logger.LogError(ex, "自动保存任务定义 {TaskName} 时发生错误", parentDefinition.Name);
}
};
// 为子任务设置监听器
foreach (var child in task.Children)
{
SetupTaskPropertyChangeListener(child, parentDefinition);
}
// 监听子任务集合变化
task.Children.CollectionChanged += (sender, e) =>
{
if (e.NewItems != null)
{
foreach (GearTaskViewModel newTask in e.NewItems)
{
SetupTaskPropertyChangeListener(newTask, parentDefinition);
}
}
};
}
}