mirror of
https://github.com/babalae/better-genshin-impact.git
synced 2026-03-20 08:29:50 +08:00
* feat: 实现启动时自动更新已订阅脚本及多仓库分离存储
- ScriptConfig: 新增 AutoUpdateSubscribedScripts 配置项
- ScriptRepoUpdater: 动态 CenterRepoPath, 按仓库URL分离存储
- 内容重合度检测(Jaccard系数)判断仓库异同
- URL→文件夹名持久化映射(repo_folder_mapping.json)
- repo_updated.json 存放于各自仓库文件夹内
- AutoUpdateSubscribedScripts 启动时自动更新订阅脚本
- 静默同步仓库、渠道URL解析、检出更新脚本
- RepoWebBridge: 使用动态路径, 辅助方法改为 internal
- MainWindowViewModel: 启动时调用自动更新
* feat: 基于内容重合度的导入zip仓库
* perf: 合并默认仓库url映射
* perf: 清理兼容字段
* perf: 添加线程锁以避免并发调用
* fix: 缓存FolderMapping、修复重合度异常返回值、目录扫描异常隔离、移除未使用变量
* perf: 优化更新流程
* perf: 内存缓存添加锁
* fix: 修复更新状态逻辑,确保克隆失败时不标记为已更新
* perf: 文件夹映射先写磁盘再写缓存
* refactor: 简化生成唯一文件夹名称的方法,移除不必要的参数
* fix: ResetRepo加写锁并清理URL映射条目
* perf: 优化重合度算法
* docs: 更新注释
* fix: 仅重置实际更新成功的脚本的 hasUpdate 标记
* feat: 手动一键更新按钮
* feat: 订阅路径迁移至独立文件存储并简化更新逻辑
- 订阅数据从 config.json 迁移到 User/subscriptions/{repo}.json 独立文件
- 添加 ReaderWriterLockSlim 保护订阅文件并发读写
- 使用 System.Text.Json + ConfigService.JsonOptions 序列化
- 新增 RepoWebBridge.GetSubscribedScriptPaths() 桥接方法
- 启动时自动从旧 config.json 迁移订阅数据到独立文件
- 合并手动/自动更新为 UpdateAllSubscribedScriptsCore 共用核心
- 移除 hasUpdate 检查,直接全量更新所有订阅脚本
- 移除冗余 logPrefix 参数
* refactor: 简化启动时自动更新调用
- 移除 Task.Run + try-catch 包装,异常处理已内置于方法中
- 直接使用 fire-and-forget 异步调用
* fix: 订阅目录命名改为 PascalCase (Subscriptions)
* refactor: 移除死代码和冗余中间层方法
* fix: ReadSubscriptionFile 异常时记录日志避免订阅数据静默丢失
* fix: 进度条改为Indeterminate模式、异常日志补全、订阅去重、迁移批量写入、锁注释
* refactor: 提取 ReadFolderMappingFromDisk 消除映射方法嵌套 try
* fix: 补全静态方法异常日志、WriteSubscriptionFile异常保护、ManualUpdate注释
* fix: ManualUpdateSubscribedScripts 加 try-catch 兜底并提示用户重置仓库
* fix: Dialog打开时检测后台自动更新状态,自动禁用按钮并显示进度提示
- ScriptRepoUpdater 新增 IsAutoUpdating 标志和 AutoUpdateStateChanged 事件
- ScriptRepoWindow 订阅事件,自动更新期间显示进度条、禁用所有操作按钮并 Toast 提示
- 更新仓库/重置仓库按钮也加上 IsEnabled 绑定 IsUpdating
* fix: 将自动更新调用包裹在 Task.Run 中避免 UI 线程阻塞
AutoUpdateSubscribedScripts 的 await 后续会被 WPF SynchronizationContext
调度回 UI 线程,导致大量 Git checkout 和文件 IO 操作阻塞界面。
用 Task.Run 确保整个流程在线程池执行。
* fix: 进度条分离 IsProgressIndeterminate 属性,按操作类型正确切换确定/不确定模式
* refactor: 消除 pathing 展开重复逻辑、用布尔字段替换字符串比较追踪状态来源、补全锁注释、统一日志方式
* fix: ExpandTopLevelPaths 泛化展开所有 PathMapper 顶层 key 防止误删用户目录,迁移移入锁内
* fix: 命令行启动配置组/一条龙前先等待自动更新订阅脚本完成
* feat: 添加命令行启动时是否先自动更新选项
* fix: 修复按钮位置
302 lines
9.4 KiB
C#
302 lines
9.4 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Runtime.InteropServices;
|
||
using System.Threading.Tasks;
|
||
using BetterGenshinImpact.View.Windows;
|
||
using BetterGenshinImpact.ViewModel.Message;
|
||
using CommunityToolkit.Mvvm.Messaging;
|
||
using Newtonsoft.Json.Linq;
|
||
using System.Net;
|
||
using BetterGenshinImpact.GameTask;
|
||
|
||
namespace BetterGenshinImpact.Core.Script.WebView;
|
||
|
||
/// <summary>
|
||
/// 给 WebView 提供的桥接类
|
||
/// 用于调用 C# 方法
|
||
/// </summary>
|
||
[ClassInterface(ClassInterfaceType.AutoDual)]
|
||
[ComVisible(true)]
|
||
public sealed class RepoWebBridge
|
||
{
|
||
private static readonly HashSet<string> AllowedTextExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
".txt", ".md", ".json", ".js", ".ts",
|
||
".vue", ".css", ".html", ".csv", ".xml",
|
||
".yaml", ".yml", ".ini", ".config"
|
||
};
|
||
|
||
private static readonly HashSet<string> AllowedImageExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico"
|
||
};
|
||
|
||
public async Task<string> GetRepoJson()
|
||
{
|
||
try
|
||
{
|
||
if (!Directory.Exists(ScriptRepoUpdater.CenterRepoPath))
|
||
{
|
||
throw new InvalidOperationException("仓库文件夹不存在,请至少成功更新一次仓库!");
|
||
}
|
||
|
||
string localRepoJsonPath = GetRepoJsonPath();
|
||
return await File.ReadAllTextAsync(localRepoJsonPath);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
await ThemedMessageBox.ErrorAsync(ex.Message, "获取仓库信息失败");
|
||
return string.Empty;
|
||
}
|
||
}
|
||
|
||
public async void ImportUri(string url)
|
||
{
|
||
try
|
||
{
|
||
await ScriptRepoUpdater.Instance.ImportScriptFromUri(url, false);
|
||
WeakReferenceMessenger.Default.Send(new RefreshDataMessage("Refresh"));
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
await ThemedMessageBox.ErrorAsync(e.Message, "订阅脚本链接失败!");
|
||
}
|
||
}
|
||
|
||
public async Task<string> GetUserConfigJson()
|
||
{
|
||
string userConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "User", "config.json");
|
||
|
||
if (!File.Exists(userConfigPath))
|
||
{
|
||
await ThemedMessageBox.ErrorAsync($"用户配置文件不存在: {userConfigPath}", "获取用户配置失败");
|
||
return string.Empty;
|
||
}
|
||
|
||
return await File.ReadAllTextAsync(userConfigPath);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前仓库的已订阅脚本路径列表(JSON 数组)。
|
||
/// 相比 GetUserConfigJson() 更轻量,仅返回当前仓库的订阅路径。
|
||
/// </summary>
|
||
public string GetSubscribedScriptPaths()
|
||
{
|
||
try
|
||
{
|
||
var paths = ScriptRepoUpdater.GetSubscribedPathsForCurrentRepo();
|
||
if (paths.Count > 0)
|
||
{
|
||
return Newtonsoft.Json.JsonConvert.SerializeObject(paths);
|
||
}
|
||
|
||
return "[]";
|
||
}
|
||
catch
|
||
{
|
||
return "[]";
|
||
}
|
||
}
|
||
|
||
public Task<string> GetFile(string relPath)
|
||
{
|
||
try
|
||
{
|
||
// URL 解码路径(处理中文文件名)
|
||
relPath = WebUtility.UrlDecode(relPath);
|
||
|
||
string filePath = Path.Combine(ScriptRepoUpdater.CenterRepoPath, "repo", relPath)
|
||
.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||
|
||
// 验证解析后的路径在允许的目录范围内
|
||
string normalizedBasePath = Path.GetFullPath(Path.Combine(ScriptRepoUpdater.CenterRepoPath, "repo"));
|
||
string normalizedFilePath = Path.GetFullPath(filePath);
|
||
if (!normalizedFilePath.StartsWith(normalizedBasePath, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return Task.FromResult("404");
|
||
}
|
||
|
||
string extension = Path.GetExtension(filePath).ToLower();
|
||
|
||
if (AllowedTextExtensions.Contains(extension))
|
||
{
|
||
// 读取文本文件
|
||
string? content = ScriptRepoUpdater.Instance.ReadFileFromCenterRepo(relPath);
|
||
return Task.FromResult(string.IsNullOrEmpty(content) ? "404" : content);
|
||
}
|
||
else if (AllowedImageExtensions.Contains(extension))
|
||
{
|
||
// 读取图片文件,返回 Base64 编码
|
||
byte[]? bytes = ScriptRepoUpdater.Instance.ReadBinaryFileFromCenterRepo(relPath);
|
||
if (bytes == null || bytes.Length == 0)
|
||
{
|
||
return Task.FromResult("404");
|
||
}
|
||
|
||
string base64 = Convert.ToBase64String(bytes);
|
||
return Task.FromResult(base64);
|
||
}
|
||
|
||
return Task.FromResult("404");
|
||
}
|
||
catch
|
||
{
|
||
return Task.FromResult("404");
|
||
}
|
||
}
|
||
|
||
private static string GetMimeType(string extension)
|
||
{
|
||
return extension.ToLower() switch
|
||
{
|
||
".png" => "image/png",
|
||
".jpg" or ".jpeg" => "image/jpeg",
|
||
".gif" => "image/gif",
|
||
".bmp" => "image/bmp",
|
||
".webp" => "image/webp",
|
||
".svg" => "image/svg+xml",
|
||
".ico" => "image/x-icon",
|
||
_ => "application/octet-stream"
|
||
};
|
||
}
|
||
|
||
public async Task<bool> 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<JObject>(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 ThemedMessageBox.ErrorAsync(ex.Message, "信息更新失败");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public async Task<bool> ClearUpdate()
|
||
{
|
||
try
|
||
{
|
||
string? repoJsonPath = Directory
|
||
.GetFiles(ScriptRepoUpdater.CenterRepoPath, "repo.json", SearchOption.AllDirectories)
|
||
.FirstOrDefault();
|
||
|
||
if (string.IsNullOrEmpty(repoJsonPath))
|
||
{
|
||
throw new FileNotFoundException("找不到原始 repo.json 文件");
|
||
}
|
||
|
||
string targetPath = ScriptRepoUpdater.RepoUpdatedJsonPath;
|
||
|
||
File.Copy(repoJsonPath, targetPath, overwrite: true);
|
||
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
await ThemedMessageBox.ErrorAsync($"清空更新标记失败: {ex.Message}", "操作失败");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 设置新手引导标志位
|
||
public bool SetGuideStatus(bool status)
|
||
{
|
||
try
|
||
{
|
||
var scriptConfig = TaskContext.Instance().Config.ScriptConfig;
|
||
scriptConfig.GuideStatus = status;
|
||
return true;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Console.WriteLine(e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 获取新手引导标志位
|
||
public bool GetGuideStatus()
|
||
{
|
||
try
|
||
{
|
||
return TaskContext.Instance().Config.ScriptConfig.GuideStatus;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Console.WriteLine(e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static string GetRepoJsonPath()
|
||
{
|
||
string updatedRepoJsonPath = ScriptRepoUpdater.RepoUpdatedJsonPath;
|
||
|
||
if (File.Exists(updatedRepoJsonPath))
|
||
{
|
||
return updatedRepoJsonPath;
|
||
}
|
||
|
||
string? repoJson = Directory
|
||
.GetFiles(ScriptRepoUpdater.CenterRepoPath, "repo.json", SearchOption.AllDirectories)
|
||
.FirstOrDefault();
|
||
|
||
return repoJson ?? throw new FileNotFoundException("repo.json 仓库索引文件不存在,请至少成功更新一次仓库!");
|
||
}
|
||
|
||
internal static void ProcessPathRecursively(JArray array, string[] pathParts, int currentIndex)
|
||
{
|
||
foreach (JObject item in array.OfType<JObject>())
|
||
{
|
||
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;
|
||
}
|
||
}
|
||
|
||
internal 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<JObject>())
|
||
{
|
||
ResetHasUpdateFlag(child);
|
||
}
|
||
}
|
||
}
|
||
}
|