From 786f2715a7f91da8447e97b57ebf5c84efa42b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=89=E9=B8=AD=E8=9B=8B?= Date: Tue, 5 May 2026 03:05:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=84=E7=90=86=E5=BC=95=E7=94=A8=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E7=9A=84=E4=BB=BB=E5=8A=A1=EF=BC=8C=E5=A4=84=E7=90=86?= =?UTF-8?q?=E4=BB=93=E5=BA=93=E5=8F=98=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Script/ScriptRepoUpdater.cs | 232 +++++++++++++ .../Service/GearTask/ContainerGearTask.cs | 16 + .../Service/GearTask/ErrorGearTask.cs | 24 ++ .../Service/GearTask/GearTaskConverter.cs | 307 ++++++++++++++++-- .../GearTask/GearTaskExecutionManager.cs | 2 + .../Service/GearTask/GearTaskExecutor.cs | 1 + .../GearTask/PathingTaskSelectionViewModel.cs | 121 +++---- 7 files changed, 604 insertions(+), 99 deletions(-) create mode 100644 BetterGenshinImpact/Service/GearTask/ContainerGearTask.cs create mode 100644 BetterGenshinImpact/Service/GearTask/ErrorGearTask.cs diff --git a/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs b/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs index e324837b..9f0e90b8 100644 --- a/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs +++ b/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs @@ -1376,6 +1376,229 @@ public class ScriptRepoUpdater : Singleton return (Tree)repoEntry.Target; } + /// + /// 获取仓库内指定相对目录下的直接子项(目录和文件)。 + /// relDir 为相对于 repo/ 的路径,例如 "pathing"、"pathing/璃月"。 + /// + public List GetChildrenFromCenterRepo(string relDir) + { + var result = new List(); + try + { + var normalizedRelDir = NormalizeRepoRelativePath(relDir); + var repoPath = CenterRepoPath; + + if (IsGitRepository(repoPath)) + { + using var repo = new Repository(repoPath); + var rootTree = GetRepoSubdirectoryTree(repo); + if (!TryGetTreeByRelativePath(rootTree, normalizedRelDir, out var targetTree)) + { + return result; + } + + foreach (var entry in targetTree) + { + if (entry.TargetType != TreeEntryTargetType.Tree && entry.TargetType != TreeEntryTargetType.Blob) + { + continue; + } + + var childRelPath = string.IsNullOrEmpty(normalizedRelDir) + ? entry.Name + : $"{normalizedRelDir}/{entry.Name}"; + + result.Add(new RepoTreeEntryInfo + { + Name = entry.Name, + RelativePath = childRelPath, + IsDirectory = entry.TargetType == TreeEntryTargetType.Tree + }); + } + } + else + { + var dirPath = string.IsNullOrEmpty(normalizedRelDir) + ? Path.Combine(repoPath, "repo") + : Path.Combine(repoPath, "repo", normalizedRelDir); + + if (!Directory.Exists(dirPath)) + { + return result; + } + + foreach (var dir in Directory.GetDirectories(dirPath)) + { + var name = Path.GetFileName(dir); + var childRelPath = string.IsNullOrEmpty(normalizedRelDir) + ? name + : $"{normalizedRelDir}/{name}"; + result.Add(new RepoTreeEntryInfo + { + Name = name, + RelativePath = childRelPath, + IsDirectory = true + }); + } + + foreach (var file in Directory.GetFiles(dirPath)) + { + var name = Path.GetFileName(file); + var childRelPath = string.IsNullOrEmpty(normalizedRelDir) + ? name + : $"{normalizedRelDir}/{name}"; + result.Add(new RepoTreeEntryInfo + { + Name = name, + RelativePath = childRelPath, + IsDirectory = false + }); + } + } + + return result + .OrderByDescending(x => x.IsDirectory) + .ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase) + .ToList(); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取仓库子项失败: {RelDir}", relDir); + return result; + } + } + + /// + /// 判断仓库内相对目录是否存在。 + /// relDir 为相对于 repo/ 的路径。 + /// + public bool DirectoryExistsInCenterRepo(string relDir) + { + try + { + var normalizedRelDir = NormalizeRepoRelativePath(relDir); + var repoPath = CenterRepoPath; + + if (IsGitRepository(repoPath)) + { + using var repo = new Repository(repoPath); + var rootTree = GetRepoSubdirectoryTree(repo); + return TryGetTreeByRelativePath(rootTree, normalizedRelDir, out _); + } + + var dirPath = string.IsNullOrEmpty(normalizedRelDir) + ? Path.Combine(repoPath, "repo") + : Path.Combine(repoPath, "repo", normalizedRelDir); + return Directory.Exists(dirPath); + } + catch (Exception ex) + { + _logger.LogError(ex, "判断仓库目录是否存在失败: {RelDir}", relDir); + return false; + } + } + + /// + /// 统计仓库内指定目录下匹配扩展名的文件数量。 + /// relDir 为相对于 repo/ 的路径,支持递归统计。 + /// + public int CountFilesInCenterRepo(string relDir, string extensionWithDot, bool recursive) + { + try + { + var normalizedRelDir = NormalizeRepoRelativePath(relDir); + var extension = extensionWithDot.StartsWith(".") + ? extensionWithDot + : "." + extensionWithDot; + var repoPath = CenterRepoPath; + + if (IsGitRepository(repoPath)) + { + using var repo = new Repository(repoPath); + var rootTree = GetRepoSubdirectoryTree(repo); + if (!TryGetTreeByRelativePath(rootTree, normalizedRelDir, out var targetTree)) + { + return 0; + } + + return recursive + ? CountFilesByExtensionInTreeRecursive(targetTree, extension) + : targetTree.Count(e => e.TargetType == TreeEntryTargetType.Blob + && e.Name.EndsWith(extension, StringComparison.OrdinalIgnoreCase)); + } + + var dirPath = string.IsNullOrEmpty(normalizedRelDir) + ? Path.Combine(repoPath, "repo") + : Path.Combine(repoPath, "repo", normalizedRelDir); + if (!Directory.Exists(dirPath)) + { + return 0; + } + + var option = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + return Directory.GetFiles(dirPath, "*" + extension, option).Length; + } + catch (Exception ex) + { + _logger.LogError(ex, "统计仓库文件失败: {RelDir}", relDir); + return 0; + } + } + + private static string NormalizeRepoRelativePath(string relPath) + { + if (string.IsNullOrWhiteSpace(relPath)) + { + return string.Empty; + } + + var normalized = relPath.Replace('\\', '/').Trim('/'); + return normalized; + } + + private static bool TryGetTreeByRelativePath(Tree rootTree, string relDir, out Tree tree) + { + tree = rootTree; + if (string.IsNullOrEmpty(relDir)) + { + return true; + } + + var parts = relDir.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var part in parts) + { + var entry = tree[part]; + if (entry == null || entry.TargetType != TreeEntryTargetType.Tree) + { + return false; + } + tree = (Tree)entry.Target; + } + + return true; + } + + private static int CountFilesByExtensionInTreeRecursive(Tree tree, string extensionWithDot) + { + int count = 0; + foreach (var entry in tree) + { + if (entry.TargetType == TreeEntryTargetType.Blob) + { + if (entry.Name.EndsWith(extensionWithDot, StringComparison.OrdinalIgnoreCase)) + { + count++; + } + } + else if (entry.TargetType == TreeEntryTargetType.Tree) + { + count += CountFilesByExtensionInTreeRecursive((Tree)entry.Target, extensionWithDot); + } + } + + return count; + } + /// /// 从中央仓库读取文件内容 /// @@ -3339,3 +3562,12 @@ public class ScriptRepoUpdater : Singleton } } } + +public sealed class RepoTreeEntryInfo +{ + public string Name { get; init; } = string.Empty; + + public string RelativePath { get; init; } = string.Empty; + + public bool IsDirectory { get; init; } +} diff --git a/BetterGenshinImpact/Service/GearTask/ContainerGearTask.cs b/BetterGenshinImpact/Service/GearTask/ContainerGearTask.cs new file mode 100644 index 00000000..34d84f9a --- /dev/null +++ b/BetterGenshinImpact/Service/GearTask/ContainerGearTask.cs @@ -0,0 +1,16 @@ +using System.Threading; +using System.Threading.Tasks; +using BetterGenshinImpact.Model.Gear.Tasks; + +namespace BetterGenshinImpact.Service; + +/// +/// 容器任务,用于目录类型或禁用任务的结构承载,不执行实际逻辑。 +/// +internal class ContainerGearTask : BaseGearTask +{ + public override Task Run(CancellationToken ct) + { + return Task.CompletedTask; + } +} diff --git a/BetterGenshinImpact/Service/GearTask/ErrorGearTask.cs b/BetterGenshinImpact/Service/GearTask/ErrorGearTask.cs new file mode 100644 index 00000000..d74b62db --- /dev/null +++ b/BetterGenshinImpact/Service/GearTask/ErrorGearTask.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using BetterGenshinImpact.Model.Gear.Tasks; + +namespace BetterGenshinImpact.Service; + +/// +/// 错误占位任务,执行时抛出创建阶段捕获到的异常信息。 +/// +internal class ErrorGearTask : BaseGearTask +{ + private readonly string _errorMessage; + + public ErrorGearTask(string errorMessage) + { + _errorMessage = errorMessage; + } + + public override Task Run(CancellationToken ct) + { + throw new InvalidOperationException($"任务转换失败: {_errorMessage}"); + } +} diff --git a/BetterGenshinImpact/Service/GearTask/GearTaskConverter.cs b/BetterGenshinImpact/Service/GearTask/GearTaskConverter.cs index 14464144..0ca2c702 100644 --- a/BetterGenshinImpact/Service/GearTask/GearTaskConverter.cs +++ b/BetterGenshinImpact/Service/GearTask/GearTaskConverter.cs @@ -3,9 +3,12 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.IO; using BetterGenshinImpact.Model.Gear; using BetterGenshinImpact.Model.Gear.Tasks; using BetterGenshinImpact.Model.Gear.Parameter; +using BetterGenshinImpact.Core.Script; +using BetterGenshinImpact.Core.Config; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -16,8 +19,12 @@ namespace BetterGenshinImpact.Service; /// public class GearTaskConverter { + private const string PathingRepoFolderPlaceholder = "{pathingRepoFolder}"; + private readonly ILogger _logger; private readonly GearTaskFactory _taskFactory; + private readonly object _mirrorLock = new(); + private bool _pathingRepoMirrorInitialized; public GearTaskConverter(ILogger logger, GearTaskFactory taskFactory) { @@ -78,6 +85,11 @@ public class GearTaskConverter // 如果是目录类型或者任务被禁用,创建容器任务 if (taskData.IsDirectory || !taskData.IsEnabled) { + if (taskData.IsDirectory && taskData.IsEnabled) + { + MaterializePathingReferenceIfNeeded(taskData); + } + task = new ContainerGearTask { Name = taskData.Name, @@ -92,7 +104,8 @@ public class GearTaskConverter else { // 使用工厂创建具体的任务实例 - task = await _taskFactory.CreateTaskAsync(taskData); + var preparedTaskData = PrepareTaskDataForExecution(taskData); + task = await _taskFactory.CreateTaskAsync(preparedTaskData); task.Father = parent; _logger.LogDebug("创建具体任务: {TaskName} ({TaskType})", taskData.Name, taskData.TaskType); @@ -286,6 +299,268 @@ public class GearTaskConverter } return count; } + + private void MaterializePathingReferenceIfNeeded(GearTaskData taskData) + { + if (taskData.Children.Count > 0 || string.IsNullOrWhiteSpace(taskData.Path)) + { + return; + } + + if (!TryExtractPathingRepoRelativePath(taskData.Path, out var repoRelativePath)) + { + return; + } + + var children = BuildPathingReferenceChildren(repoRelativePath); + if (children.Count == 0) + { + _logger.LogWarning("引用目录为空或不存在: {Path}", taskData.Path); + return; + } + + taskData.Children = children; + _logger.LogDebug("已展开地图追踪引用节点: {NodeName}, 子节点数量: {ChildCount}", taskData.Name, children.Count); + } + + private List BuildPathingReferenceChildren(string repoRelativePath) + { + var result = new List(); + var children = ScriptRepoUpdater.Instance.GetChildrenFromCenterRepo(repoRelativePath); + foreach (var entry in children) + { + if (entry.IsDirectory) + { + result.Add(new GearTaskData + { + Name = entry.Name, + TaskType = string.Empty, + IsEnabled = true, + IsDirectory = true, + Path = BuildPathingPlaceholderPath(entry.RelativePath, true), + Parameters = "{}", + }); + continue; + } + + if (!entry.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var executionPath = GetPathingExecutionFilePath(entry.RelativePath); + var parameters = new PathingGearTaskParams { Path = executionPath }; + result.Add(new GearTaskData + { + Name = Path.GetFileNameWithoutExtension(entry.Name), + TaskType = "Pathing", + IsEnabled = true, + IsDirectory = false, + Path = BuildPathingPlaceholderPath(entry.RelativePath, false), + Parameters = JsonConvert.SerializeObject(parameters), + }); + } + + return result; + } + + private GearTaskData PrepareTaskDataForExecution(GearTaskData taskData) + { + if (!string.Equals(taskData.TaskType, "Pathing", StringComparison.OrdinalIgnoreCase)) + { + return taskData; + } + + var parameters = DeserializePathingParams(taskData.Parameters); + if (!string.IsNullOrWhiteSpace(parameters.Path)) + { + return taskData; + } + + if (string.IsNullOrWhiteSpace(taskData.Path)) + { + return taskData; + } + + if (TryExtractPathingRepoRelativePath(taskData.Path, out var repoRelativePath) + && repoRelativePath.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) + { + parameters.Path = GetPathingExecutionFilePath(repoRelativePath); + } + else + { + parameters.Path = taskData.Path.Trim().TrimEnd('\\', '/'); + } + + return new GearTaskData + { + Name = taskData.Name, + TaskType = taskData.TaskType, + Path = taskData.Path, + IsEnabled = taskData.IsEnabled, + IsDirectory = taskData.IsDirectory, + IsExpanded = taskData.IsExpanded, + Parameters = JsonConvert.SerializeObject(parameters), + CreatedTime = taskData.CreatedTime, + ModifiedTime = taskData.ModifiedTime, + Priority = taskData.Priority, + Children = taskData.Children + }; + } + + private PathingGearTaskParams DeserializePathingParams(string? parametersJson) + { + if (string.IsNullOrWhiteSpace(parametersJson)) + { + return new PathingGearTaskParams(); + } + + try + { + return JsonConvert.DeserializeObject(parametersJson) ?? new PathingGearTaskParams(); + } + catch + { + return new PathingGearTaskParams(); + } + } + + private static string BuildPathingPlaceholderPath(string repoRelativePath, bool isDirectory) + { + var normalized = repoRelativePath.Replace('/', Path.DirectorySeparatorChar); + if (normalized.StartsWith("pathing\\", StringComparison.OrdinalIgnoreCase)) + { + normalized = normalized["pathing\\".Length..]; + } + else if (string.Equals(normalized, "pathing", StringComparison.OrdinalIgnoreCase)) + { + normalized = string.Empty; + } + + var path = string.IsNullOrEmpty(normalized) + ? PathingRepoFolderPlaceholder + : $@"{PathingRepoFolderPlaceholder}\{normalized}"; + + if (isDirectory && !path.EndsWith('\\')) + { + path += "\\"; + } + + return path; + } + + private bool TryExtractPathingRepoRelativePath(string sourcePath, out string repoRelativePath) + { + repoRelativePath = string.Empty; + if (string.IsNullOrWhiteSpace(sourcePath)) + { + return false; + } + + var normalized = sourcePath.Replace('\\', '/').Trim(); + if (!normalized.StartsWith(PathingRepoFolderPlaceholder, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + var relative = normalized[PathingRepoFolderPlaceholder.Length..].Trim('/'); + repoRelativePath = string.IsNullOrEmpty(relative) + ? "pathing" + : $"pathing/{relative}"; + return true; + } + + private string GetPathingExecutionFilePath(string repoRelativeJsonPath) + { + EnsurePathingRepoMirrorInitialized(); + + var normalized = repoRelativeJsonPath.Replace('\\', '/').Trim('/'); + var relativeUnderPathing = normalized.StartsWith("pathing/", StringComparison.OrdinalIgnoreCase) + ? normalized["pathing/".Length..] + : normalized; + + var mirrorRoot = GetPathingRepoMirrorRoot(); + var target = Path.Combine(mirrorRoot, relativeUnderPathing.Replace('/', Path.DirectorySeparatorChar)); + if (File.Exists(target)) + { + return target; + } + + // 兜底:镜像中不存在时按需写入 + var content = ScriptRepoUpdater.Instance.ReadFileFromCenterRepo(normalized); + if (string.IsNullOrWhiteSpace(content)) + { + throw new FileNotFoundException($"仓库中不存在地图追踪文件: {normalized}"); + } + + var dir = Path.GetDirectoryName(target); + if (!string.IsNullOrEmpty(dir)) + { + Directory.CreateDirectory(dir); + } + File.WriteAllText(target, content); + return target; + } + + private void EnsurePathingRepoMirrorInitialized() + { + if (_pathingRepoMirrorInitialized) + { + return; + } + + lock (_mirrorLock) + { + if (_pathingRepoMirrorInitialized) + { + return; + } + + var mirrorRoot = GetPathingRepoMirrorRoot(); + if (Directory.Exists(mirrorRoot)) + { + Directory.Delete(mirrorRoot, true); + } + Directory.CreateDirectory(mirrorRoot); + + MirrorPathingJsonRecursively("pathing", mirrorRoot); + _pathingRepoMirrorInitialized = true; + } + } + + private void MirrorPathingJsonRecursively(string repoRelativePath, string localPath) + { + var entries = ScriptRepoUpdater.Instance.GetChildrenFromCenterRepo(repoRelativePath); + foreach (var entry in entries) + { + if (entry.IsDirectory) + { + var dirPath = Path.Combine(localPath, entry.Name); + Directory.CreateDirectory(dirPath); + MirrorPathingJsonRecursively(entry.RelativePath, dirPath); + continue; + } + + if (!entry.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var content = ScriptRepoUpdater.Instance.ReadFileFromCenterRepo(entry.RelativePath); + if (string.IsNullOrWhiteSpace(content)) + { + continue; + } + + var filePath = Path.Combine(localPath, entry.Name); + File.WriteAllText(filePath, content); + } + } + + private static string GetPathingRepoMirrorRoot() + { + return Global.Absolute(@"User\Temp\GearTask\PathingRepoMirror"); + } } /// @@ -297,33 +572,3 @@ public class TaskValidationResult public List Errors { get; set; } = new(); public List Warnings { get; set; } = new(); } - -/// -/// 容器任务,用于目录类型或禁用的任务 -/// -internal class ContainerGearTask : BaseGearTask -{ - public override async Task Run(CancellationToken ct) - { - // 容器任务本身不执行任何操作,只是作为子任务的容器 - await Task.CompletedTask; - } -} - -/// -/// 错误任务,用于转换失败的任务占位符 -/// -internal class ErrorGearTask : BaseGearTask -{ - private readonly string _errorMessage; - - public ErrorGearTask(string errorMessage) - { - _errorMessage = errorMessage; - } - - public override async Task Run(CancellationToken ct) - { - throw new InvalidOperationException($"任务转换失败: {_errorMessage}"); - } -} \ No newline at end of file diff --git a/BetterGenshinImpact/Service/GearTask/GearTaskExecutionManager.cs b/BetterGenshinImpact/Service/GearTask/GearTaskExecutionManager.cs index dcde6029..5825a153 100644 --- a/BetterGenshinImpact/Service/GearTask/GearTaskExecutionManager.cs +++ b/BetterGenshinImpact/Service/GearTask/GearTaskExecutionManager.cs @@ -149,6 +149,8 @@ public partial class GearTaskExecutionManager : ObservableObject // 初始化执行信息 InitializeExecutionInfo(rootTask); + await ScriptService.StartGameTask(); + // 开始执行 await ExecuteTaskWithTrackingAsync(rootTask, _cancellationTokenSource.Token); diff --git a/BetterGenshinImpact/Service/GearTask/GearTaskExecutor.cs b/BetterGenshinImpact/Service/GearTask/GearTaskExecutor.cs index 74bfbd24..6a05d476 100644 --- a/BetterGenshinImpact/Service/GearTask/GearTaskExecutor.cs +++ b/BetterGenshinImpact/Service/GearTask/GearTaskExecutor.cs @@ -253,6 +253,7 @@ public partial class GearTaskExecutor : ObservableObject { Name = viewModel.Name, TaskType = viewModel.TaskType, + Path = viewModel.Path, IsEnabled = viewModel.IsEnabled, IsDirectory = viewModel.IsDirectory, Parameters = viewModel.Parameters, diff --git a/BetterGenshinImpact/ViewModel/Windows/GearTask/PathingTaskSelectionViewModel.cs b/BetterGenshinImpact/ViewModel/Windows/GearTask/PathingTaskSelectionViewModel.cs index 868a8d6d..9bef1880 100644 --- a/BetterGenshinImpact/ViewModel/Windows/GearTask/PathingTaskSelectionViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Windows/GearTask/PathingTaskSelectionViewModel.cs @@ -114,15 +114,15 @@ public partial class PathingTaskSelectionViewModel : ViewModel { PathingTasks.Clear(); - var pathingPath = Path.Combine(ScriptRepoUpdater.CenterRepoPath, "repo", "pathing"); - if (!Directory.Exists(pathingPath)) + const string pathingRoot = "pathing"; + if (!ScriptRepoUpdater.Instance.DirectoryExistsInCenterRepo(pathingRoot)) { - _logger.LogWarning($"地图追踪任务目录不存在: {pathingPath}"); + _logger.LogWarning("地图追踪任务目录不存在: {PathingRoot}", pathingRoot); return; } - // 加载根目录下的直接子项 - LoadDirectChildrenFromDirectory(pathingPath, pathingPath, PathingTasks); + // 加载 pathing 根目录下的直接子项 + LoadDirectChildrenFromRepo(pathingRoot, PathingTasks); FilterTasks(); } catch (Exception ex) @@ -132,46 +132,37 @@ public partial class PathingTaskSelectionViewModel : ViewModel } /// - /// 从目录加载直接子项(用于构建层级结构) + /// 从仓库加载直接子项(用于构建层级结构) /// - private void LoadDirectChildrenFromDirectory(string directoryPath, string rootPath, ObservableCollection parentCollection) + private void LoadDirectChildrenFromRepo(string repoRelativePath, ObservableCollection parentCollection) { try { - // 加载文件夹 - foreach (var dir in Directory.GetDirectories(directoryPath)) + var children = ScriptRepoUpdater.Instance.GetChildrenFromCenterRepo(repoRelativePath); + foreach (var child in children) { - var taskInfo = new PathingTaskInfo(dir, rootPath) + var taskInfo = new PathingTaskInfo { - IsDirectory = true + Name = child.Name, + FolderName = Path.GetFileNameWithoutExtension(child.Name), + FullPath = child.RelativePath, + IsDirectory = child.IsDirectory, + ParentPath = GetParentPath(child.RelativePath), + RelativePath = TrimPathingRoot(child.RelativePath).Replace('/', Path.DirectorySeparatorChar) }; - // 设置图标 SetTaskIcon(taskInfo); - // 递归加载子目录到当前任务的Children集合中 - LoadDirectChildrenFromDirectory(dir, rootPath, taskInfo.Children); - - parentCollection.Add(taskInfo); - } - - // 加载JSON文件(默认展示到文件级别) - foreach (var file in Directory.GetFiles(directoryPath, "*.json")) - { - var taskInfo = new PathingTaskInfo(file, rootPath) + if (taskInfo.IsDirectory) { - IsDirectory = false - }; - - // 设置图标 - SetTaskIcon(taskInfo); - + LoadDirectChildrenFromRepo(child.RelativePath, taskInfo.Children); + } parentCollection.Add(taskInfo); } } catch (Exception ex) { - _logger.LogError(ex, $"加载目录任务失败: {directoryPath}"); + _logger.LogError(ex, "加载目录任务失败: {RepoRelativePath}", repoRelativePath); } } @@ -203,16 +194,13 @@ public partial class PathingTaskSelectionViewModel : ViewModel { if (taskInfo.IsDirectory && string.IsNullOrEmpty(taskInfo.ReadmeContent)) { - var readmePath = Path.Combine(taskInfo.FullPath, "README.md"); - if (File.Exists(readmePath)) - { - taskInfo.ReadmeContent = File.ReadAllText(readmePath); - } + var readmePath = $"{NormalizeRepoPath(taskInfo.FullPath)}/README.md"; + taskInfo.ReadmeContent = ScriptRepoUpdater.Instance.ReadFileFromCenterRepo(readmePath) ?? string.Empty; } } catch (Exception ex) { - _logger.LogError(ex, $"加载README内容失败: {taskInfo.FullPath}"); + _logger.LogError(ex, "加载README内容失败: {FullPath}", taskInfo.FullPath); taskInfo.ReadmeContent = "README加载失败"; } } @@ -226,7 +214,13 @@ public partial class PathingTaskSelectionViewModel : ViewModel { if (!taskInfo.IsDirectory && taskInfo.FullPath.EndsWith(".json") && string.IsNullOrEmpty(taskInfo.JsonContent)) { - var jsonContent = File.ReadAllText(taskInfo.FullPath); + var jsonContent = ScriptRepoUpdater.Instance.ReadFileFromCenterRepo(taskInfo.FullPath); + if (string.IsNullOrWhiteSpace(jsonContent)) + { + taskInfo.JsonContent = "JSON文件不存在"; + return; + } + // 格式化JSON var jsonObject = JsonConvert.DeserializeObject(jsonContent); taskInfo.JsonContent = JsonConvert.SerializeObject(jsonObject, Formatting.Indented); @@ -234,7 +228,7 @@ public partial class PathingTaskSelectionViewModel : ViewModel } catch (Exception ex) { - _logger.LogError(ex, $"加载JSON内容失败: {taskInfo.FullPath}"); + _logger.LogError(ex, "加载JSON内容失败: {FullPath}", taskInfo.FullPath); taskInfo.JsonContent = "JSON格式错误"; } } @@ -353,49 +347,40 @@ public partial class PathingTaskSelectionViewModel : ViewModel if (!directory.IsDirectory) return 0; - int count = 0; - - // 计算当前目录下的JSON文件数量 try { - count += Directory.GetFiles(directory.FullPath, "*.json").Length; - - // 递归计算子目录 - foreach (var subDir in Directory.GetDirectories(directory.FullPath)) - { - count += CountTasksInDirectoryPath(subDir); - } + return ScriptRepoUpdater.Instance.CountFilesInCenterRepo(directory.FullPath, ".json", recursive: true); } catch (Exception ex) { - _logger.LogError(ex, $"计算目录任务数量失败: {directory.FullPath}"); + _logger.LogError(ex, "计算目录任务数量失败: {FullPath}", directory.FullPath); + return 0; } - - return count; } - /// - /// 计算指定路径目录下的任务数量(递归) - /// - private int CountTasksInDirectoryPath(string directoryPath) + private static string NormalizeRepoPath(string path) { - int count = 0; + return path.Replace('\\', '/').Trim('/'); + } - try + private static string TrimPathingRoot(string repoRelativePath) + { + var normalized = NormalizeRepoPath(repoRelativePath); + if (normalized.StartsWith("pathing/", StringComparison.OrdinalIgnoreCase)) { - count += Directory.GetFiles(directoryPath, "*.json").Length; - - foreach (var subDir in Directory.GetDirectories(directoryPath)) - { - count += CountTasksInDirectoryPath(subDir); - } - } - catch (Exception ex) - { - _logger.LogError(ex, $"计算目录任务数量失败: {directoryPath}"); + return normalized["pathing/".Length..]; } - return count; + return string.Equals(normalized, "pathing", StringComparison.OrdinalIgnoreCase) + ? string.Empty + : normalized; + } + + private static string GetParentPath(string repoRelativePath) + { + var normalized = NormalizeRepoPath(repoRelativePath); + var idx = normalized.LastIndexOf('/'); + return idx > 0 ? normalized[..idx] : string.Empty; } /// @@ -594,4 +579,4 @@ public partial class PathingTaskSelectionViewModel : ViewModel return null; } } -} \ No newline at end of file +}