Files

3342 lines
124 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.Core.Script.Project;
using BetterGenshinImpact.Core.Script.WebView;
using BetterGenshinImpact.GameTask;
using BetterGenshinImpact.Helpers;
using BetterGenshinImpact.Helpers.Http;
using BetterGenshinImpact.Helpers.Ui;
using BetterGenshinImpact.Helpers.Win32;
using BetterGenshinImpact.Model;
using BetterGenshinImpact.Service;
using BetterGenshinImpact.View.Controls.Webview;
using BetterGenshinImpact.ViewModel.Pages;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using Microsoft.Web.WebView2.Core;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using BetterGenshinImpact.View.Windows;
using LibGit2Sharp;
using LibGit2Sharp.Handlers;
using Vanara.PInvoke;
using Wpf.Ui.Violeta.Controls;
namespace BetterGenshinImpact.Core.Script;
public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
{
private readonly ILogger<ScriptRepoUpdater> _logger = App.GetLogger<ScriptRepoUpdater>();
private static readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(30) };
/// <summary>
/// 全局互斥锁,串行化所有对仓库目录和用户脚本目录的写操作,
/// 防止自动更新、手动更新、ZIP 导入等并发冲突。
/// </summary>
private readonly SemaphoreSlim _repoWriteLock = new(1, 1);
/// <summary>
/// 指示后台自动更新是否正在进行。
/// Dialog 可据此禁用手动操作按钮并显示进度提示。
/// </summary>
private volatile bool _isAutoUpdating;
public bool IsAutoUpdating => _isAutoUpdating;
/// <summary>
/// 后台自动更新状态变化事件(开始/结束),
/// 注意:可能在非 UI 线程触发,订阅方需自行 Dispatch。
/// </summary>
public event EventHandler? AutoUpdateStateChanged;
/// <summary>
/// 命令行启动时并行执行的自动更新 Task。
/// StartGameTask 结束后会 await 此 Task确保更新完成后再执行任务。
/// </summary>
public Task? CommandLineAutoUpdateTask { get; set; }
// 仓储位置
public static readonly string ReposPath = Global.Absolute("Repos");
// 仓储临时目录 用于下载与解压
public static readonly string ReposTempPath = Path.Combine(ReposPath, "Temp");
// // 中央仓库信息地址
// public static readonly List<string> CenterRepoInfoUrls =
// [
// "https://raw.githubusercontent.com/babalae/bettergi-scripts-list/refs/heads/main/repo.json",
// "https://r2-script.bettergi.com/github_mirror/repo.json",
// ];
// 中央仓库默认文件夹名
public static readonly string CenterRepoFolderName = "bettergi-scripts-list";
/// <summary>
/// 当前活跃的中央仓库路径(根据用户配置的渠道动态解析)
/// </summary>
public static string CenterRepoPath
{
get
{
try
{
var config = TaskContext.Instance().Config.ScriptConfig;
var url = ResolveRepoUrl(config);
var folderName = GetRepoFolderName(url);
return Path.Combine(ReposPath, folderName);
}
catch (Exception ex)
{
Debug.WriteLine($"[ScriptRepoUpdater] CenterRepoPath 解析失败,回退到默认路径: {ex.Message}");
return Path.Combine(ReposPath, CenterRepoFolderName);
}
}
}
public static readonly string CenterRepoPathOld = Path.Combine(ReposPath, "bettergi-scripts-list-main");
public static readonly Dictionary<string, string> PathMapper = new Dictionary<string, string>
{
{ "pathing", Global.Absolute("User\\AutoPathing") },
{ "js", Global.Absolute("User\\JsScript") },
{ "combat", Global.Absolute("User\\AutoFight") },
{ "tcg", Global.Absolute("User\\AutoGeniusInvokation") },
};
private WebpageWindow? _webWindow;
// [Obsolete]
// public void AutoUpdate()
// {
// var scriptConfig = TaskContext.Instance().Config.ScriptConfig;
//
// if (!Directory.Exists(ReposPath))
// {
// Directory.CreateDirectory(ReposPath);
// }
//
// // 判断更新周期是否到达
// if (DateTime.Now - scriptConfig.LastUpdateScriptRepoTime >=
// TimeSpan.FromDays(scriptConfig.AutoUpdateScriptRepoPeriod))
// {
// // 更新仓库
// Task.Run(async () =>
// {
// try
// {
// var (repoPath, updated) = await UpdateCenterRepo();
// Debug.WriteLine($"脚本仓库更新完成,路径:{repoPath}");
// scriptConfig.LastUpdateScriptRepoTime = DateTime.Now;
// if (updated)
// {
// scriptConfig.ScriptRepoHintDotVisible = true;
// }
// }
// catch (Exception e)
// {
// _logger.LogDebug(e, $"脚本仓库更新失败:{e.Message}");
// }
// });
// }
// }
/// <summary>
/// 自动更新已订阅的脚本
/// 在启动时先拉取最新仓库,然后检查已订阅的脚本是否有更新,
/// 如果有则自动从仓库中检出最新版本到用户目录。
/// 类似于 Web 端的"一键更新"功能
/// </summary>
public async Task AutoUpdateSubscribedScripts()
{
// 迁移旧 config.json 中的订阅路径到独立文件
MigrateSubscribedPathsFromConfig();
try
{
var scriptConfig = TaskContext.Instance().Config.ScriptConfig;
// 检查是否启用自动更新
if (!scriptConfig.AutoUpdateSubscribedScripts)
{
_logger.LogDebug("已禁用自动更新订阅脚本");
return;
}
_isAutoUpdating = true;
AutoUpdateStateChanged?.Invoke(this, EventArgs.Empty);
await _repoWriteLock.WaitAsync();
try
{
var subscribedPaths = GetSubscribedPathsForCurrentRepo();
if (subscribedPaths.Count == 0)
{
_logger.LogDebug("没有已订阅的脚本,跳过自动更新");
return;
}
var (successCount, failCount) = await UpdateAllSubscribedScriptsCore(scriptConfig);
if (successCount > 0)
{
_logger.LogInformation("自动更新订阅脚本完成: 成功 {Success} 个, 失败 {Fail} 个", successCount, failCount);
UIDispatcherHelper.Invoke(() => Toast.Success($"已自动更新 {successCount} 个订阅脚本"));
}
}
finally
{
_repoWriteLock.Release();
}
}
catch (Exception e)
{
_logger.LogDebug(e, "自动更新订阅脚本失败");
}
finally
{
_isAutoUpdating = false;
AutoUpdateStateChanged?.Invoke(this, EventArgs.Empty);
}
}
/// <summary>
/// 手动一键更新已订阅的脚本(不检查 AutoUpdateSubscribedScripts 配置开关)。
/// 更新所有订阅脚本。
/// </summary>
public async Task ManualUpdateSubscribedScripts()
{
try
{
var scriptConfig = TaskContext.Instance().Config.ScriptConfig;
var subscribedPaths = GetSubscribedPathsForCurrentRepo();
if (subscribedPaths.Count == 0)
{
_logger.LogInformation("没有已订阅的脚本");
UIDispatcherHelper.Invoke(() => Toast.Information("没有已订阅的脚本,请先在仓库中订阅脚本"));
return;
}
await _repoWriteLock.WaitAsync();
try
{
var (successCount, failCount) = await UpdateAllSubscribedScriptsCore(scriptConfig);
_logger.LogInformation("一键更新订阅脚本完成: 成功 {Success} 个, 失败 {Fail} 个", successCount, failCount);
UIDispatcherHelper.Invoke(() =>
{
if (failCount == 0)
Toast.Success($"已更新 {successCount} 个订阅脚本");
else
Toast.Warning($"已更新 {successCount} 个订阅脚本,{failCount} 个失败");
});
}
finally
{
_repoWriteLock.Release();
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "一键更新订阅脚本失败");
UIDispatcherHelper.Invoke(() => Toast.Error($"更新订阅脚本失败,建议重置仓库后重试\n原因{ex.Message}"));
}
}
/// <summary>
/// 自动/手动更新所有订阅脚本的通用核心逻辑。
/// 更新全部订阅脚本,不检查 hasUpdate 标记。
/// </summary>
private async Task<(int successCount, int failCount)> UpdateAllSubscribedScriptsCore(ScriptConfig scriptConfig)
{
// 第一步:拉取最新仓库
await UpdateCenterRepoSilently(scriptConfig);
// 检查仓库是否存在
if (!Directory.Exists(CenterRepoPath))
{
_logger.LogWarning("仓库文件夹不存在,请先更新仓库");
UIDispatcherHelper.Invoke(() => Toast.Warning("仓库文件夹不存在,请先更新仓库"));
return (0, 0);
}
// 重新加载订阅路径
var subscribedPaths = GetSubscribedPathsForCurrentRepo();
if (subscribedPaths.Count == 0)
{
return (0, 0);
}
// 查找仓库路径
string repoPath;
try
{
repoPath = FindCenterRepoPath();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "查找中央仓库路径失败");
UIDispatcherHelper.Invoke(() => Toast.Warning("查找仓库路径失败,请先更新仓库"));
return (0, 0);
}
// 展开所有订阅路径
var expandedPaths = ExpandTopLevelPaths(subscribedPaths, repoPath);
// 过滤掉仓库中已不存在的路径(幽灵订阅),避免删除用户文件后检出空内容
var validPaths = FilterExistingPaths(expandedPaths, repoPath);
// 清理订阅文件中的幽灵项:直接对原始订阅路径做过滤
if (validPaths.Count < expandedPaths.Count)
{
var cleaned = FilterExistingPaths(subscribedPaths, repoPath);
if (cleaned.Count < subscribedPaths.Count)
{
SetSubscribedPathsForCurrentRepo(cleaned);
}
}
int successCount = 0;
int failCount = 0;
foreach (var path in validPaths)
{
try
{
var (first, remainingPath) = GetFirstFolderAndRemainingPath(path);
if (!PathMapper.TryGetValue(first, out var userPath))
{
_logger.LogDebug("未知的脚本路径类型: {Path}", path);
continue;
}
var destPath = Path.Combine(userPath, remainingPath);
// 备份需要保存的文件(仅 JS 脚本)
List<string> backupFiles = new();
if (first == "js")
{
backupFiles = BackupScriptFiles(path, repoPath);
}
// 删除旧文件/目录
if (Directory.Exists(destPath))
{
DirectoryHelper.DeleteDirectoryWithReadOnlyCheck(destPath);
}
else if (File.Exists(destPath))
{
File.Delete(destPath);
}
// 从仓库检出最新文件
CheckoutPath(repoPath, path, destPath);
// 图标处理(仅对目录)
if (Directory.Exists(destPath))
{
DealWithIconFolder(destPath);
}
// 恢复备份的文件
if (first == "js" && backupFiles.Count > 0)
{
RestoreScriptFiles(path, repoPath);
}
// 解析 JS 脚本依赖
if (first == "js")
{
ResolveScriptDependencies(repoPath, destPath);
}
successCount++;
_logger.LogInformation("更新脚本成功: {Path}", path);
}
catch (Exception ex)
{
failCount++;
_logger.LogWarning(ex, "更新脚本失败: {Path}", path);
}
}
if (successCount > 0)
{
UpdateSubscribedScriptPaths();
}
return (successCount, failCount);
}
/// <summary>
/// 展开裸顶层路径为其子目录(如 "pathing" -> "pathing/xxx", "pathing/yyy""js" -> "js/aaa", "js/bbb")。
/// 这样可以避免后续检出时 destPath 等于整个用户目录而误删所有用户脚本。
/// 非 PathMapper 顶层 key 或已包含子路径的条目原样保留。
/// </summary>
private List<string> ExpandTopLevelPaths(List<string> paths, string repoPath)
{
var topLevelKeys = PathMapper.Keys.ToHashSet(StringComparer.OrdinalIgnoreCase);
var result = new List<string>();
foreach (var path in paths)
{
// 仅当路径恰好是一个裸顶层 key如 "pathing"、"js"、"combat")时才展开
if (!topLevelKeys.Contains(path))
{
result.Add(path);
continue;
}
bool isGitRepo = IsGitRepository(repoPath);
if (isGitRepo)
{
using var repo = new Repository(repoPath);
var commit = repo.Head.Tip;
if (commit != null)
{
var repoTree = GetRepoSubdirectoryTree(repo);
var entry = repoTree[path];
if (entry?.TargetType == TreeEntryTargetType.Tree)
{
var subTree = (Tree)entry.Target;
foreach (var child in subTree)
{
if (child.TargetType == TreeEntryTargetType.Tree)
{
result.Add(path + "/" + child.Name);
}
}
}
}
}
else
{
var dir = Path.Combine(repoPath, path);
if (Directory.Exists(dir))
{
foreach (var subDir in Directory.GetDirectories(dir, "*", SearchOption.TopDirectoryOnly))
{
result.Add(path + "/" + Path.GetFileName(subDir));
}
}
}
}
return result;
}
/// <summary>
/// 过滤掉仓库中已不存在的路径,防止幽灵订阅导致误删用户文件。
/// </summary>
private List<string> FilterExistingPaths(List<string> paths, string repoPath)
{
bool isGitRepo = IsGitRepository(repoPath);
if (isGitRepo)
{
using var repo = new Repository(repoPath);
if (repo.Head.Tip == null) return paths;
var repoTree = GetRepoSubdirectoryTree(repo);
return paths.Where(path =>
{
var parts = path.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
var currentTree = repoTree;
foreach (var part in parts)
{
var entry = currentTree[part];
if (entry == null) return false;
if (entry.TargetType == TreeEntryTargetType.Tree)
currentTree = (Tree)entry.Target;
}
return true;
}).ToList();
}
else
{
return paths.Where(path =>
{
var fullPath = Path.Combine(repoPath, path);
return Directory.Exists(fullPath) || File.Exists(fullPath);
}).ToList();
}
}
/// <summary>
/// 静默更新中央仓库(用于自动更新订阅脚本前同步最新仓库内容)。
/// 注意:此方法设计为在 _repoWriteLock 持有期间调用,
/// 内部直接调用 UpdateCenterRepoByGitCore 而非带锁包装的 UpdateCenterRepoByGit以避免死锁。
/// </summary>
private async Task UpdateCenterRepoSilently(ScriptConfig scriptConfig)
{
try
{
// 获取仓库URL
var repoUrl = ResolveRepoUrl(scriptConfig);
if (string.IsNullOrEmpty(repoUrl))
{
_logger.LogDebug("无法确定仓库URL跳过仓库更新");
return;
}
_logger.LogInformation("自动更新订阅脚本:开始静默更新脚本仓库...");
var (_, updated) = await UpdateCenterRepoByGitCore(repoUrl, null);
if (updated)
{
_logger.LogInformation("自动更新订阅脚本:仓库有新内容");
scriptConfig.ScriptRepoHintDotVisible = true;
}
else
{
_logger.LogDebug("自动更新订阅脚本:仓库已是最新");
}
scriptConfig.LastUpdateScriptRepoTime = DateTime.Now;
}
catch (Exception ex)
{
_logger.LogDebug(ex, "静默更新仓库失败,将基于本地仓库继续检查订阅更新");
}
}
/// <summary>
/// 渠道名称到URL的映射
/// </summary>
public static readonly Dictionary<string, string> RepoChannels = new()
{
{ "CNB", "https://cnb.cool/bettergi/bettergi-scripts-list" },
{ "GitCode", "https://gitcode.com/huiyadanli/bettergi-scripts-list" },
{ "GitHub", "https://github.com/babalae/bettergi-scripts-list" },
};
/// <summary>
/// 根据用户配置中的渠道名称解析仓库URL
/// </summary>
private static string? ResolveRepoUrl(ScriptConfig scriptConfig)
{
var channelName = scriptConfig.SelectedChannelName;
if (string.IsNullOrEmpty(channelName))
{
// 默认使用 CNB
return RepoChannels["CNB"];
}
if (channelName == "自定义")
{
var customUrl = scriptConfig.CustomRepoUrl;
if (!string.IsNullOrWhiteSpace(customUrl) && customUrl != "https://example.com/custom-repo")
{
return customUrl;
}
return null;
}
return RepoChannels.TryGetValue(channelName, out var url) ? url : RepoChannels["CNB"];
}
/// <summary>
/// 从仓库 URL 中提取文件夹名称(用于按仓库分开存储)
/// 优先查找持久化的 URL→文件夹名 映射,若无映射则根据 URL 结构推导
/// </summary>
internal static string GetRepoFolderName(string? repoUrl)
{
if (string.IsNullOrEmpty(repoUrl))
return CenterRepoFolderName;
var trimmedUrl = repoUrl.TrimEnd('/');
// 优先查找已保存的映射(基于目录结构重合度确定的文件夹名)
var mapping = LoadFolderMapping();
if (mapping.TryGetValue(trimmedUrl, out var savedName) && !string.IsNullOrEmpty(savedName))
return savedName;
// 无映射,根据 URL 推导默认名称
return DeriveBaseFolderName(trimmedUrl);
}
/// <summary>
/// 根据 URL 推导基础文件夹名称(仅使用仓库名,不包含拥有者)
/// </summary>
private static string DeriveBaseFolderName(string trimmedUrl)
{
try
{
var uri = new Uri(trimmedUrl);
var segments = uri.Segments
.Select(s => s.TrimEnd('/'))
.Where(s => !string.IsNullOrEmpty(s))
.ToArray();
if (segments.Length == 0)
return CenterRepoFolderName;
var repoName = segments.Last();
// 去掉 .git 后缀
if (repoName.EndsWith(".git", StringComparison.OrdinalIgnoreCase))
repoName = repoName[..^4];
return SanitizeFolderName(repoName);
}
catch
{
return CenterRepoFolderName;
}
}
/// <summary>
/// 净化文件夹名称,移除不合法字符
/// </summary>
private static string SanitizeFolderName(string name)
{
var invalidChars = Path.GetInvalidFileNameChars();
var sanitized = new string(name.Select(c => invalidChars.Contains(c) ? '_' : c).ToArray());
return string.IsNullOrEmpty(sanitized) ? CenterRepoFolderName : sanitized;
}
/// <summary>
/// 获取当前活跃仓库对应的 repo_updated.json 路径(位于仓库文件夹内部)
/// </summary>
public static string RepoUpdatedJsonPath => Path.Combine(CenterRepoPath, "repo_updated.json");
/// <summary>
/// 根据仓库文件夹名获取对应的 repo_updated.json 路径(位于仓库文件夹内部)
/// </summary>
internal static string GetRepoUpdatedJsonPathForFolder(string repoFolderName)
{
return Path.Combine(ReposPath, repoFolderName, "repo_updated.json");
}
// URL → 文件夹名 映射文件路径
private static readonly string FolderMappingPath = Path.Combine(ReposPath, "repo_folder_mapping.json");
/// <summary>
/// 缓存的 URL→文件夹名 映射,避免每次访问 CenterRepoPath 都读磁盘
/// </summary>
private static Dictionary<string, string>? _folderMappingCache;
private static readonly object _cacheLock = new();
/// <summary>
/// 从磁盘读取映射文件(不加锁,调用方负责加锁)
/// </summary>
private static Dictionary<string, string>? ReadFolderMappingFromDisk()
{
try
{
if (File.Exists(FolderMappingPath))
{
var json = File.ReadAllText(FolderMappingPath);
return Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, string>>(json);
}
}
catch (Exception ex)
{
Debug.WriteLine($"[ScriptRepoUpdater] 读取映射文件失败: {ex.Message}");
}
return null;
}
/// <summary>
/// 加载 URL→文件夹名 映射(带内存缓存,线程安全)
/// </summary>
private static Dictionary<string, string> LoadFolderMapping()
{
lock (_cacheLock)
{
if (_folderMappingCache != null)
return new Dictionary<string, string>(_folderMappingCache);
_folderMappingCache = ReadFolderMappingFromDisk() ?? new();
return new Dictionary<string, string>(_folderMappingCache);
}
}
/// <summary>
/// 保存 URL→文件夹名 映射(同时刷新内存缓存,线程安全)
/// </summary>
private static void SaveFolderMapping(string url, string folderName)
{
try
{
if (!Directory.Exists(ReposPath)) Directory.CreateDirectory(ReposPath);
lock (_cacheLock)
{
var mapping = ReadFolderMappingFromDisk()
?? (_folderMappingCache != null ? new Dictionary<string, string>(_folderMappingCache) : new());
mapping[url.TrimEnd('/')] = folderName;
// 先写磁盘,成功后再更新缓存,确保一致性
var jsonOut = Newtonsoft.Json.JsonConvert.SerializeObject(mapping, Newtonsoft.Json.Formatting.Indented);
File.WriteAllText(FolderMappingPath, jsonOut);
_folderMappingCache = mapping;
}
}
catch (Exception ex)
{
Debug.WriteLine($"保存仓库文件夹映射失败: {ex.Message}");
}
}
/// <summary>
/// 生成不重复的文件夹名(使用数字后缀 _1, _2, ...
/// </summary>
private static string GenerateUniqueFolderName(string baseName)
{
for (int i = 1; i < 100; i++)
{
var candidate = $"{baseName}_{i}";
var candidatePath = Path.Combine(ReposPath, candidate);
if (!Directory.Exists(candidatePath))
return candidate;
}
// 极端情况100个同名文件夹都被占用使用更大的数字
return $"{baseName}_{DateTime.Now.Ticks}";
}
/// <summary>
/// 从映射中移除指定 URL 的条目(线程安全)
/// </summary>
private static void RemoveFolderMapping(string url)
{
try
{
lock (_cacheLock)
{
var mapping = ReadFolderMappingFromDisk();
if (mapping == null) return;
var trimmed = url.TrimEnd('/');
if (!mapping.Remove(trimmed)) return;
var jsonOut = Newtonsoft.Json.JsonConvert.SerializeObject(mapping, Newtonsoft.Json.Formatting.Indented);
File.WriteAllText(FolderMappingPath, jsonOut);
_folderMappingCache = mapping;
}
}
catch (Exception ex)
{
Debug.WriteLine($"移除仓库映射失败: {ex.Message}");
}
}
/// <summary>
/// 重置当前活跃仓库(带写锁,同时清理映射条目)
/// </summary>
public async Task ResetCurrentRepoAsync()
{
await _repoWriteLock.WaitAsync();
try
{
var config = TaskContext.Instance().Config.ScriptConfig;
var repoUrl = ResolveRepoUrl(config);
var repoPath = CenterRepoPath;
if (Directory.Exists(repoPath))
{
DirectoryHelper.DeleteReadOnlyDirectory(repoPath);
}
// 清理 URL → 文件夹名 映射
if (!string.IsNullOrEmpty(repoUrl))
{
RemoveFolderMapping(repoUrl);
}
}
finally
{
_repoWriteLock.Release();
}
}
public async Task<(string, bool)> UpdateCenterRepoByGit(string repoUrl, CheckoutProgressHandler? onCheckoutProgress)
{
await _repoWriteLock.WaitAsync();
try
{
return await UpdateCenterRepoByGitCore(repoUrl, onCheckoutProgress);
}
finally
{
_repoWriteLock.Release();
}
}
private async Task<(string, bool)> UpdateCenterRepoByGitCore(string repoUrl, CheckoutProgressHandler? onCheckoutProgress)
{
if (string.IsNullOrEmpty(repoUrl))
{
throw new ArgumentException("仓库URL不能为空", nameof(repoUrl));
}
var repoPath = Path.Combine(ReposPath, GetRepoFolderName(repoUrl));
var updated = false;
// 备份相关变量
string? oldRepoJsonContent = null;
await Task.Run(() =>
{
Repository? repo = null;
try
{
GlobalSettings.SetOwnerValidation(false);
if (!Directory.Exists(repoPath))
{
// 如果仓库不存在,执行浅克隆操作
_logger.LogInformation($"浅克隆仓库: {repoUrl} 到 {repoPath}");
CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress);
SaveFolderMapping(repoUrl.TrimEnd('/'), Path.GetFileName(repoPath));
updated = true;
}
else
{
try
{
// 检测repo.json是否存在存在则备份
var oldRepoJsonPath = Path.Combine(repoPath, "repo.json");
if (File.Exists(oldRepoJsonPath))
{
oldRepoJsonContent = File.ReadAllText(oldRepoJsonPath);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "备份repo.json失败继续更新仓库");
}
// 检查是否为有效的Git仓库
if (!Repository.IsValid(repoPath))
{
Toast.Error($"不是有效的Git仓库将重新克隆");
UIDispatcherHelper.Invoke(() => Toast.Error("不是有效的Git仓库将重新克隆"));
CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress);
updated = true;
return;
}
repo = new Repository(repoPath);
// 检查远程URL是否需要更新
var origin = repo.Network.Remotes["origin"];
if (origin.Url != repoUrl)
{
// 远程URL已更改克隆到临时文件夹后基于目录结构重合度决定存放位置
_logger.LogInformation($"远程URL已更改: 从 {origin.Url} 到 {repoUrl}");
repo?.Dispose();
repo = null;
var tempPath = repoPath + "_temp_" + Guid.NewGuid().ToString("N")[..8];
// Step 1: 克隆到临时文件夹
bool cloneSucceeded = false;
try
{
CloneRepository(repoUrl, tempPath, "release", onCheckoutProgress);
cloneSucceeded = true;
}
catch (Exception cloneEx)
{
_logger.LogError(cloneEx, "克隆到临时文件夹失败,保留原仓库");
if (Directory.Exists(tempPath))
DirectoryHelper.DeleteReadOnlyDirectory(tempPath);
}
// Step 2: 基于目录结构重合度决定存放位置
if (cloneSucceeded)
{
try
{
var newTempRepoJson = Directory.GetFiles(tempPath, "repo.json", SearchOption.AllDirectories).FirstOrDefault();
var newContent = newTempRepoJson != null ? File.ReadAllText(newTempRepoJson) : null;
double overlapRatio = 0;
if (!string.IsNullOrEmpty(oldRepoJsonContent) && !string.IsNullOrEmpty(newContent))
{
overlapRatio = CalculateRepoOverlapRatio(oldRepoJsonContent, newContent);
}
if (overlapRatio >= 0.5)
{
// 目录结构重合度高 → 同一仓库的不同镜像,替换原文件夹
_logger.LogInformation("目录结构重合度 {Ratio:P0},判定为同一仓库镜像,替换原文件夹", overlapRatio);
DirectoryHelper.DeleteReadOnlyDirectory(repoPath);
Directory.Move(tempPath, repoPath);
SaveFolderMapping(repoUrl.TrimEnd('/'), Path.GetFileName(repoPath));
}
else
{
// 目录结构重合度低 → 不同仓库,创建新文件夹
var baseName = DeriveBaseFolderName(repoUrl.TrimEnd('/'));
var uniqueName = GenerateUniqueFolderName(baseName);
var newRepoPath = Path.Combine(ReposPath, uniqueName);
_logger.LogInformation("目录结构重合度 {Ratio:P0},判定为不同仓库,创建新文件夹: {Folder}", overlapRatio, uniqueName);
Directory.Move(tempPath, newRepoPath);
repoPath = newRepoPath;
SaveFolderMapping(repoUrl.TrimEnd('/'), uniqueName);
}
}
catch (Exception moveEx)
{
_logger.LogError(moveEx, "处理临时文件夹失败,清理临时目录,保留原仓库");
if (Directory.Exists(tempPath))
DirectoryHelper.DeleteReadOnlyDirectory(tempPath);
cloneSucceeded = false; // move 失败,视为未更新
}
}
updated = cloneSucceeded;
return;
}
// 直接获取远程分支的 Commit SHA
var remoteReferences = repo.Network.ListReferences(repoUrl, CreateCredentialsHandler());
var remoteBranch = remoteReferences.FirstOrDefault(r => r.CanonicalName == "refs/heads/release");
if (remoteBranch == null)
{
throw new Exception("未找到远程release分支");
}
var remoteCommitSha = remoteBranch.TargetIdentifier;
var currentCommitSha = repo.Branches["release"]?.Tip?.Sha;
// 比较本地和远程commit
if (currentCommitSha == remoteCommitSha)
{
_logger.LogInformation("本地仓库已是最新版本,无需更新");
updated = false;
}
else
{
_logger.LogInformation($"检测到远程更新: 本地 {currentCommitSha?[..7] ?? ""} -> 远程 {remoteCommitSha[..7]}");
repo?.Dispose();
repo = null;
CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress);
updated = true;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Git仓库更新失败");
UIDispatcherHelper.Invoke(() => Toast.Error("脚本仓库更新异常,直接删除后重新克隆\n原因" + ex.Message));
repo?.Dispose();
repo = null;
CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress);
updated = true;
}
finally
{
repo?.Dispose();
}
});
// 标记新repo.json中的更新节点
try
{
// 查找repo.json文件
var newRepoJsonPath = Directory.GetFiles(repoPath, "repo.json", SearchOption.AllDirectories).FirstOrDefault();
if (!string.IsNullOrEmpty(newRepoJsonPath))
{
var newRepoJsonContent = await File.ReadAllTextAsync(newRepoJsonPath);
// 检查是否存在repo_updated.json如果存在则直接与它比对
var repoFolderName = GetRepoFolderName(repoUrl);
var repoUpdateJsonPath = GetRepoUpdatedJsonPathForFolder(repoFolderName);
string updatedContent;
if (File.Exists(repoUpdateJsonPath))
{
var repoUpdateContent = await File.ReadAllTextAsync(repoUpdateJsonPath);
// 检查目录结构重合度,低于阈值则判定为不同仓库,不继承旧的更新标记
var overlapRatio = CalculateRepoOverlapRatio(repoUpdateContent, newRepoJsonContent);
if (overlapRatio < 0.5)
{
_logger.LogInformation("仓库目录结构重合度低 ({Ratio:P0}),判定为不同仓库,不继承旧的更新标记", overlapRatio);
updatedContent = newRepoJsonContent;
}
else
{
updatedContent = AddUpdateMarkersToNewRepo(repoUpdateContent, newRepoJsonContent);
}
}
else if (!string.IsNullOrEmpty(oldRepoJsonContent))
{
// 如果没有repo_updated.json则使用备份的旧内容进行比对
var overlapRatio = CalculateRepoOverlapRatio(oldRepoJsonContent, newRepoJsonContent);
if (overlapRatio < 0.5)
{
_logger.LogInformation("仓库目录结构重合度低 ({Ratio:P0}),判定为不同仓库,不继承旧的更新标记", overlapRatio);
updatedContent = newRepoJsonContent;
}
else
{
updatedContent = AddUpdateMarkersToNewRepo(oldRepoJsonContent, newRepoJsonContent);
}
}
else
{
// 全新仓库,无旧内容可比对
updatedContent = newRepoJsonContent;
}
// 保存到按仓库区分的 repo_updated 文件
var updatedRepoJsonPath = GetRepoUpdatedJsonPathForFolder(repoFolderName);
await File.WriteAllTextAsync(updatedRepoJsonPath, updatedContent);
_logger.LogInformation($"已标记repo.json中的更新节点并保存到: {updatedRepoJsonPath}");
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "标记repo.json更新节点失败");
}
return (repoPath, updated);
}
/// <summary>
/// 计算两个 repo.json 的目录结构重合度(基于目录路径的 Overlap Coefficient
/// 用于判断是否为同一仓库的不同版本还是完全不同的仓库
/// </summary>
/// <param name="oldContent">旧版 repo.json 内容</param>
/// <param name="newContent">新版 repo.json 内容</param>
/// <returns>重合度 0.0 ~ 1.0,解析失败返回 -1.0</returns>
private double CalculateRepoOverlapRatio(string oldContent, string newContent)
{
try
{
var oldJson = JObject.Parse(oldContent);
var newJson = JObject.Parse(newContent);
var oldPaths = new HashSet<string>();
var newPaths = new HashSet<string>();
CollectDirectoryPaths(oldJson["indexes"] as JArray, "", oldPaths);
CollectDirectoryPaths(newJson["indexes"] as JArray, "", newPaths);
if (oldPaths.Count == 0 && newPaths.Count == 0) return 1.0;
if (oldPaths.Count == 0 || newPaths.Count == 0) return 0.0;
var intersection = oldPaths.Intersect(newPaths).Count();
var minCount = Math.Min(oldPaths.Count, newPaths.Count);
// 使用 Overlap Coefficient: intersection / min(|A|, |B|)
// 对仓库正常增长(目录只增不减)更加宽容
var ratio = minCount > 0 ? (double)intersection / minCount : 0.0;
_logger.LogDebug("仓库目录结构重合度(Overlap): {Ratio:P0} (旧 {OldCount} 个目录, 新 {NewCount} 个目录, 交集 {Intersection} 个)",
ratio, oldPaths.Count, newPaths.Count, intersection);
return ratio;
}
catch (Exception ex)
{
_logger.LogDebug(ex, "计算仓库重合度失败JSON 解析异常)");
return -1.0;
}
}
/// <summary>
/// 递归收集 indexes 中的所有目录节点路径(仅 type == "directory" 的节点)
/// 只用目录结构判断仓库重合度,不受具体文件增删影响
/// </summary>
private void CollectDirectoryPaths(JArray? nodes, string prefix, HashSet<string> paths)
{
if (nodes == null) return;
foreach (var node in nodes.OfType<JObject>())
{
var name = node["name"]?.ToString();
if (string.IsNullOrEmpty(name)) continue;
if (node["type"]?.ToString() != "directory") continue;
var fullPath = string.IsNullOrEmpty(prefix) ? name : $"{prefix}/{name}";
paths.Add(fullPath);
if (node["children"] is JArray children && children.Count > 0)
{
CollectDirectoryPaths(children, fullPath, paths);
}
}
}
/// <summary>
/// 在新repo.json中添加更新标记
/// </summary>
/// <param name="oldContent">旧版repo.json内容</param>
/// <param name="newContent">新版repo.json内容</param>
/// <returns>添加了hasUpdate标记的新repo.json内容</returns>
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;
}
}
/// <summary>
/// 直接在新版节点上标记更新
/// </summary>
/// <param name="newNode">新版节点</param>
/// <param name="oldNodes">老版节点数组</param>
/// <returns>是否有更新(节点本身或子树)</returns>
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)
{
// 若历史上已标记,则保留该标记
if (IsTruthy(oldNode["hasUpdate"]))
{
newNode["hasUpdate"] = true;
hasDirectUpdate = true;
}
// 对比时间戳
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 && IsTruthy(newChildObj["hasUpdate"]))
{
var parentTime = ParseLastUpdated(newNode["lastUpdated"]?.ToString());
var childTime = ParseLastUpdated(newChildObj["lastUpdated"]?.ToString());
newNode["hasUpdate"] = true;
hasDirectUpdate = true;
if (childTime > parentTime && newChildObj["lastUpdated"] != null)
{
newNode["lastUpdated"] = newChildObj["lastUpdated"];
}
}
}
}
}
}
return hasDirectUpdate || hasChildUpdate;
}
/// <summary>
/// 解析lastUpdated时间戳
/// </summary>
/// <param name="timeString">时间字符串</param>
/// <returns>DateTime对象</returns>
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 bool IsTruthy(JToken? token)
{
if (token == null || token.Type == JTokenType.Null) return false;
if (token.Type == JTokenType.Boolean) return (bool)token;
if (token.Type == JTokenType.String) return string.Equals(token.ToString(), "true", StringComparison.OrdinalIgnoreCase);
return false;
}
private static void SimpleCloneRepository(string repoUrl, string repoPath,
CheckoutProgressHandler? onCheckoutProgress)
{
var options = new CloneOptions
{
BranchName = "release", // 指定分支
Checkout = true,
IsBare = false,
RecurseSubmodules = false, // 不递归克隆子模块
OnCheckoutProgress = onCheckoutProgress
};
// options.FetchOptions.Depth = 1; // 浅克隆,只获取最新的提交
// 设置凭据处理器
options.FetchOptions.CredentialsProvider = Instance.CreateCredentialsHandler();
options.FetchOptions.OnTransferProgress = progress =>
{
onCheckoutProgress?.Invoke($"拉取对象 {progress.ReceivedObjects}/{progress.TotalObjects}", progress.ReceivedObjects, progress.TotalObjects);
return true;
};
// 克隆仓库
Repository.Clone(repoUrl, repoPath, options);
}
/// <summary>
/// 克隆Git仓库只检出repo.json
/// 相当于 Repository.Clone(repoUrl, repoPath, options);
/// 用这个方法可以无视本地代理
/// </summary>
/// <param name="repoUrl"></param>
/// <param name="repoPath"></param>
/// <param name="branchName"></param>
/// <param name="onCheckoutProgress"></param>
/// <exception cref="Exception"></exception>
private void CloneRepository(string repoUrl, string repoPath, string branchName, CheckoutProgressHandler? onCheckoutProgress)
{
DirectoryHelper.DeleteReadOnlyDirectory(repoPath);
Directory.CreateDirectory(repoPath);
Repository.Init(repoPath);
var repo = new Repository(repoPath);
try
{
GitConfig(repo);
// 添加远程源
Remote remote = repo.Network.Remotes.Add("origin", repoUrl);
// 只拉取指定分支
var fetchOptions = new FetchOptions
{
TagFetchMode = TagFetchMode.None,
ProxyOptions = { ProxyType = ProxyType.None },
Depth = 1, // 浅拉取,只获取最新的提交
CredentialsProvider = CreateCredentialsHandler(), // 添加凭据处理器
OnTransferProgress = progress =>
{
onCheckoutProgress?.Invoke($"拉取对象 {progress.ReceivedObjects}/{progress.TotalObjects}", progress.ReceivedObjects, progress.TotalObjects);
return true;
}
};
string refSpec = $"+refs/heads/{branchName}:refs/remotes/origin/{branchName}";
Commands.Fetch(repo, remote.Name, new[] { refSpec }, fetchOptions, "初始化拉取");
// 获取远程分支
var remoteBranch = repo.Branches[$"origin/{branchName}"];
if (remoteBranch == null)
throw new Exception($"远程仓库中未找到 {branchName} 分支");
// 创建本地分支
var localBranch = repo.CreateBranch(branchName, remoteBranch.Tip);
repo.Branches.Update(localBranch, b => b.TrackedBranch = remoteBranch.CanonicalName);
// 手动检出HEAD到新分支
repo.Refs.UpdateTarget(repo.Refs.Head, localBranch.CanonicalName);
// 手动检出 repo.json 文件
CheckoutRepoJson(repo, remoteBranch.Tip);
}
finally
{
repo?.Dispose();
}
}
/// <summary>
/// 从Git仓库检出repo.json文件到工作目录
/// </summary>
/// <param name="repo"></param>
/// <param name="commit"></param>
private void CheckoutRepoJson(Repository repo, Commit commit)
{
try
{
// 查找repo.json文件
var repoJsonEntry = commit.Tree.FirstOrDefault(e => e.Name == "repo.json");
if (repoJsonEntry != null && repoJsonEntry.TargetType == TreeEntryTargetType.Blob)
{
var blob = (Blob)repoJsonEntry.Target;
var repoJsonPath = Path.Combine(repo.Info.WorkingDirectory ?? repo.Info.Path, "repo.json");
// 创建目录(如果需要)
var dir = Path.GetDirectoryName(repoJsonPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
// 写入文件
using (var contentStream = blob.GetContentStream())
using (var fileStream = File.Create(repoJsonPath))
{
contentStream.CopyTo(fileStream);
}
}
else
{
_logger.LogWarning("未在仓库中找到 repo.json 文件");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "检出 repo.json 失败");
}
}
/// <summary>
/// 检查指定路径是否为 Git 仓库(非文件式仓库)
/// </summary>
/// <param name="repoPath">仓库路径</param>
/// <returns>如果是 Git 仓库返回 true否则返回 false</returns>
private bool IsGitRepository(string repoPath)
{
return Repository.IsValid(repoPath) && !Directory.Exists(Path.Combine(repoPath, "repo"));
}
/// <summary>
/// 获取仓库中 repo/ 子目录的树对象
/// </summary>
private Tree GetRepoSubdirectoryTree(Repository repo)
{
var commit = repo.Head?.Tip;
if (commit == null)
{
throw new Exception("仓库HEAD未指向任何提交");
}
// 脚本内容都在 repo/ 子目录下
var repoEntry = commit.Tree["repo"];
if (repoEntry == null || repoEntry.TargetType != TreeEntryTargetType.Tree)
{
throw new Exception("仓库结构错误:未找到 repo/ 子目录");
}
return (Tree)repoEntry.Target;
}
/// <summary>
/// 从中央仓库读取文件内容
/// </summary>
/// <param name="relPath">相对于仓库根目录的路径</param>
/// <returns>文件内容如果文件不存在则返回null</returns>
public string? ReadFileFromCenterRepo(string relPath)
{
try
{
var repoPath = CenterRepoPath;
// 判断是否为 Git 仓库
bool isGitRepo = IsGitRepository(repoPath);
if (isGitRepo)
{
return ReadFileFromGitRepository(repoPath, relPath);
}
else
{
// 文件式仓库:从文件系统读取
var filePath = Path.Combine(repoPath, "repo", relPath);
if (File.Exists(filePath))
{
return File.ReadAllText(filePath);
}
return null;
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"从中央仓库读取文件失败: {relPath}");
return null;
}
}
/// <summary>
/// 从中央仓库读取二进制文件
/// </summary>
/// <param name="relPath">相对于仓库根目录的路径</param>
/// <returns>文件字节数组如果文件不存在则返回null</returns>
public byte[]? ReadBinaryFileFromCenterRepo(string relPath)
{
try
{
var repoPath = CenterRepoPath;
// 判断是否为 Git 仓库
bool isGitRepo = IsGitRepository(repoPath);
if (isGitRepo)
{
return ReadBinaryFileFromGitRepository(repoPath, relPath);
}
else
{
// 文件式仓库:从文件系统读取
var filePath = Path.Combine(repoPath, "repo", relPath);
if (File.Exists(filePath))
{
return File.ReadAllBytes(filePath);
}
return null;
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"从中央仓库读取二进制文件失败: {relPath}");
return null;
}
}
/// <summary>
/// 从Git仓库读取文件内容
/// </summary>
private string? ReadFileFromGitRepository(string repoPath, string filePath)
{
try
{
// 判断是否为 Git 仓库
bool isGitRepo = IsGitRepository(repoPath);
if (!isGitRepo)
{
return null;
}
using var repo = new Repository(repoPath);
var manifestPath = $"repo/{filePath}";
var pathParts = manifestPath.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
Tree currentTree = repo.Head.Tip!.Tree;
TreeEntry? entry = null;
for (int i = 0; i < pathParts.Length; i++)
{
entry = currentTree[pathParts[i]];
if (entry == null)
{
return null;
}
if (i < pathParts.Length - 1)
{
if (entry.TargetType != TreeEntryTargetType.Tree)
{
return null;
}
currentTree = (Tree)entry.Target;
}
}
if (entry == null || entry.TargetType != TreeEntryTargetType.Blob)
{
return null;
}
var blob = (Blob)entry.Target;
using var contentStream = blob.GetContentStream();
using var reader = new StreamReader(contentStream);
return reader.ReadToEnd();
}
catch (Exception ex)
{
_logger.LogError(ex, $"从Git仓库读取文件失败: {filePath}");
return null;
}
}
/// <summary>
/// 从Git仓库读取二进制文件内容
/// </summary>
private byte[]? ReadBinaryFileFromGitRepository(string repoPath, string filePath)
{
try
{
// 判断是否为 Git 仓库
bool isGitRepo = IsGitRepository(repoPath);
if (!isGitRepo)
{
return null;
}
using var repo = new Repository(repoPath);
var manifestPath = $"repo/{filePath}";
var pathParts = manifestPath.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
Tree currentTree = repo.Head.Tip!.Tree;
TreeEntry? entry = null;
for (int i = 0; i < pathParts.Length; i++)
{
entry = currentTree[pathParts[i]];
if (entry == null)
{
return null;
}
if (i < pathParts.Length - 1)
{
if (entry.TargetType != TreeEntryTargetType.Tree)
{
return null;
}
currentTree = (Tree)entry.Target;
}
}
if (entry == null || entry.TargetType != TreeEntryTargetType.Blob)
{
return null;
}
var blob = (Blob)entry.Target;
using var contentStream = blob.GetContentStream();
using var memoryStream = new MemoryStream();
contentStream.CopyTo(memoryStream);
return memoryStream.ToArray();
}
catch (Exception ex)
{
_logger.LogError(ex, $"从Git仓库读取二进制文件失败: {filePath}");
return null;
}
}
/// <summary>
/// 从仓库中检出指定路径的文件或文件夹到目标位置
/// </summary>
/// <param name="repoPath">仓库路径</param>
/// <param name="sourcePath">仓库中的相对路径</param>
/// <param name="destPath">目标路径</param>
private void CheckoutPath(string repoPath, string sourcePath, string destPath)
{
// 判断仓库类型:检查是否为 Git 仓库且不存在 repo/ 子目录
bool isGitRepo = IsGitRepository(repoPath);
if (isGitRepo)
{
// 从Git仓库检出
using var repo = new Repository(repoPath);
var commit = repo.Head.Tip;
if (commit == null)
{
_logger.LogError($"仓库HEAD未指向任何提交。HEAD: {repo.Head?.CanonicalName ?? "null"}");
throw new Exception("仓库HEAD未指向任何提交");
}
// 递归查找路径
TreeEntry? entry = null;
var pathParts = sourcePath.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
Tree currentTree = GetRepoSubdirectoryTree(repo);
for (int i = 0; i < pathParts.Length; i++)
{
entry = currentTree[pathParts[i]];
if (entry == null)
{
// 调试信息:列出当前树中的所有条目
// var availableEntries = string.Join(", ", currentTree.Select(e => e.Name));
// _logger.LogError($"在路径 '{string.Join("/", pathParts.Take(i))}' 中未找到 '{pathParts[i]}'");
// _logger.LogError($"可用的条目: {availableEntries}");
// throw new Exception($"仓库中不存在路径: {sourcePath}");
return;
}
if (i < pathParts.Length - 1)
{
if (entry.TargetType != TreeEntryTargetType.Tree)
{
throw new Exception($"路径中间部分不是目录: {string.Join("/", pathParts.Take(i + 1))}");
}
currentTree = (Tree)entry.Target;
}
}
// 检出文件或目录
if (entry == null)
{
// throw new Exception($"未找到路径: {sourcePath}");
return;
}
if (entry.TargetType == TreeEntryTargetType.Blob)
{
// 检出单个文件
var blob = (Blob)entry.Target;
// 确保目标目录存在
var dir = Path.GetDirectoryName(destPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
using (var contentStream = blob.GetContentStream())
using (var fileStream = File.Create(destPath))
{
contentStream.CopyTo(fileStream);
}
}
else if (entry.TargetType == TreeEntryTargetType.Tree)
{
// 检出目录
var tree = (Tree)entry.Target;
CheckoutTree(tree, destPath, sourcePath);
}
}
else
{
// 文件式仓库:从文件系统复制
var scriptPath = Path.Combine(repoPath, sourcePath);
if (Directory.Exists(scriptPath))
{
// 复制目录
CopyDirectory(scriptPath, destPath);
}
else if (File.Exists(scriptPath))
{
// 复制文件
var dir = Path.GetDirectoryName(destPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
File.Copy(scriptPath, destPath, true);
}
else
{
throw new Exception($"仓库中不存在路径: {sourcePath}");
}
}
}
/// <summary>
/// 递归检出树对象
/// </summary>
private void CheckoutTree(Tree tree, string destPath, string currentPath)
{
if (!Directory.Exists(destPath))
{
Directory.CreateDirectory(destPath);
}
foreach (var entry in tree)
{
var entryDestPath = Path.Combine(destPath, entry.Name);
var entrySourcePath = $"{currentPath}/{entry.Name}";
if (entry.TargetType == TreeEntryTargetType.Blob)
{
var blob = (Blob)entry.Target;
using var contentStream = blob.GetContentStream();
using var fileStream = File.Create(entryDestPath);
contentStream.CopyTo(fileStream);
}
else if (entry.TargetType == TreeEntryTargetType.Tree)
{
var subTree = (Tree)entry.Target;
CheckoutTree(subTree, entryDestPath, entrySourcePath);
}
}
}
private void GitConfig(Repository repo)
{
// 设置 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");
}
/// <summary>
/// 创建凭据处理器(用于私有仓库身份验证)
/// </summary>
/// <returns>凭据处理器</returns>
private CredentialsHandler? CreateCredentialsHandler()
{
// 从凭据管理器读取 Git 凭据
var credential = CredentialManagerHelper.ReadCredential("BetterGenshinImpact.GitCredentials");
// 返回凭据处理器
return (url, usernameFromUrl, types) =>
{
_logger.LogInformation($"使用配置的Git凭据进行身份验证");
return new UsernamePasswordCredentials
{
Username = credential?.UserName ?? "",
Password = credential?.Password ?? ""
};
};
}
// [Obsolete]
// public async Task<(string, bool)> UpdateCenterRepo()
// {
// // 测速并获取信息
// var (fastUrl, jsonString) = await ProxySpeedTester.GetFastestUrlAsync(CenterRepoInfoUrls);
// if (string.IsNullOrEmpty(jsonString))
// {
// throw new Exception("从互联网下载最新的仓库信息失败");
// }
//
// var (time, url, file) = ParseJson(jsonString);
//
// var updated = false;
//
// // 检查仓库是否存在,不存在则下载
// var needDownload = false;
// if (Directory.Exists(CenterRepoPath))
// {
// var p = Directory.GetFiles(CenterRepoPath, "repo.json", SearchOption.AllDirectories).FirstOrDefault();
// if (p is null)
// {
// needDownload = true;
// }
// }
// else
// {
// needDownload = true;
// }
//
// if (needDownload)
// {
// await DownloadRepoAndUnzip(url);
// updated = true;
// }
//
// // 搜索本地的 repo.json
// var localRepoJsonPath = Directory.GetFiles(CenterRepoPath, file, SearchOption.AllDirectories).FirstOrDefault();
// if (localRepoJsonPath is null)
// {
// throw new Exception("本地仓库缺少 repo.json");
// }
//
// var (time2, url2, file2) = ParseJson(await File.ReadAllTextAsync(localRepoJsonPath));
//
// // 检查是否需要更新
// if (long.Parse(time) > long.Parse(time2))
// {
// await DownloadRepoAndUnzip(url2);
// updated = true;
// }
//
// // 获取与 localRepoJsonPath 同名(无扩展名)的文件夹路径
// var folderName = Path.GetFileNameWithoutExtension(localRepoJsonPath);
// var folderPath = Path.Combine(Path.GetDirectoryName(localRepoJsonPath)!, folderName);
// if (!Directory.Exists(folderPath))
// {
// throw new Exception("本地仓库文件夹不存在");
// }
//
// return (folderPath, updated);
// }
public string FindCenterRepoPath()
{
// 查找 repo.json 文件
var repoJsonPath = Path.Combine(CenterRepoPath, "repo.json");
string? repoJsonDir = null;
if (File.Exists(repoJsonPath))
{
repoJsonDir = CenterRepoPath;
}
else
{
// 递归查找 repo.json
var localRepoJsonPath = Directory
.GetFiles(CenterRepoPath, "repo.json", SearchOption.AllDirectories).FirstOrDefault();
if (localRepoJsonPath != null)
{
repoJsonDir = Path.GetDirectoryName(localRepoJsonPath);
}
}
if (string.IsNullOrEmpty(repoJsonDir))
{
throw new Exception("本地仓库缺少 repo.json");
}
// 检查 repo.json 同级是否存在 repo/ 目录来判断仓库类型
var repoSubDir = Path.Combine(repoJsonDir, "repo");
if (Directory.Exists(repoSubDir))
{
// 存在 repo/ 目录,说明是文件式仓库
return repoSubDir;
}
else
{
// 不存在 repo/ 目录,说明是 Git 仓库
return repoJsonDir;
}
}
private (string time, string url, string file) ParseJson(string jsonString)
{
var json = JObject.Parse(jsonString);
var time = json["time"]?.ToString();
var url = json["url"]?.ToString();
var file = json["file"]?.ToString();
// 检查是否有空值
if (time is null || url is null || file is null)
{
throw new Exception("repo.json 解析失败");
}
return (time, url, file);
}
/// <summary>
/// 统一的本地 zip 导入方法
/// 解压后自动识别仓库内容,基于目录结构重合度决定覆盖已有仓库还是创建新文件夹,
/// 并生成 repo_updated.json 更新标记
/// </summary>
/// <param name="zipFilePath">本地 zip 文件路径</param>
/// <param name="onProgress">进度回调 (0-100, 描述文本)</param>
/// <returns>导入后的仓库文件夹路径</returns>
public async Task<string> ImportLocalRepoZip(string zipFilePath, Action<int, string>? onProgress = null)
{
await _repoWriteLock.WaitAsync();
try
{
return await ImportLocalRepoZipCore(zipFilePath, onProgress);
}
finally
{
_repoWriteLock.Release();
}
}
private async Task<string> ImportLocalRepoZipCore(string zipFilePath, Action<int, string>? onProgress = null)
{
var tempUnzipDir = Path.Combine(ReposTempPath, "importZipFile");
string targetFolderName = CenterRepoFolderName;
try
{
// 阶段1: 准备 (0-10%)
onProgress?.Invoke(0, "正在准备导入环境...");
DirectoryHelper.DeleteReadOnlyDirectory(ReposTempPath);
Directory.CreateDirectory(tempUnzipDir);
onProgress?.Invoke(10, "准备完成,开始解压文件...");
// 阶段2: 解压 (10-50%)
await Task.Run(() => ZipFile.ExtractToDirectory(zipFilePath, tempUnzipDir, true));
onProgress?.Invoke(50, "文件解压完成,正在验证仓库结构...");
// 阶段3: 查找 repo.json (50-55%)
var repoJsonPath = Directory.GetFiles(tempUnzipDir, "repo.json", SearchOption.AllDirectories).FirstOrDefault();
if (repoJsonPath == null)
{
throw new FileNotFoundException("未找到 repo.json 文件,不是有效的脚本仓库压缩包。");
}
var repoDir = Path.GetDirectoryName(repoJsonPath)!;
var newRepoJsonContent = await File.ReadAllTextAsync(repoJsonPath);
onProgress?.Invoke(55, "仓库结构验证通过,正在分析仓库内容...");
// 阶段4: 基于目录结构重合度决定目标文件夹 (55-70%)
string? bestMatchFolder = null;
double bestOverlap = 0;
// 扫描已有仓库,找目录结构重合度最高的
if (Directory.Exists(ReposPath))
{
foreach (var existingDir in Directory.GetDirectories(ReposPath))
{
try
{
var dirName = Path.GetFileName(existingDir);
if (dirName == "Temp") continue;
// 尝试读取已有仓库的 repo.json 或 repo_updated.json
var existingRepoUpdated = Path.Combine(existingDir, "repo_updated.json");
var existingRepoJson = Directory.GetFiles(existingDir, "repo.json", SearchOption.AllDirectories).FirstOrDefault();
var existingContent = File.Exists(existingRepoUpdated)
? await File.ReadAllTextAsync(existingRepoUpdated)
: (existingRepoJson != null ? await File.ReadAllTextAsync(existingRepoJson) : null);
if (!string.IsNullOrEmpty(existingContent))
{
var overlap = CalculateRepoOverlapRatio(existingContent, newRepoJsonContent);
if (overlap > bestOverlap)
{
bestOverlap = overlap;
bestMatchFolder = dirName;
}
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Zip导入扫描仓库目录 {Dir} 时出错,跳过", existingDir);
}
}
}
onProgress?.Invoke(65, "内容分析完成,正在确定目标位置...");
string targetPath;
string? oldRepoContent = null;
if (bestOverlap >= 0.5 && bestMatchFolder != null)
{
// 高重合度 → 覆盖已有仓库
targetFolderName = bestMatchFolder;
targetPath = Path.Combine(ReposPath, targetFolderName);
_logger.LogInformation("Zip导入目录结构重合度 {Ratio:P0},覆盖已有仓库 {Folder}", bestOverlap, targetFolderName);
// 读取旧的 repo_updated.json 用于生成更新标记
var oldUpdatedPath = Path.Combine(targetPath, "repo_updated.json");
if (File.Exists(oldUpdatedPath))
{
oldRepoContent = await File.ReadAllTextAsync(oldUpdatedPath);
}
else
{
var oldRepoJson = Directory.GetFiles(targetPath, "repo.json", SearchOption.AllDirectories).FirstOrDefault();
if (oldRepoJson != null)
oldRepoContent = await File.ReadAllTextAsync(oldRepoJson);
}
DirectoryHelper.DeleteReadOnlyDirectory(targetPath);
}
else if (bestOverlap < 0.5 && bestMatchFolder != null)
{
// 低重合度 → 全新仓库,创建新文件夹
var baseName = CenterRepoFolderName;
// 如果默认文件夹不存在,就用默认名
if (!Directory.Exists(Path.Combine(ReposPath, baseName)))
{
targetFolderName = baseName;
}
else
{
targetFolderName = GenerateUniqueFolderName(baseName);
}
targetPath = Path.Combine(ReposPath, targetFolderName);
_logger.LogInformation("Zip导入目录结构重合度 {Ratio:P0},创建新文件夹 {Folder}", bestOverlap, targetFolderName);
}
else
{
// 没有已有仓库,使用默认文件夹名
targetPath = Path.Combine(ReposPath, targetFolderName);
if (Directory.Exists(targetPath))
{
// 读取旧内容用于生成更新标记
var oldUpdatedPath = Path.Combine(targetPath, "repo_updated.json");
if (File.Exists(oldUpdatedPath))
oldRepoContent = await File.ReadAllTextAsync(oldUpdatedPath);
DirectoryHelper.DeleteReadOnlyDirectory(targetPath);
}
}
onProgress?.Invoke(70, "正在复制仓库文件...");
// 阶段5: 拷贝仓库到目标位置 (70-90%)
DirectoryHelper.CopyDirectory(repoDir, targetPath);
onProgress?.Invoke(90, "仓库复制完成,正在生成更新标记...");
// 阶段6: 生成 repo_updated.json (90-95%)
try
{
var updatedJsonPath = Path.Combine(targetPath, "repo_updated.json");
if (!string.IsNullOrEmpty(oldRepoContent))
{
var overlapWithOld = CalculateRepoOverlapRatio(oldRepoContent, newRepoJsonContent);
if (overlapWithOld >= 0.5)
{
var updatedContent = AddUpdateMarkersToNewRepo(oldRepoContent, newRepoJsonContent);
await File.WriteAllTextAsync(updatedJsonPath, updatedContent);
_logger.LogInformation("Zip导入已生成更新标记 repo_updated.json");
}
else
{
// 目录结构差异太大,直接使用新内容
await File.WriteAllTextAsync(updatedJsonPath, newRepoJsonContent);
}
}
else
{
// 全新导入,直接使用新内容
await File.WriteAllTextAsync(updatedJsonPath, newRepoJsonContent);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Zip导入生成 repo_updated.json 失败");
}
onProgress?.Invoke(95, "正在清理临时文件...");
}
finally
{
// 阶段7: 清理 (95-100%)
DirectoryHelper.DeleteReadOnlyDirectory(ReposTempPath);
}
onProgress?.Invoke(100, "导入完成");
_logger.LogInformation("Zip导入完成目标文件夹: {Folder}", targetFolderName);
return Path.Combine(ReposPath, targetFolderName);
}
public async Task DownloadRepoAndUnzip(string url)
{
await _repoWriteLock.WaitAsync();
try
{
await DownloadRepoAndUnzipCore(url);
}
finally
{
_repoWriteLock.Release();
}
}
private async Task DownloadRepoAndUnzipCore(string url)
{
// 下载
var res = await _httpClient.GetAsync(url);
if (!res.IsSuccessStatusCode)
{
throw new Exception("下载失败");
}
var bytes = await res.Content.ReadAsByteArrayAsync();
// 获取文件名
var contentDisposition = res.Content.Headers.ContentDisposition;
var fileName = contentDisposition is { FileName: not null }
? contentDisposition.FileName.Trim('"')
: "temp.zip";
// 创建临时目录
if (!Directory.Exists(ReposTempPath))
{
Directory.CreateDirectory(ReposTempPath);
}
// 保存下载的文件
var zipPath = Path.Combine(ReposTempPath, fileName);
await File.WriteAllBytesAsync(zipPath, bytes);
// 删除旧文件夹
if (Directory.Exists(CenterRepoPath))
{
DirectoryHelper.DeleteReadOnlyDirectory(CenterRepoPath);
}
// 使用 System.IO.Compression 解压
ZipFile.ExtractToDirectory(zipPath, ReposPath, true);
}
public async Task ImportScriptFromClipboard(string clipboardText)
{
// 获取剪切板内容
try
{
await ImportScriptFromUri(clipboardText, true);
}
catch (Exception e)
{
// 剪切板内容可能获取会失败
Console.WriteLine(e);
}
}
public async Task ImportScriptFromUri(string uri, bool formClipboard)
{
// 检查剪切板内容是否符合特定的URL格式
if (!string.IsNullOrEmpty(uri) && uri.Trim().ToLower().StartsWith("bettergi://script?import="))
{
Debug.WriteLine($"脚本订购内容:{uri}");
// 执行相关操作
var pathJson = ParseUri(uri);
if (!string.IsNullOrEmpty(pathJson))
{
var uiMessageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "脚本订阅",
Content =
$"检测到{(formClipboard ? "" : "")}脚本订阅链接,解析后需要导入的脚本为:{pathJson}。\n是否导入并覆盖此文件或者文件夹下的脚本",
CloseButtonText = "关闭",
PrimaryButtonText = "确认导入",
Owner = Application.Current.MainWindow,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
};
uiMessageBox.SourceInitialized += (s, e) => WindowHelper.TryApplySystemBackdrop(uiMessageBox);
var result = await uiMessageBox.ShowDialogAsync();
if (result == Wpf.Ui.Controls.MessageBoxResult.Primary)
{
await ImportScriptFromPathJson(pathJson);
}
// ContentDialog dialog = new()
// {
// Title = "脚本订阅",
// Content = $"检测到{(formClipboard ? "剪切板上存在" : "")}脚本订阅链接,解析后需要导入的脚本为:{pathJson}。\n是否导入并覆盖此文件或者文件夹下的脚本",
// CloseButtonText = "关闭",
// PrimaryButtonText = "确认导入",
// };
//
// var result = await dialog.ShowAsync();
// if (result == ContentDialogResult.Primary)
// {
// await ImportScriptFromPathJson(pathJson);
// }
}
if (formClipboard)
{
// 清空剪切板内容
Clipboard.Clear();
}
}
}
private string? ParseUri(string uriString)
{
var uri = new Uri(uriString);
// 获取 query 参数
string query = uri.Query;
Debug.WriteLine($"Query: {query}");
// 解析 query 参数
var queryParams = System.Web.HttpUtility.ParseQueryString(query);
var import = queryParams["import"];
if (string.IsNullOrEmpty(import))
{
Debug.WriteLine("未找到 import 参数");
return null;
}
// Base64 解码后再使用url解码
byte[] data = Convert.FromBase64String(import);
return System.Web.HttpUtility.UrlDecode(System.Text.Encoding.UTF8.GetString(data));
}
public async Task ImportScriptFromPathJson(string pathJson)
{
await _repoWriteLock.WaitAsync();
try
{
await ImportScriptFromPathJsonCore(pathJson);
}
finally
{
_repoWriteLock.Release();
}
}
private async Task ImportScriptFromPathJsonCore(string pathJson)
{
var paths = Newtonsoft.Json.JsonConvert.DeserializeObject<List<string>>(pathJson);
if (paths is null || paths.Count == 0)
{
Toast.Warning("订阅脚本路径为空");
return;
}
// 保存订阅信息(按当前仓库存储到文件)
AddSubscribedPathsForCurrentRepo(paths);
Toast.Information("获取最新仓库信息中...");
string repoPath;
try
{
repoPath = FindCenterRepoPath();
}
catch
{
await ThemedMessageBox.ErrorAsync("本地无仓库信息,请至少成功更新一次脚本仓库信息!");
return;
}
// // 收集将被覆盖的文件和文件夹
// var filesToOverwrite = new List<string>();
// foreach (var path in paths)
// {
// var first = GetFirstFolder(path);
// if (PathMapper.TryGetValue(first, out var userPath))
// {
// var scriptPath = Path.Combine(repoPath, path);
// var destPath = Path.Combine(userPath, path.Replace(first, ""));
// if (Directory.Exists(scriptPath))
// {
// if (Directory.Exists(destPath))
// {
// filesToOverwrite.Add(destPath);
// }
// }
// else if (File.Exists(scriptPath))
// {
// if (File.Exists(destPath))
// {
// filesToOverwrite.Add(destPath);
// }
// }
// }
// else
// {
// Toast.Warning($"未知的脚本路径:{path}");
// }
// }
//
// // 提示用户确认
// if (filesToOverwrite.Count > 0)
// {
// var message = "以下文件和文件夹将被覆盖:\n" + string.Join("\n", filesToOverwrite) + "\n是否覆盖所有文件和文件夹";
// var uiMessageBox = new Wpf.Ui.Controls.MessageBox
// {
// Title = "确认覆盖",
// Content = message,
// CloseButtonText = "取消",
// PrimaryButtonText = "确认覆盖",
// WindowStartupLocation = WindowStartupLocation.CenterOwner,
// Owner = Application.Current.MainWindow,
// };
//
// var result = await uiMessageBox.ShowDialogAsync();
// if (result != Wpf.Ui.Controls.MessageBoxResult.Primary)
// {
// return;
// }
// }
//顶层目录订阅时,展开 "pathing" 等顶层路径为具体子目录
List<string> newPaths = ExpandTopLevelPaths(paths, repoPath);
// 从 Git 仓库检出文件到用户文件夹
foreach (var path in newPaths)
{
var (first, remainingPath) = GetFirstFolderAndRemainingPath(path);
if (PathMapper.TryGetValue(first, out var userPath))
{
var destPath = Path.Combine(userPath, remainingPath);
// 备份需要保存的文件
List<string> backupFiles = new List<string>();
if (first == "js") // 只对JS脚本进行备份
{
backupFiles = BackupScriptFiles(path, repoPath);
}
// 如果目标路径存在,先删除
if (Directory.Exists(destPath))
{
DirectoryHelper.DeleteDirectoryWithReadOnlyCheck(destPath);
}
else if (File.Exists(destPath))
{
File.Delete(destPath);
}
// 从 Git 仓库检出文件或目录
CheckoutPath(repoPath, path, destPath);
// 图标处理(仅对目录)
if (Directory.Exists(destPath))
{
DealWithIconFolder(destPath);
}
// 恢复备份的文件
if (first == "js" && backupFiles.Count > 0) // 只对JS脚本进行恢复
{
RestoreScriptFiles(path, repoPath);
}
// Resolving dependencies for JS scripts
if (first == "js")
{
ResolveScriptDependencies(repoPath, destPath);
}
UpdateSubscribedScriptPaths();
Toast.Success("脚本订阅链接导入完成");
}
else
{
Toast.Warning($"未知的脚本路径:{path}");
}
}
}
// ========== 文件级订阅路径存储 ==========
// 订阅数据存储在 User/subscriptions/{repoFolderName}.json与仓库目录和主配置解耦
/// <summary>
/// 订阅文件存储目录
/// </summary>
public static readonly string SubscriptionsPath = Global.Absolute(@"User\Subscriptions");
/// <summary>
/// 获取当前活跃仓库的文件夹名称
/// </summary>
private static string GetCurrentRepoFolderName()
{
return Path.GetFileName(CenterRepoPath);
}
/// <summary>
/// 获取指定仓库的订阅文件路径
/// </summary>
private static string GetSubscriptionFilePath(string repoFolderName)
{
return Path.Combine(SubscriptionsPath, $"{repoFolderName}.json");
}
/// <summary>
/// 获取当前仓库的已订阅脚本路径列表
/// </summary>
public static List<string> GetSubscribedPathsForCurrentRepo()
{
var filePath = GetSubscriptionFilePath(GetCurrentRepoFolderName());
return ReadSubscriptionFile(filePath);
}
/// <summary>
/// 设置当前仓库的已订阅脚本路径列表
/// </summary>
private static void SetSubscribedPathsForCurrentRepo(List<string> paths)
{
var filePath = GetSubscriptionFilePath(GetCurrentRepoFolderName());
WriteSubscriptionFile(filePath, paths);
}
/// <summary>
/// 向当前仓库的已订阅路径中追加新路径(自动去重)。
/// 注意:内部的读-合并-写不是原子操作,调用方应持有 _repoWriteLock 以避免并发丢失更新。
/// </summary>
private static void AddSubscribedPathsForCurrentRepo(List<string> paths)
{
var existing = GetSubscribedPathsForCurrentRepo();
var merged = existing.Union(paths).ToList();
SetSubscribedPathsForCurrentRepo(merged);
}
/// <summary>
/// 订阅文件读写锁,允许并发读、互斥写
/// </summary>
private static readonly ReaderWriterLockSlim _subscriptionRwLock = new();
/// <summary>
/// 从订阅文件读取路径列表
/// </summary>
private static List<string> ReadSubscriptionFile(string filePath)
{
_subscriptionRwLock.EnterReadLock();
try
{
if (!File.Exists(filePath))
return new List<string>();
var json = File.ReadAllText(filePath);
if (string.IsNullOrWhiteSpace(json))
return new List<string>();
return System.Text.Json.JsonSerializer.Deserialize<List<string>>(json, ConfigService.JsonOptions) ?? new List<string>();
}
catch (Exception ex)
{
Debug.WriteLine($"[ScriptRepoUpdater] 读取订阅文件失败: {filePath},订阅数据可能已损坏: {ex.Message}");
return new List<string>();
}
finally
{
_subscriptionRwLock.ExitReadLock();
}
}
/// <summary>
/// 将路径列表写入订阅文件
/// </summary>
private static void WriteSubscriptionFile(string filePath, List<string> paths)
{
_subscriptionRwLock.EnterWriteLock();
try
{
if (!Directory.Exists(SubscriptionsPath))
Directory.CreateDirectory(SubscriptionsPath);
if (paths.Count == 0)
{
// 空列表时删除文件
if (File.Exists(filePath))
File.Delete(filePath);
return;
}
var json = System.Text.Json.JsonSerializer.Serialize(paths, ConfigService.JsonOptions);
File.WriteAllText(filePath, json);
}
catch (Exception ex)
{
Debug.WriteLine($"[ScriptRepoUpdater] 写入订阅文件失败: {filePath}: {ex.Message}");
throw; // 传播异常让调用方决定如何处理
}
finally
{
_subscriptionRwLock.ExitWriteLock();
}
}
/// <summary>
/// 在启动时从旧 config.json 中的 subscribedScriptPaths 迁移到独立订阅文件。
/// 通过框架反序列化读取旧数据,迁移后清空配置属性让框架自动保存。
/// </summary>
public void MigrateSubscribedPathsFromConfig()
{
try
{
// 如果订阅目录已存在且非空,说明已迁移过
if (Directory.Exists(SubscriptionsPath) && Directory.GetFiles(SubscriptionsPath, "*.json").Length > 0)
return;
var scriptConfig = TaskContext.Instance().Config.ScriptConfig;
var oldPaths = scriptConfig.SubscribedScriptPaths;
if (oldPaths.Count == 0)
return;
// 全部归入当前仓库,幽灵路径由后续 UpdateAllSubscribedScriptsCore 统一清理
WriteSubscriptionFile(GetSubscriptionFilePath(GetCurrentRepoFolderName()), [.. oldPaths]);
// 清空配置属性,框架自动保存
scriptConfig.SubscribedScriptPaths = new List<string>();
_logger.LogInformation("已完成订阅路径迁移到独立文件: {Count} 个路径", oldPaths.Count);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "从 config.json 迁移订阅路径到独立文件失败");
}
}
// 更新订阅脚本路径列表,移除无效路径(仅处理当前仓库的订阅)
public void UpdateSubscribedScriptPaths()
{
var validRoots = PathMapper.Keys.ToHashSet();
var allPaths = GetSubscribedPathsForCurrentRepo()
.Distinct()
.OrderBy(path => path)
.ToList();
var pathsToKeep = new HashSet<string>();
foreach (var path in allPaths)
{
if (string.IsNullOrEmpty(path) || !path.Contains('/'))
continue;
var root = path.Split('/')[0];
if (!validRoots.Contains(root))
continue;
var (_, remainingPath) = GetFirstFolderAndRemainingPath(path);
var userPath = Path.Combine(PathMapper[root], remainingPath);
if (!Directory.Exists(userPath) && !File.Exists(userPath))
continue;
// 检查是否已被父路径覆盖
bool isCoveredByParent = pathsToKeep.Any(p =>
path.StartsWith(p + "/") || path == p);
if (!isCoveredByParent)
{
pathsToKeep.Add(path);
}
}
// 添加父节点自动订阅逻辑
try
{
// 获取所有可用路径
var allAvailablePaths = new HashSet<string>();
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<string, List<string>>();
// 遍历所有路径,找到每个节点的父节点
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<string>();
}
if (!parentChildMap[parentPath].Contains(path))
{
parentChildMap[parentPath].Add(path);
}
}
}
// 递归检查父节点,直到没有新的父节点需要添加
bool hasNewPaths;
do
{
hasNewPaths = false;
var pathsToAdd = new HashSet<string>();
// 检查每个父节点
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, "添加父节点时发生错误");
}
SetSubscribedPathsForCurrentRepo(pathsToKeep
.OrderBy(path => path)
.ToList());
}
private void CopyDirectory(string sourceDir, string destDir)
{
// 创建目标目录
Directory.CreateDirectory(destDir);
// 拷贝文件
foreach (var file in Directory.GetFiles(sourceDir))
{
var destFile = Path.Combine(destDir, Path.GetFileName(file));
File.Copy(file, destFile, true);
}
// 拷贝子目录
foreach (var dir in Directory.GetDirectories(sourceDir))
{
var destSubDir = Path.Combine(destDir, Path.GetFileName(dir));
CopyDirectory(dir, destSubDir);
// 图标处理
DealWithIconFolder(destSubDir);
}
}
/// <summary>
/// 解析并下载脚本依赖 (packages)
/// </summary>
/// <param name="repoPath">仓库路径</param>
/// <param name="localScriptPath">本地脚本路径</param>
private void ResolveScriptDependencies(string repoPath, string localScriptPath)
{
try
{
var processedFiles = new HashSet<string>();
var processingQueue = new Queue<string>();
// 确定根目录
string baseDestDir;
if (File.Exists(localScriptPath))
{
processingQueue.Enqueue(localScriptPath);
baseDestDir = Path.GetDirectoryName(localScriptPath) ?? localScriptPath;
}
else if (Directory.Exists(localScriptPath))
{
baseDestDir = localScriptPath;
// 初始加入目录下的所有 JS 文件
foreach (var f in Directory.GetFiles(localScriptPath, "*.js", SearchOption.AllDirectories))
{
processingQueue.Enqueue(f);
}
}
else
{
return;
}
// 清理 packages
var targetPackagesDir = Path.Combine(baseDestDir, "packages");
if (Directory.Exists(targetPackagesDir))
{
try
{
Directory.Delete(targetPackagesDir, true);
// _logger.LogInformation($"已清理旧依赖目录: {targetPackagesDir}");
}
catch (Exception ex)
{
_logger.LogWarning($"清理依赖目录失败: {ex.Message}");
}
}
// 捕获导入的变量名
var regex = new System.Text.RegularExpressions.Regex(
@"(import\s+([\w\d_$]+)\s+from\s+['""]|import\s+(?:[\w\s{},*]*?from\s+)?['""]|export\s+(?:[\w\s{},*]*?from\s+)?['""]|import\s+['""]|require\s*\(\s*['""])([^'""\n]+)(['""])");
while (processingQueue.Count > 0)
{
var currentFile = processingQueue.Dequeue();
// 避免重复处理
if (processedFiles.Contains(currentFile)) continue;
processedFiles.Add(currentFile);
try
{
if (!File.Exists(currentFile)) continue;
var content = File.ReadAllText(currentFile);
var matches = regex.Matches(content);
foreach (System.Text.RegularExpressions.Match match in matches)
{
var originalPath = match.Groups[3].Value;
string? packagePath = null;
// 识别是否为 packages 引用
int packageIndex = originalPath.IndexOf("packages/", StringComparison.OrdinalIgnoreCase);
if (packageIndex >= 0)
{
packagePath = originalPath.Substring(packageIndex).Replace('\\', '/');
}
// 识别是否为 packages 内部的相对引用
else if (originalPath.StartsWith("."))
{
// 检查当前文件是否在 packages 目录下
var localPackagesDir = Path.Combine(baseDestDir, "packages");
if (currentFile.StartsWith(localPackagesDir, StringComparison.OrdinalIgnoreCase))
{
// 计算当前文件对应的 repo 路径
var relToScript = Path.GetRelativePath(baseDestDir, currentFile);
var relDir = Path.GetDirectoryName(relToScript); // e.g. packages/utils
if (relDir != null)
{
var depPackagePath = Path.Combine(relDir, originalPath).Replace('\\', '/');
// 规范化路径
depPackagePath = Path.GetFullPath(Path.Combine(baseDestDir, depPackagePath));
depPackagePath = Path.GetRelativePath(baseDestDir, depPackagePath).Replace('\\', '/');
if (depPackagePath.StartsWith("packages/", StringComparison.OrdinalIgnoreCase))
{
packagePath = depPackagePath;
}
}
}
}
if (packagePath != null)
{
var destPath = Path.Combine(baseDestDir, packagePath);
bool isCode = packagePath.EndsWith(".js", StringComparison.OrdinalIgnoreCase);
// 如果文件不存在,下载
if (!File.Exists(destPath))
{
bool downloaded = false;
// 尝试精确下载
if (DownloadAndQueue(repoPath, packagePath, destPath, processingQueue))
{
downloaded = true;
}
// 尝试 .js
else if (isCode || packagePath.IndexOf('.') == -1)
{
// 尝试补充 .js
if (DownloadAndQueue(repoPath, packagePath + ".js", destPath + ".js", processingQueue)) downloaded = true;
}
if (!downloaded)
{
_logger.LogWarning($"依赖未找到: {packagePath} (in {Path.GetFileName(currentFile)})");
}
}
else
{
// 文件已存在
if (isCode)
{
if (!processedFiles.Contains(destPath) && !processingQueue.Contains(destPath)) processingQueue.Enqueue(destPath);
}
}
}
}
}
catch (Exception ex)
{
_logger.LogWarning($"分析文件依赖出错: {currentFile}, {ex.Message}");
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "解析脚本依赖主流程失败");
}
}
private bool DownloadAndQueue(string repoPath, string sourcePath, string destPath, Queue<string> queue)
{
if (CheckoutRepoRootPath(repoPath, sourcePath, destPath))
{
queue.Enqueue(destPath);
return true;
}
return false;
}
/// <summary>
/// 从仓库根目录检出指定路径
/// </summary>
/// <returns>是否成功检出</returns>
private bool CheckoutRepoRootPath(string repoPath, string sourcePath, string destPath)
{
bool isGitRepo = IsGitRepository(repoPath);
if (isGitRepo)
{
using var repo = new Repository(repoPath);
var commit = repo.Head.Tip;
if (commit == null) throw new Exception("仓库HEAD未指向任何提交");
TreeEntry? entry = null;
var pathParts = sourcePath.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
Tree currentTree = commit.Tree; // Start from ROOT tree
for (int i = 0; i < pathParts.Length; i++)
{
entry = currentTree[pathParts[i]];
if (entry == null) return false; // Path not found
if (i < pathParts.Length - 1)
{
if (entry.TargetType != TreeEntryTargetType.Tree)
// 路径中间部分不是目录,说明路径错误
return false;
currentTree = (Tree)entry.Target;
}
}
if (entry == null) return false;
if (entry.TargetType == TreeEntryTargetType.Blob)
{
var blob = (Blob)entry.Target;
var dir = Path.GetDirectoryName(destPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir);
using var contentStream = blob.GetContentStream();
using var fileStream = File.Create(destPath);
contentStream.CopyTo(fileStream);
return true;
}
else if (entry.TargetType == TreeEntryTargetType.Tree)
{
var tree = (Tree)entry.Target;
CheckoutTree(tree, destPath, sourcePath);
return true;
}
return false;
}
else
{
var potentialRoot = Directory.GetParent(repoPath)?.FullName;
if (potentialRoot != null)
{
var scriptPath = Path.Combine(potentialRoot, sourcePath);
if (Directory.Exists(scriptPath))
{
CopyDirectory(scriptPath, destPath);
return true;
}
else if (File.Exists(scriptPath))
{
var dir = Path.GetDirectoryName(destPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir);
File.Copy(scriptPath, destPath, true);
return true;
}
}
return false;
}
}
private static (string firstFolder, string remainingPath) GetFirstFolderAndRemainingPath(string path)
{
// 检查路径是否为空或仅包含部分字符
if (string.IsNullOrEmpty(path))
{
return (string.Empty, string.Empty);
}
// 使用路径分隔符分割路径
string[] parts = path.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar },
StringSplitOptions.RemoveEmptyEntries);
// 返回第一个文件夹和剩余路径
return parts.Length > 0
? (parts[0], string.Join(Path.DirectorySeparatorChar, parts.Skip(1)))
: (string.Empty, string.Empty);
}
public void OpenLocalRepoInWebView()
{
UpdateSubscribedScriptPaths();
if (_webWindow is not { IsVisible: true })
{
var scriptConfig = TaskContext.Instance().Config.ScriptConfig;
// 计算宽高默认0.7屏幕宽高)
double width = scriptConfig.WebviewWidth == 0
? SystemParameters.WorkArea.Width * 0.7
: scriptConfig.WebviewWidth;
double height = scriptConfig.WebviewHeight == 0
? SystemParameters.WorkArea.Height * 0.7
: scriptConfig.WebviewHeight;
// 计算位置(默认居中)
double left = scriptConfig.WebviewLeft == 0
? (SystemParameters.WorkArea.Width - width) / 2
: scriptConfig.WebviewLeft;
double top = scriptConfig.WebviewTop == 0
? (SystemParameters.WorkArea.Height - height) / 2
: scriptConfig.WebviewTop;
WindowState state = scriptConfig.WebviewState;
var screen = SystemParameters.WorkArea;
bool isSmallScreen = screen.Width <= 1600 || screen.Height <= 900;
// 如果未设置或非法值,则默认 Normal小屏则直接最大化
if (isSmallScreen)
{
state = WindowState.Maximized;
}
else if (!Enum.IsDefined(typeof(WindowState), scriptConfig.WebviewState))
{
state = WindowState.Normal;
}
else
{
state = scriptConfig.WebviewState;
}
_webWindow = new WebpageWindow
{
Title = "Genshin Copilot Scripts | BetterGI 脚本本地中央仓库",
Width = width,
Height = height,
Left = left,
Top = top,
WindowStartupLocation = WindowStartupLocation.Manual,
WindowState = state
};
// 关闭时保存窗口位置与大小
_webWindow.Closed += (s, e) =>
{
if (_webWindow != null)
{
scriptConfig.WebviewLeft = _webWindow.Left;
scriptConfig.WebviewTop = _webWindow.Top;
scriptConfig.WebviewWidth = _webWindow.Width;
scriptConfig.WebviewHeight = _webWindow.Height;
scriptConfig.WebviewState = _webWindow.WindowState;
}
_webWindow = null;
};
_webWindow.Panel!.DownloadFolderPath = MapPathingViewModel.PathJsonPath;
// _webWindow.NavigateToFile(Global.Absolute(@"Assets\Web\ScriptRepo\index.html"));
_webWindow.Panel!.OnWebViewInitializedAction = () =>
{
var assetsPath = Global.Absolute(@"Assets\Web\ScriptRepo");
_webWindow.Panel!.WebView.CoreWebView2.SetVirtualHostNameToFolderMapping(
"bettergi.local",
assetsPath,
CoreWebView2HostResourceAccessKind.Allow
);
// _webWindow.Panel!.WebView.CoreWebView2.Navigate("https://bettergi.local/index.html");
_webWindow.Panel!.WebView.CoreWebView2.AddHostObjectToScript("repoWebBridge", new RepoWebBridge());
// 允许内部外链使用默认浏览器打开
_webWindow.Panel!.WebView.CoreWebView2.NewWindowRequested += (sender, e) =>
{
var psi = new ProcessStartInfo
{
UseShellExecute = true,
FileName = e.Uri
};
Process.Start(psi);
e.Handled = true;
};
};
_webWindow.NavigateToUri(new Uri("https://bettergi.local/index.html"));
_webWindow.Show();
}
else
{
_webWindow.Activate();
}
}
public void OpenScriptRepoWindow()
{
var scriptRepoWindow = new ScriptRepoWindow { Owner = Application.Current.MainWindow };
scriptRepoWindow.ShowDialog();
}
/// <summary>
/// 处理带有 icon.ico 和 desktop.ini 的文件夹
/// </summary>
/// <param name="folderPath"></param>
private void DealWithIconFolder(string folderPath)
{
if (Directory.Exists(folderPath)
&& File.Exists(Path.Combine(folderPath, "desktop.ini")))
{
// 使用 Vanara 库中的 SetFileAttributes 函数设置文件夹属性
if (Kernel32.SetFileAttributes(folderPath, FileFlagsAndAttributes.FILE_ATTRIBUTE_READONLY))
{
Debug.WriteLine($"成功将文件夹设置为只读: {folderPath}");
}
else
{
Debug.WriteLine($"无法设置文件夹为只读: {folderPath}");
}
}
}
/// <summary>
/// 根据通配符或正则表达式获取匹配的文件列表
/// </summary>
/// <param name="basePath">基础路径</param>
/// <param name="pattern">通配符模式或正则表达式</param>
/// <returns>匹配的文件路径列表</returns>
private List<string> GetMatchedFiles(string basePath, string pattern)
{
var matchedFiles = new List<string>();
try
{
// 检查是否是正则表达式(以^开头或包含特殊字符)
bool isRegex = pattern.StartsWith("^") || pattern.Contains(".*") || pattern.Contains("\\d") || pattern.Contains("\\w");
if (isRegex)
{
// 使用正则表达式匹配
var regex = new System.Text.RegularExpressions.Regex(pattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
var allFiles = Directory.GetFiles(basePath, "*", SearchOption.AllDirectories);
foreach (var file in allFiles)
{
var relativePath = Path.GetRelativePath(basePath, file);
if (regex.IsMatch(relativePath))
{
matchedFiles.Add(file);
}
}
}
else
{
// 使用通配符匹配
var searchPattern = Path.GetFileName(pattern);
var searchDir = Path.GetDirectoryName(pattern);
if (string.IsNullOrEmpty(searchDir))
{
// 只在当前目录搜索
var files = Directory.GetFiles(basePath, searchPattern);
matchedFiles.AddRange(files);
}
else
{
// 在指定子目录搜索
var searchPath = Path.Combine(basePath, searchDir);
if (Directory.Exists(searchPath))
{
var files = Directory.GetFiles(searchPath, searchPattern);
matchedFiles.AddRange(files);
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"获取匹配文件时发生错误: {pattern}");
}
return matchedFiles;
}
/// <summary>
/// 备份脚本中需要保存的文件到Temp目录
/// </summary>
/// <param name="scriptPath">脚本在中央仓库中的路径</param>
/// <param name="repoPath">中央仓库路径</param>
/// <returns>备份的文件路径列表</returns>
private List<string> BackupScriptFiles(string scriptPath, string repoPath)
{
var backupFiles = new List<string>();
var tempBackupPath = Global.Absolute("User\\Temp");
var scriptPathSafe = scriptPath;
var backupScriptDir = Path.Combine(tempBackupPath, scriptPathSafe);
try
{
if (!Directory.Exists(backupScriptDir))
{
Directory.CreateDirectory(backupScriptDir);
}
// 获取脚本的manifest文件内容
string? manifestContent = null;
// 判断仓库类型
bool isGitRepo = IsGitRepository(repoPath);
if (isGitRepo)
{
// 从Git仓库读取
manifestContent = ReadFileFromGitRepository(repoPath, $"{scriptPath}/manifest.json");
if (manifestContent == null)
{
_logger.LogWarning($"脚本manifest文件不存在: {scriptPath}/manifest.json");
return backupFiles;
}
}
else
{
// 文件式仓库:从文件系统读取
var scriptManifestPath = Path.Combine(repoPath, scriptPath, "manifest.json");
if (!File.Exists(scriptManifestPath))
{
_logger.LogWarning($"脚本manifest文件不存在: {scriptManifestPath}");
return backupFiles;
}
manifestContent = File.ReadAllText(scriptManifestPath);
}
// 解析manifest文件获取savedFiles
var manifest = Manifest.FromJson(manifestContent);
if (manifest.SavedFiles == null || manifest.SavedFiles.Length == 0)
{
_logger.LogInformation($"脚本 {scriptPath} 没有需要保存的文件");
return backupFiles;
}
// 获取脚本在用户目录中的路径
var (first, remainingPath) = GetFirstFolderAndRemainingPath(scriptPath);
if (!PathMapper.TryGetValue(first, out var userPath))
{
_logger.LogWarning($"未知的脚本路径映射: {scriptPath}");
return backupFiles;
}
var scriptUserPath = Path.Combine(userPath, remainingPath);
// 备份每个需要保存的文件
foreach (var savedFileRaw in manifest.SavedFiles)
{
// 自动补全所有目录路径为以/结尾
var savedFile = savedFileRaw;
var fullPath = Path.Combine(scriptUserPath, savedFile.TrimEnd('/', '\\'));
bool isDir = savedFile.EndsWith("/") || Directory.Exists(fullPath);
if (!savedFile.EndsWith("/") && Directory.Exists(fullPath))
{
savedFile += "/";
isDir = true;
}
if (isDir)
{
var dirPath = Path.Combine(scriptUserPath, savedFile.TrimEnd('/', '\\'));
if (Directory.Exists(dirPath))
{
var destDir = Path.Combine(backupScriptDir, savedFile.TrimEnd('/', '\\'));
CopyDirectory(dirPath, destDir);
backupFiles.Add(destDir);
}
else
{
_logger.LogWarning($"需要备份的文件夹不存在: {dirPath}");
}
}
else
{
var matchedFiles = GetMatchedFiles(scriptUserPath, savedFile);
foreach (var matchedFile in matchedFiles)
{
var relativePath = Path.GetRelativePath(scriptUserPath, matchedFile);
var backupFilePath = Path.Combine(backupScriptDir, relativePath);
var backupFileDir = Path.GetDirectoryName(backupFilePath);
if (!string.IsNullOrEmpty(backupFileDir) && !Directory.Exists(backupFileDir))
{
Directory.CreateDirectory(backupFileDir);
}
try
{
File.Copy(matchedFile, backupFilePath, true);
backupFiles.Add(backupFilePath);
}
catch (Exception ex)
{
_logger.LogError(ex, $"备份文件失败: {matchedFile}");
}
}
if (matchedFiles.Count == 0)
{
_logger.LogWarning($"没有找到匹配的文件: {savedFile}");
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"备份脚本文件时发生错误: {scriptPath}");
}
return backupFiles;
}
/// <summary>
/// 恢复备份的文件到指定位置并清理Temp目录
/// </summary>
/// <param name="scriptPath">脚本在中央仓库中的路径</param>
/// <param name="repoPath">中央仓库路径</param>
private void RestoreScriptFiles(string scriptPath, string repoPath)
{
var tempBackupPath = Global.Absolute("User\\Temp");
var scriptPathSafe = scriptPath;
var backupScriptDir = Path.Combine(tempBackupPath, scriptPathSafe);
try
{
// 获取脚本的manifest文件内容
string? manifestContent = null;
// 判断仓库类型
bool isGitRepo = IsGitRepository(repoPath);
if (isGitRepo)
{
// 从Git仓库读取
manifestContent = ReadFileFromGitRepository(repoPath, $"{scriptPath}/manifest.json");
if (manifestContent == null)
{
_logger.LogWarning($"脚本manifest文件不存在: {scriptPath}/manifest.json");
return;
}
}
else
{
// 文件式仓库:从文件系统读取
var scriptManifestPath = Path.Combine(repoPath, scriptPath, "manifest.json");
if (!File.Exists(scriptManifestPath))
{
_logger.LogWarning($"脚本manifest文件不存在: {scriptManifestPath}");
return;
}
manifestContent = File.ReadAllText(scriptManifestPath);
}
// 解析manifest文件获取savedFiles
var manifest = Manifest.FromJson(manifestContent);
if (manifest.SavedFiles == null || manifest.SavedFiles.Length == 0)
{
_logger.LogInformation($"脚本 {scriptPath} 没有需要恢复的文件");
return;
}
// 获取脚本在用户目录中的路径
var (first, remainingPath) = GetFirstFolderAndRemainingPath(scriptPath);
if (!PathMapper.TryGetValue(first, out var userPath))
{
_logger.LogWarning($"未知的脚本路径映射: {scriptPath}");
return;
}
var scriptUserPath = Path.Combine(userPath, remainingPath);
// 还原所有备份文件
if (Directory.Exists(backupScriptDir))
{
foreach (var file in Directory.GetFiles(backupScriptDir, "*", SearchOption.AllDirectories))
{
var relativePath = Path.GetRelativePath(backupScriptDir, file);
var restorePath = Path.Combine(scriptUserPath, relativePath);
var restoreDir = Path.GetDirectoryName(restorePath);
if (!string.IsNullOrEmpty(restoreDir) && !Directory.Exists(restoreDir))
{
Directory.CreateDirectory(restoreDir);
}
try
{
File.Copy(file, restorePath, true);
}
catch (Exception ex)
{
_logger.LogError(ex, $"恢复文件失败: {file} -> {restorePath}");
}
}
}
else
{
_logger.LogWarning($"备份目录不存在: {backupScriptDir}");
}
// 清理Temp目录下该脚本的备份
try
{
if (Directory.Exists(backupScriptDir))
{
Directory.Delete(backupScriptDir, true);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "清理Temp脚本备份目录失败");
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"恢复脚本文件时发生错误: {scriptPath}");
}
}
}