From 1add385e8a4de8252f8019afaa6523ed5d4909e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BA=81=E5=8A=A8=E7=9A=84=E6=B0=A8=E6=B0=94?= <131591012+zaodonganqi@users.noreply.github.com> Date: Sat, 2 Aug 2025 17:31:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=8F=E4=BC=98=E5=8C=96=E5=8F=8A=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E8=84=9A=E6=9C=AC=E4=BB=93=E5=BA=93=E6=A1=A5=E6=8E=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20(#1952)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Script/ScriptRepoUpdater.cs | 293 +++++++++++++++++- .../Core/Script/WebView/RepoWebBridge.cs | 186 ++++++++--- .../GameTask/Common/Job/ExitAndReloginJob.cs | 3 +- .../View/Windows/PromptDialog.xaml.cs | 8 +- .../ViewModel/Pages/ScriptControlViewModel.cs | 29 +- 5 files changed, 448 insertions(+), 71 deletions(-) diff --git a/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs b/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs index 84619c1d..5381778a 100644 --- a/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs +++ b/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs @@ -96,7 +96,6 @@ public class ScriptRepoUpdater : Singleton // } // } - public async Task<(string, bool)> UpdateCenterRepoByGit(string repoUrl, CheckoutProgressHandler? onCheckoutProgress) { if (string.IsNullOrEmpty(repoUrl)) @@ -107,6 +106,9 @@ public class ScriptRepoUpdater : Singleton var repoPath = Path.Combine(ReposPath, "bettergi-scripts-list-git"); var updated = false; + // 备份相关变量 + string? oldRepoJsonContent = null; + await Task.Run(() => { try @@ -124,7 +126,21 @@ public class ScriptRepoUpdater : Singleton } else { - // 仓库已经存在,执行拉取更新 + try + { + // 检测repo.json是否存在,存在则备份 + var oldRepoJsonPath = Directory + .GetFiles(CenterRepoPath, "repo.json", SearchOption.AllDirectories).FirstOrDefault(); + if (oldRepoJsonPath != null) + { + oldRepoJsonContent = File.ReadAllText(oldRepoJsonPath); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "备份repo.json失败,继续更新仓库"); + } + using var repo = new Repository(repoPath); // 检查远程URL是否需要更新 @@ -187,11 +203,161 @@ public class ScriptRepoUpdater : Singleton throw; } }); + // 如果仓库有更新且有备份内容,则标记新repo.json中的更新节点 + if (updated && !string.IsNullOrEmpty(oldRepoJsonContent)) + { + try + { + var newRepoJsonPath = Directory.GetFiles(CenterRepoPath, "repo.json", SearchOption.AllDirectories) + .FirstOrDefault(); + if (newRepoJsonPath != null) + { + var newRepoJsonContent = await File.ReadAllTextAsync(newRepoJsonPath); + var updatedContent = AddUpdateMarkersToNewRepo(oldRepoJsonContent, newRepoJsonContent); + + // 保存到同级目录 + var parentDir = Path.GetDirectoryName(repoPath); + var updatedRepoJsonPath = Path.Combine(parentDir!, "repo_updated.json"); + + await File.WriteAllTextAsync(updatedRepoJsonPath, updatedContent); + _logger.LogInformation($"已标记repo.json中的更新节点并保存到: {updatedRepoJsonPath}"); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "标记repo.json更新节点失败"); + } + } return (repoPath, updated); } - private static void SimpleCloneRepository(string repoUrl, string repoPath, CheckoutProgressHandler? onCheckoutProgress) + /// + /// 在新repo.json中添加更新标记 + /// + /// 旧版repo.json内容 + /// 新版repo.json内容 + /// 添加了hasUpdate标记的新repo.json内容 + private string AddUpdateMarkersToNewRepo(string oldContent, string newContent) + { + try + { + var oldJson = JObject.Parse(oldContent); + var newJson = JObject.Parse(newContent); + + if (oldJson["indexes"] is JArray oldIndexes && newJson["indexes"] is JArray newIndexes) + { + foreach (var newIndex in newIndexes) + { + if (newIndex is JObject newIndexObj) + { + MarkNodeUpdates(newIndexObj, oldIndexes); + } + } + } + + return newJson.ToString(Newtonsoft.Json.Formatting.Indented); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "标记repo.json更新失败,返回原内容"); + return newContent; + } + } + + /// + /// 直接在新版节点上标记更新 + /// + /// 新版节点 + /// 老版节点数组 + /// 是否有更新(节点本身或子树) + private bool MarkNodeUpdates(JObject newNode, JArray oldNodes) + { + var newName = newNode["name"]?.ToString(); + if (string.IsNullOrEmpty(newName)) + return false; + + // 在老版节点中查找对应的节点 + var oldNode = oldNodes.FirstOrDefault(n => n is JObject obj && obj["name"]?.ToString() == newName) as JObject; + + bool hasDirectUpdate = false; + bool hasChildUpdate = false; + + // 检查节点本身是否有更新 + if (oldNode != null) + { + // 对比时间戳 + var oldTime = ParseLastUpdated(oldNode["lastUpdated"]?.ToString()); + var newTime = ParseLastUpdated(newNode["lastUpdated"]?.ToString()); + + if (newTime > oldTime) + { + newNode["hasUpdate"] = true; + hasDirectUpdate = true; + } + } + else + { + newNode["hasUpdate"] = true; + hasDirectUpdate = true; + } + + // 处理子节点 + if (newNode["children"] is JArray newChildren && newChildren.Count > 0) + { + var oldChildren = oldNode?["children"] as JArray ?? new JArray(); + + // 遍历新版的每个子节点 + foreach (var newChild in newChildren) + { + if (newChild is JObject newChildObj) + { + bool childHasUpdate = MarkNodeUpdates(newChildObj, oldChildren); + if (childHasUpdate) + { + hasChildUpdate = true; + + // 如果是叶子节点更新,父节点也标记更新 + var isLeafChild = newChildObj["children"] == null || + !((JArray?)newChildObj["children"])?.Any() == true; + if (isLeafChild && !hasDirectUpdate && newChildObj["hasUpdate"] != null) + { + newNode["hasUpdate"] = true; + hasDirectUpdate = true; + } + } + } + } + } + + return hasDirectUpdate || hasChildUpdate; + } + + /// + /// 解析lastUpdated时间戳 + /// + /// 时间字符串 + /// DateTime对象 + private DateTime ParseLastUpdated(string? timeString) + { + if (string.IsNullOrEmpty(timeString)) + return DateTime.MinValue; + + try + { + if (DateTime.TryParse(timeString, out var result)) + return result; + + return DateTime.MinValue; + } + catch + { + return DateTime.MinValue; + } + } + + private static void SimpleCloneRepository(string repoUrl, string repoPath, + CheckoutProgressHandler? onCheckoutProgress) { var options = new CloneOptions { @@ -223,7 +389,7 @@ public class ScriptRepoUpdater : Singleton using var repo = new Repository(repoPath); GitConfig(repo); - + // 3. 添加远程源 Remote remote = repo.Network.Remotes.Add("origin", repoUrl); @@ -254,7 +420,7 @@ public class ScriptRepoUpdater : Singleton CheckoutOptions checkoutOptions = new CheckoutOptions { OnCheckoutProgress = onCheckoutProgress - }; + }; Commands.Checkout(repo, localBranch, checkoutOptions); } @@ -263,16 +429,15 @@ public class ScriptRepoUpdater : Singleton // 设置 Git 配置 // 1. 设置 core.longpaths 为 true repo.Config.Set("core.longpaths", true); - + // 2. 添加 safe.directory * repo.Config.Set("safe.directory", "*"); - + // 3. 移除 http.proxy 和 https.proxy 配置 repo.Config.Unset("http.proxy"); repo.Config.Unset("https.proxy"); } - // [Obsolete] // public async Task<(string, bool)> UpdateCenterRepo() // { @@ -570,7 +735,7 @@ public class ScriptRepoUpdater : Singleton // return; // } // } - + //顶层目录订阅时,不会删除其下,不在订阅中的文件夹 List newPaths = new List(); foreach (var path in paths) @@ -585,7 +750,7 @@ public class ScriptRepoUpdater : Singleton string[] directories = Directory.GetDirectories(scriptPath, "*", SearchOption.TopDirectoryOnly); foreach (var dir in directories) { - newPaths.Add("pathing"+"/"+Path.GetFileName(dir)); + newPaths.Add("pathing" + "/" + Path.GetFileName(dir)); } } else @@ -597,10 +762,8 @@ public class ScriptRepoUpdater : Singleton { newPaths.Add(path); } - } - // 拷贝文件 foreach (var path in newPaths) { @@ -672,15 +835,117 @@ public class ScriptRepoUpdater : Singleton continue; // 检查是否已被父路径覆盖 - bool isCoveredByParent = pathsToKeep.Any(p => + bool isCoveredByParent = pathsToKeep.Any(p => path.StartsWith(p + "/") || path == p); - + if (!isCoveredByParent) { pathsToKeep.Add(path); } } + // 添加父节点自动订阅逻辑 + try + { + // 获取所有可用路径 + var allAvailablePaths = new HashSet(); + var repoJsonPath = Directory.GetFiles(CenterRepoPath, "repo.json", SearchOption.AllDirectories).FirstOrDefault(); + if (!string.IsNullOrEmpty(repoJsonPath)) + { + var jsonContent = File.ReadAllText(repoJsonPath); + var jsonObj = JObject.Parse(jsonContent); + + if (jsonObj["indexes"] is JArray indexes) + { + // 递归收集所有路径 + void CollectPaths(JArray nodes, string currentPath) + { + foreach (var node in nodes) + { + if (node is JObject nodeObj) + { + var name = nodeObj["name"]?.ToString(); + if (!string.IsNullOrEmpty(name)) + { + var fullPath = string.IsNullOrEmpty(currentPath) ? name : $"{currentPath}/{name}"; + allAvailablePaths.Add(fullPath); + + if (nodeObj["children"] is JArray children) + { + CollectPaths(children, fullPath); + } + } + } + } + } + + CollectPaths(indexes, ""); + } + } + + // 如果父节点的所有直接子节点都已被订阅,则将父节点也加入订阅 + if (allAvailablePaths.Count > 0) + { + // 构建父子关系映射,只记录直接子节点 + var parentChildMap = new Dictionary>(); + + // 遍历所有路径,找到每个节点的父节点 + foreach (var path in allAvailablePaths) + { + var pathParts = path.Split('/'); + if (pathParts.Length > 1) + { + var parentPath = string.Join("/", pathParts.Take(pathParts.Length - 1)); + if (!parentChildMap.ContainsKey(parentPath)) + { + parentChildMap[parentPath] = new List(); + } + + if (!parentChildMap[parentPath].Contains(path)) + { + parentChildMap[parentPath].Add(path); + } + } + } + + // 递归检查父节点,直到没有新的父节点需要添加 + bool hasNewPaths; + do + { + hasNewPaths = false; + var pathsToAdd = new HashSet(); + + // 检查每个父节点 + foreach (var kvp in parentChildMap) + { + var parentPath = kvp.Key; + var directChildren = kvp.Value; + + // 检查所有直接子节点是否都已被订阅 + bool allDirectChildrenSubscribed = directChildren.All(child => + pathsToKeep.Contains(child)); + + // 如果所有直接子节点都已被订阅,且父节点本身未被订阅,则添加父节点 + if (allDirectChildrenSubscribed && !pathsToKeep.Contains(parentPath)) + { + pathsToAdd.Add(parentPath); + hasNewPaths = true; + } + } + + // 将需要添加的父节点加入订阅列表 + foreach (var pathToAdd in pathsToAdd) + { + pathsToKeep.Add(pathToAdd); + } + } while (hasNewPaths); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "添加父节点时发生错误"); + } + scriptConfig.SubscribedScriptPaths = pathsToKeep .OrderBy(path => path) .ToList(); diff --git a/BetterGenshinImpact/Core/Script/WebView/RepoWebBridge.cs b/BetterGenshinImpact/Core/Script/WebView/RepoWebBridge.cs index 4dc41f71..0fd1459b 100644 --- a/BetterGenshinImpact/Core/Script/WebView/RepoWebBridge.cs +++ b/BetterGenshinImpact/Core/Script/WebView/RepoWebBridge.cs @@ -1,12 +1,12 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; -using System.Security.Policy; using System.Threading.Tasks; using BetterGenshinImpact.ViewModel.Message; using CommunityToolkit.Mvvm.Messaging; -using Wpf.Ui.Violeta.Controls; +using Newtonsoft.Json.Linq; namespace BetterGenshinImpact.Core.Script.WebView; @@ -16,31 +16,31 @@ namespace BetterGenshinImpact.Core.Script.WebView; /// [ClassInterface(ClassInterfaceType.AutoDual)] [ComVisible(true)] -public class RepoWebBridge +public sealed class RepoWebBridge { + private static readonly HashSet AllowedTextExtensions = new(StringComparer.OrdinalIgnoreCase) + { + ".txt", ".md", ".json", ".js", ".ts", + ".vue", ".css", ".html", ".csv", ".xml", + ".yaml", ".yml", ".ini", ".config" + }; + public async Task GetRepoJson() { try { if (!Directory.Exists(ScriptRepoUpdater.CenterRepoPath)) { - throw new Exception("仓库文件夹不存在,请至少成功更新一次仓库!"); + throw new InvalidOperationException("仓库文件夹不存在,请至少成功更新一次仓库!"); } - var localRepoJsonPath = Directory - .GetFiles(ScriptRepoUpdater.CenterRepoPath, "repo.json", SearchOption.AllDirectories).FirstOrDefault(); - if (localRepoJsonPath is null) - { - throw new Exception("repo.json 仓库索引文件不存在,请至少成功更新一次仓库!"); - } - - var json = await File.ReadAllTextAsync(localRepoJsonPath); - return json; + string localRepoJsonPath = GetRepoJsonPath(); + return await File.ReadAllTextAsync(localRepoJsonPath); } - catch (Exception e) + catch (Exception ex) { - await MessageBox.ShowAsync(e.Message, "获取仓库信息失败"); - return ""; + await MessageBox.ShowAsync(ex.Message, "获取仓库信息失败"); + return string.Empty; } } @@ -59,21 +59,15 @@ public class RepoWebBridge public async Task GetUserConfigJson() { - try + string userConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "User", "config.json"); + + if (!File.Exists(userConfigPath)) { - string userConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "User", "config.json"); - if (!File.Exists(userConfigPath)) - { - throw new Exception("用户配置文件不存在: " + userConfigPath); - } + await MessageBox.ShowAsync($"用户配置文件不存在: {userConfigPath}", "获取用户配置失败"); + return string.Empty; + } - return await File.ReadAllTextAsync(userConfigPath); - } - catch (Exception e) - { - await MessageBox.ShowAsync(e.Message, "获取用户配置失败"); - return ""; - } + return await File.ReadAllTextAsync(userConfigPath); } public async Task GetFile(string relPath) @@ -81,31 +75,131 @@ public class RepoWebBridge try { string filePath = Path.Combine(ScriptRepoUpdater.CenterRepoPath, "repo", relPath) - .Replace('/', Path.DirectorySeparatorChar); - + .Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + if (!File.Exists(filePath)) - return "404"; - - // 允许返回内容的文本文件扩展名 - string[] allowedTextExtensions = { - ".txt", ".md", ".json", ".js", ".ts", - ".vue", ".css", ".html", ".csv", ".xml", - ".yaml", ".yml", ".ini", ".config" - }; - - string ext = Path.GetExtension(filePath).ToLower(); - - if (allowedTextExtensions.Contains(ext)) { - return await File.ReadAllTextAsync(filePath); + return "404"; } - // 其他所有文件类型返回404 - return "404"; + string extension = Path.GetExtension(filePath); + return AllowedTextExtensions.Contains(extension) + ? await File.ReadAllTextAsync(filePath) + : "404"; } catch { return "404"; } } + + public async Task UpdateSubscribed(string path) + { + try + { + if (!Directory.Exists(ScriptRepoUpdater.CenterRepoPath)) + { + throw new InvalidOperationException("仓库文件夹不存在,请至少成功更新一次仓库!"); + } + + string localRepoJsonPath = GetRepoJsonPath(); + string json = await File.ReadAllTextAsync(localRepoJsonPath); + + var jsonObj = Newtonsoft.Json.JsonConvert.DeserializeObject(json); + if (jsonObj?["indexes"] is not JArray indexes) return false; + + string[] pathParts = path.Split('/'); + ProcessPathRecursively(indexes, pathParts, 0); + + string modifiedJson = Newtonsoft.Json.JsonConvert.SerializeObject(jsonObj, Newtonsoft.Json.Formatting.Indented); + await File.WriteAllTextAsync(localRepoJsonPath, modifiedJson); + + return true; + } + catch (Exception ex) + { + await MessageBox.ShowAsync(ex.Message, "信息更新失败"); + return false; + } + } + + public async Task ClearUpdate() + { + try + { + string? repoJsonPath = Directory + .GetFiles(ScriptRepoUpdater.CenterRepoPath, "repo.json", SearchOption.AllDirectories) + .FirstOrDefault(); + + if (string.IsNullOrEmpty(repoJsonPath)) + { + throw new FileNotFoundException("找不到原始 repo.json 文件"); + } + + string targetPath = Path.Combine(ScriptRepoUpdater.ReposPath, "repo_updated.json"); + + File.Copy(repoJsonPath, targetPath, overwrite: true); + + return true; + } + catch (Exception ex) + { + await MessageBox.ShowAsync($"清空更新标记失败: {ex.Message}", "操作失败"); + return false; + } + } + + private static string GetRepoJsonPath() + { + string updatedRepoJsonPath = Path.Combine( + Path.GetDirectoryName(Path.Combine(ScriptRepoUpdater.ReposPath, "bettergi-scripts-list-git"))!, + "repo_updated.json" + ); + + if (File.Exists(updatedRepoJsonPath)) + { + return updatedRepoJsonPath; + } + + string? repoJson = Directory + .GetFiles(ScriptRepoUpdater.CenterRepoPath, "repo.json", SearchOption.AllDirectories) + .FirstOrDefault(); + + return repoJson ?? throw new FileNotFoundException("repo.json 仓库索引文件不存在,请至少成功更新一次仓库!"); + } + + private static void ProcessPathRecursively(JArray array, string[] pathParts, int currentIndex) + { + foreach (JObject item in array.OfType()) + { + if (item["name"]?.ToString() != pathParts[currentIndex]) continue; + + if (currentIndex == pathParts.Length - 1) + { + ResetHasUpdateFlag(item); + } + else if (item["children"] is JArray children) + { + ProcessPathRecursively(children, pathParts, currentIndex + 1); + } + break; + } + } + + private static void ResetHasUpdateFlag(JObject node) + { + if (node["hasUpdate"] is { Type: JTokenType.Boolean } hasUpdate && + (bool)hasUpdate) + { + node["hasUpdate"] = false; + } + + if (node["children"] is JArray children) + { + foreach (JObject child in children.OfType()) + { + ResetHasUpdateFlag(child); + } + } + } } diff --git a/BetterGenshinImpact/GameTask/Common/Job/ExitAndReloginJob.cs b/BetterGenshinImpact/GameTask/Common/Job/ExitAndReloginJob.cs index 4f24d743..3ae3bb0b 100644 --- a/BetterGenshinImpact/GameTask/Common/Job/ExitAndReloginJob.cs +++ b/BetterGenshinImpact/GameTask/Common/Job/ExitAndReloginJob.cs @@ -83,7 +83,7 @@ public class ExitAndReloginJob _assets.EnterGameRo, () => GameCaptureRegion.GameRegion1080PPosClick(955, 666), ct, - 10, + 120, 1000 ); } @@ -112,4 +112,3 @@ public class ExitAndReloginJob await Delay(500, ct); } } - diff --git a/BetterGenshinImpact/View/Windows/PromptDialog.xaml.cs b/BetterGenshinImpact/View/Windows/PromptDialog.xaml.cs index 32c00b43..9be866d4 100644 --- a/BetterGenshinImpact/View/Windows/PromptDialog.xaml.cs +++ b/BetterGenshinImpact/View/Windows/PromptDialog.xaml.cs @@ -1,4 +1,3 @@ -using System; using System.Windows; using System.Windows.Controls; @@ -81,15 +80,14 @@ public partial class PromptDialog }; var inst = new PromptDialog(question, title, textBox, defaultValue, config); inst.ShowDialog(); - return inst.DialogResult == true ? inst.ResponseText : defaultValue; - + return inst.DialogResult == true ? inst.ResponseText : ""; } public static string Prompt(string question, string title, UIElement uiElement, string defaultValue = "", PromptDialogConfig? config = null) { var inst = new PromptDialog(question, title, uiElement, defaultValue, config); inst.ShowDialog(); - return inst.DialogResult == true ? inst.ResponseText : defaultValue; + return inst.DialogResult == true ? inst.ResponseText : ""; } public static string Prompt(string question, string title, UIElement uiElement, Size size, PromptDialogConfig? config = null) @@ -132,4 +130,4 @@ public partial class PromptDialog { Close(); } -} \ No newline at end of file +} diff --git a/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs index 3aada3d9..489b141e 100644 --- a/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -83,7 +83,14 @@ public partial class ScriptControlViewModel : ViewModel [RelayCommand] private void OnAddScriptGroup() { - var str = PromptDialog.Prompt("请输入配置组名称", "新增配置组"); + // 创建一个TextBox并设置自动聚焦 + var textBox = new System.Windows.Controls.TextBox(); + textBox.Loaded += (sender, e) => + { + textBox.Focus(); + textBox.SelectAll(); + }; + var str = PromptDialog.Prompt("请输入配置组名称", "新增配置组", textBox); if (!string.IsNullOrEmpty(str)) { // 检查是否已存在 @@ -596,7 +603,14 @@ public partial class ScriptControlViewModel : ViewModel return; } - var str = PromptDialog.Prompt("请输入配置组名称", "复制配置组", item.Name); + var textBox = new System.Windows.Controls.TextBox(); + textBox.Loaded += (sender, e) => + { + textBox.Focus(); + textBox.SelectAll(); + }; + + var str = PromptDialog.Prompt("请输入配置组名称", "复制配置组", textBox, item.Name); if (!string.IsNullOrEmpty(str)) { // 检查是否已存在 @@ -632,7 +646,14 @@ public partial class ScriptControlViewModel : ViewModel return; } - var str = PromptDialog.Prompt("请输入配置组名称", "重命名配置组", item.Name); + var textBox = new System.Windows.Controls.TextBox(); + textBox.Loaded += (sender, e) => + { + textBox.Focus(); + textBox.SelectAll(); + }; + + var str = PromptDialog.Prompt("请输入配置组名称", "重命名配置组", textBox, item.Name); if (!string.IsNullOrEmpty(str)) { if (item.Name == str)