From 8e86ccc560b0859b8da88693dc9a567e00fdb69b Mon Sep 17 00:00:00 2001
From: fanbook-wangdage <124357765+fanbook-wangdage@users.noreply.github.com>
Date: Sat, 28 Feb 2026 15:19:16 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=AC=AC=E4=B8=89=E6=96=B9?=
=?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=8A=9F=E8=83=BD=EF=BC=88#25=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 10 +-
.../Snap.Hutao/Resource/Localization/SH.resx | 3 +
.../ThirdPartyTool/IThirdPartyToolService.cs | 14 +
.../ThirdPartyTool/ThirdPartyToolService.cs | 300 ++++++++++++++++--
.../View/Dialog/ThirdPartyToolDialog.xaml | 12 +-
.../View/Dialog/ThirdPartyToolDialog.xaml.cs | 26 +-
.../Web/ThirdPartyTool/LocalToolInfo.cs | 54 ++++
.../Snap.Hutao/Web/ThirdPartyTool/ToolInfo.cs | 9 +
8 files changed, 382 insertions(+), 46 deletions(-)
create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/ThirdPartyTool/LocalToolInfo.cs
diff --git a/README.md b/README.md
index 74f7e18..0b2f642 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
**目前元数据的编写进度:**
-| 项目(V6.3) | 是否完成 |
+| 项目(V6.4) | 是否完成 |
| ----------- | ----------- |
| 总体数据 | ✔️ |
@@ -67,6 +67,14 @@ https://deepwiki.com/DGP-Studio/Snap.Hutao.Server
- 服务端:[Snap.Server](https://github.com/wangdage12/Snap.Server)
- Web管理后台和官网:[Snap.Server.Web](https://github.com/wangdage12/Snap.Server.Web)
+**第三方工具**
+
+如果你想要添加你自己开发的工具到第三方工具列表中,请确保:
+1. 工具应该提供源码或者开源,并且可以成功编译
+2. 工具不应提供任何可能影响游戏公平性的功能
+
+工具不限于注入功能,若满足以上条件,请提 issue,或者在 discord 服务器中联系管理员
+
## 打包测试
由于采用了 wix 进行打包程序,VS 需要安装 **HeatWave for VS2022**(2026兼容)。需要 msi 安装包时,右键选中 Snap.Hutao.Installer 生成后即可在目标目录找到。默认目录:Snap.Hutao.Installer\bin\x64\Release\en-US\Snap.Hutao.Installer.msi
diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
index 9cbd417..fc26109 100644
--- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
+++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
@@ -1598,6 +1598,9 @@
启动
+
+ 版本:
+
使用米游社扫描二维码
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/ThirdPartyTool/IThirdPartyToolService.cs b/src/Snap.Hutao/Snap.Hutao/Service/ThirdPartyTool/IThirdPartyToolService.cs
index e00b25e..690e0df 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/ThirdPartyTool/IThirdPartyToolService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/ThirdPartyTool/IThirdPartyToolService.cs
@@ -34,4 +34,18 @@ internal interface IThirdPartyToolService
/// 工具信息
/// 是否已下载
bool IsToolDownloaded(ToolInfo tool);
+
+ ///
+ /// 获取本地工具信息
+ ///
+ /// 工具信息
+ /// 本地工具信息,如果不存在则返回null
+ LocalToolInfo? GetLocalToolInfo(ToolInfo tool);
+
+ ///
+ /// 检查工具是否需要更新
+ ///
+ /// 工具信息
+ /// 是否需要更新
+ bool NeedsUpdate(ToolInfo tool);
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/ThirdPartyTool/ThirdPartyToolService.cs b/src/Snap.Hutao/Snap.Hutao/Service/ThirdPartyTool/ThirdPartyToolService.cs
index 89d48be..b359369 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/ThirdPartyTool/ThirdPartyToolService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/ThirdPartyTool/ThirdPartyToolService.cs
@@ -9,7 +9,9 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
+using System.IO.Compression;
using System.Net.Http;
+using System.Text.Json;
namespace Snap.Hutao.Service.ThirdPartyTool;
@@ -19,6 +21,7 @@ internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
{
private const string ApiBaseUrl = "https://htserver.wdg.cloudns.ch/api";
private const string ToolsEndpoint = "/tools";
+ private const string ToolInfoFileName = "tool_info.json";
private readonly IHttpClientFactory httpClientFactory;
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
@@ -32,10 +35,10 @@ internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
try
{
HttpClient httpClient = httpClientFactory.CreateClient();
-
+
// 添加日志
SentrySdk.AddBreadcrumb($"Creating request to: {ApiBaseUrl}{ToolsEndpoint}", category: "ThirdPartyTool");
-
+
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri($"{ApiBaseUrl}{ToolsEndpoint}")
.Get();
@@ -89,41 +92,32 @@ internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
try
{
string toolDirectory = GetToolDirectory(tool);
- Directory.CreateDirectory(toolDirectory);
- int totalFiles = tool.Files.Count;
- int downloadedFiles = 0;
+ // 如果需要更新,先清理旧文件
+ if (NeedsUpdate(tool) && Directory.Exists(toolDirectory))
+ {
+ Directory.Delete(toolDirectory, true);
+ }
+
+ Directory.CreateDirectory(toolDirectory);
using (HttpClient httpClient = httpClientFactory.CreateClient())
{
- foreach (string fileName in tool.Files)
+ if (tool.IsCompressed)
{
- string fileUrl = $"{tool.Url}{fileName}";
- string localFilePath = Path.Combine(toolDirectory, fileName);
-
- // 如果文件已存在,跳过下载
- if (File.Exists(localFilePath))
- {
- downloadedFiles++;
- progress?.Report((double)downloadedFiles / totalFiles * 100);
- continue;
- }
-
- // 下载文件
- HttpResponseMessage response = await httpClient.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
-
- using (Stream contentStream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
- using (FileStream fileStream = new(localFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
- {
- await contentStream.CopyToAsync(fileStream, token).ConfigureAwait(false);
- }
-
- downloadedFiles++;
- progress?.Report((double)downloadedFiles / totalFiles * 100);
+ // 压缩包模式:下载并解压
+ await DownloadAndExtractCompressedToolAsync(httpClient, tool, toolDirectory, progress, token).ConfigureAwait(false);
+ }
+ else
+ {
+ // 非压缩包模式:直接下载所有文件
+ await DownloadFilesAsync(httpClient, tool, toolDirectory, progress, token).ConfigureAwait(false);
}
}
+ // 保存本地工具信息
+ SaveLocalToolInfo(tool);
+
return true;
}
catch (Exception ex)
@@ -139,15 +133,31 @@ internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
{
string toolDirectory = GetToolDirectory(tool);
- // 查找可执行文件(.exe)
- string? executablePath = tool.Files.FirstOrDefault(f => f.EndsWith(".exe", StringComparison.OrdinalIgnoreCase));
+ // 优先使用 main_exe,如果没有则查找可执行文件
+ string? executablePath = tool.MainExe;
+ if (string.IsNullOrEmpty(executablePath))
+ {
+ executablePath = tool.Files.FirstOrDefault(f => f.EndsWith(".exe", StringComparison.OrdinalIgnoreCase));
+ }
+
+ // 如果还是没有,尝试从目录中查找
+ if (string.IsNullOrEmpty(executablePath))
+ {
+ string[] exeFiles = Directory.GetFiles(toolDirectory, "*.exe", SearchOption.TopDirectoryOnly);
+ executablePath = exeFiles.FirstOrDefault();
+ }
+
if (string.IsNullOrEmpty(executablePath))
{
messenger.Send(InfoBarMessage.Warning(SH.ServiceThirdPartyToolNoExecutableFound));
return false;
}
- string fullPath = Path.Combine(toolDirectory, executablePath);
+ // 如果 executablePath 是完整路径,直接使用;否则拼接目录
+ string fullPath = Path.IsPathRooted(executablePath)
+ ? executablePath
+ : Path.Combine(toolDirectory, Path.GetFileName(executablePath));
+
if (!File.Exists(fullPath))
{
messenger.Send(InfoBarMessage.Warning(SH.FormatServiceThirdPartyToolFileNotFound(fullPath)));
@@ -192,7 +202,20 @@ internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
return false;
}
- // 检查所有文件是否存在
+ // 检查工具信息文件是否存在
+ LocalToolInfo? localInfo = GetLocalToolInfo(tool);
+ if (localInfo is null)
+ {
+ return false;
+ }
+
+ // 对于压缩包,检查目录是否有内容
+ if (tool.IsCompressed)
+ {
+ return Directory.GetFiles(toolDirectory, "*", SearchOption.AllDirectories).Length > 0;
+ }
+
+ // 对于非压缩包,检查所有文件是否存在
foreach (string fileName in tool.Files)
{
string filePath = Path.Combine(toolDirectory, fileName);
@@ -205,6 +228,217 @@ internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
return true;
}
+ public LocalToolInfo? GetLocalToolInfo(ToolInfo tool)
+ {
+ string toolDirectory = GetToolDirectory(tool);
+ string infoFilePath = Path.Combine(toolDirectory, ToolInfoFileName);
+
+ if (!File.Exists(infoFilePath))
+ {
+ return null;
+ }
+
+ try
+ {
+ string json = File.ReadAllText(infoFilePath);
+ return JsonSerializer.Deserialize(json);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public bool NeedsUpdate(ToolInfo tool)
+ {
+ LocalToolInfo? localInfo = GetLocalToolInfo(tool);
+ if (localInfo is null)
+ {
+ return true; // 没有本地信息,需要下载
+ }
+
+ // 比较版本号
+ return IsNewerVersion(tool.Version, localInfo.Version);
+ }
+
+ private static async Task DownloadAndExtractCompressedToolAsync(
+ HttpClient httpClient,
+ ToolInfo tool,
+ string toolDirectory,
+ IProgress? progress,
+ CancellationToken token)
+ {
+ // 压缩包模式通常只有一个 zip 文件
+ string zipFileName = tool.Files.FirstOrDefault(f => f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
+ ?? tool.Files[0];
+
+ string zipUrl = $"{tool.Url}{zipFileName}";
+ string zipFilePath = Path.Combine(toolDirectory, zipFileName);
+
+ // 下载 zip 文件
+ progress?.Report(0);
+
+ HttpResponseMessage response = await httpClient.GetAsync(zipUrl, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
+ using (Stream contentStream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
+ using (FileStream fileStream = new(zipFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
+ {
+ await contentStream.CopyToAsync(fileStream, token).ConfigureAwait(false);
+ }
+
+ progress?.Report(50);
+
+ // 解压 zip 文件
+ using (ZipArchive archive = ZipFile.OpenRead(zipFilePath))
+ {
+ // 检查是否有根目录需要处理
+ bool hasRootFolder = HasSingleRootFolder(archive);
+
+ foreach (ZipArchiveEntry entry in archive.Entries)
+ {
+ if (string.IsNullOrEmpty(entry.Name))
+ {
+ // 这是一个目录,创建它
+ string? destinationPath = GetDestinationPath(entry.FullName, toolDirectory, hasRootFolder);
+ if (destinationPath is not null)
+ {
+ Directory.CreateDirectory(destinationPath);
+ }
+ continue;
+ }
+
+ string? destFilePath = GetDestinationPath(entry.FullName, toolDirectory, hasRootFolder);
+ if (destFilePath is null)
+ {
+ continue;
+ }
+
+ // 确保目录存在
+ string? destDir = Path.GetDirectoryName(destFilePath);
+ if (!string.IsNullOrEmpty(destDir))
+ {
+ Directory.CreateDirectory(destDir);
+ }
+
+ entry.ExtractToFile(destFilePath, true);
+ }
+ }
+
+ progress?.Report(90);
+
+ // 删除 zip 文件
+ File.Delete(zipFilePath);
+
+ progress?.Report(100);
+ }
+
+ private static async Task DownloadFilesAsync(
+ HttpClient httpClient,
+ ToolInfo tool,
+ string toolDirectory,
+ IProgress? progress,
+ CancellationToken token)
+ {
+ int totalFiles = tool.Files.Count;
+ int downloadedFiles = 0;
+
+ foreach (string fileName in tool.Files)
+ {
+ string fileUrl = $"{tool.Url}{fileName}";
+ string localFilePath = Path.Combine(toolDirectory, fileName);
+
+ // 如果文件已存在,跳过下载
+ if (File.Exists(localFilePath))
+ {
+ downloadedFiles++;
+ progress?.Report((double)downloadedFiles / totalFiles * 100);
+ continue;
+ }
+
+ // 下载文件
+ HttpResponseMessage response = await httpClient.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
+ using (Stream contentStream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
+ using (FileStream fileStream = new(localFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
+ {
+ await contentStream.CopyToAsync(fileStream, token).ConfigureAwait(false);
+ }
+
+ downloadedFiles++;
+ progress?.Report((double)downloadedFiles / totalFiles * 100);
+ }
+ }
+
+ private void SaveLocalToolInfo(ToolInfo tool)
+ {
+ string toolDirectory = GetToolDirectory(tool);
+ string infoFilePath = Path.Combine(toolDirectory, ToolInfoFileName);
+
+ LocalToolInfo localInfo = LocalToolInfo.FromToolInfo(tool);
+ string json = JsonSerializer.Serialize(localInfo, new JsonSerializerOptions { WriteIndented = true });
+ File.WriteAllText(infoFilePath, json);
+ }
+
+ private static bool IsNewerVersion(string remoteVersion, string localVersion)
+ {
+ // 使用 Version 类进行比较
+ if (Version.TryParse(remoteVersion, out Version? remote) && Version.TryParse(localVersion, out Version? local))
+ {
+ return remote > local;
+ }
+
+ // 如果无法解析为版本号,进行字符串比较
+ return !string.Equals(remoteVersion, localVersion, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool HasSingleRootFolder(ZipArchive archive)
+ {
+ // 检查是否所有条目都在同一个根目录下
+ HashSet rootFolders = [];
+ foreach (ZipArchiveEntry entry in archive.Entries)
+ {
+ int separatorIndex = entry.FullName.IndexOf('/');
+ if (separatorIndex > 0)
+ {
+ rootFolders.Add(entry.FullName[..separatorIndex]);
+ }
+ else if (separatorIndex < 0 && !string.IsNullOrEmpty(entry.Name))
+ {
+ // 直接在根目录下的文件
+ return false;
+ }
+ }
+
+ return rootFolders.Count == 1;
+ }
+
+ private static string? GetDestinationPath(string entryPath, string toolDirectory, bool hasRootFolder)
+ {
+ if (string.IsNullOrEmpty(entryPath))
+ {
+ return null;
+ }
+
+ if (hasRootFolder)
+ {
+ // 移除根目录前缀
+ int separatorIndex = entryPath.IndexOf('/');
+ if (separatorIndex >= 0 && separatorIndex < entryPath.Length - 1)
+ {
+ return Path.Combine(toolDirectory, entryPath[(separatorIndex + 1)..]);
+ }
+ else if (separatorIndex >= 0)
+ {
+ // 这是根目录本身
+ return null;
+ }
+ }
+
+ return Path.Combine(toolDirectory, entryPath);
+ }
+
private static string GetToolDirectory(ToolInfo tool)
{
// 使用数据目录/工具名作为存储路径
diff --git a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Dialog/ThirdPartyToolDialog.xaml b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Dialog/ThirdPartyToolDialog.xaml
index 33b7504..b7ec2e1 100644
--- a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Dialog/ThirdPartyToolDialog.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Dialog/ThirdPartyToolDialog.xaml
@@ -17,6 +17,7 @@
+
+
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Dialog/ThirdPartyToolDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Dialog/ThirdPartyToolDialog.xaml.cs
index 27c823a..6e621ac 100644
--- a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Dialog/ThirdPartyToolDialog.xaml.cs
+++ b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Dialog/ThirdPartyToolDialog.xaml.cs
@@ -34,13 +34,20 @@ internal sealed partial class ThirdPartyToolDialog : ContentDialog
{
// 在 UI 线程上获取 Tool 的引用,避免后续跨线程访问依赖属性
ToolInfo? tool = Tool;
-
+
try
{
IsDownloading = true;
- // 检查工具是否已下载
- if (tool is not null && !thirdPartyToolService.IsToolDownloaded(tool))
+ if (tool is null)
+ {
+ return;
+ }
+
+ // 检查工具是否需要下载或更新
+ bool needDownload = !thirdPartyToolService.IsToolDownloaded(tool) || thirdPartyToolService.NeedsUpdate(tool);
+
+ if (needDownload)
{
// 下载工具
bool downloadSuccess = await thirdPartyToolService.DownloadToolAsync(tool, null).ConfigureAwait(false);
@@ -53,15 +60,12 @@ internal sealed partial class ThirdPartyToolDialog : ContentDialog
}
// 启动工具
- if (tool is not null)
+ bool launchSuccess = await thirdPartyToolService.LaunchToolAsync(tool).ConfigureAwait(false);
+ if (launchSuccess)
{
- bool launchSuccess = await thirdPartyToolService.LaunchToolAsync(tool).ConfigureAwait(false);
- if (launchSuccess)
- {
- await contentDialogFactory.TaskContext.SwitchToMainThreadAsync();
- Hide();
- return;
- }
+ await contentDialogFactory.TaskContext.SwitchToMainThreadAsync();
+ Hide();
+ return;
}
}
catch (Exception ex)
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ThirdPartyTool/LocalToolInfo.cs b/src/Snap.Hutao/Snap.Hutao/Web/ThirdPartyTool/LocalToolInfo.cs
new file mode 100644
index 0000000..4a4adf4
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/ThirdPartyTool/LocalToolInfo.cs
@@ -0,0 +1,54 @@
+using System.Text.Json.Serialization;
+
+namespace Snap.Hutao.Web.ThirdPartyTool;
+
+///
+/// 本地工具信息,用于保存工具的本地状态
+///
+internal sealed class LocalToolInfo
+{
+ ///
+ /// 工具名称
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = default!;
+
+ ///
+ /// 工具版本号
+ ///
+ [JsonPropertyName("version")]
+ public string Version { get; set; } = default!;
+
+ ///
+ /// 是否为压缩包
+ ///
+ [JsonPropertyName("is_compressed")]
+ public bool IsCompressed { get; set; }
+
+ ///
+ /// 主可执行文件名
+ ///
+ [JsonPropertyName("main_exe")]
+ public string? MainExe { get; set; }
+
+ ///
+ /// 下载时间
+ ///
+ [JsonPropertyName("download_time")]
+ public DateTimeOffset DownloadTime { get; set; }
+
+ ///
+ /// 从 ToolInfo 创建 LocalToolInfo
+ ///
+ public static LocalToolInfo FromToolInfo(ToolInfo toolInfo)
+ {
+ return new LocalToolInfo
+ {
+ Name = toolInfo.Name,
+ Version = toolInfo.Version,
+ IsCompressed = toolInfo.IsCompressed,
+ MainExe = toolInfo.MainExe,
+ DownloadTime = DateTimeOffset.Now,
+ };
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ThirdPartyTool/ToolInfo.cs b/src/Snap.Hutao/Snap.Hutao/Web/ThirdPartyTool/ToolInfo.cs
index 06447f6..b557bee 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/ThirdPartyTool/ToolInfo.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/ThirdPartyTool/ToolInfo.cs
@@ -16,4 +16,13 @@ internal sealed class ToolInfo
[JsonPropertyName("files")]
public List Files { get; set; } = default!;
+
+ [JsonPropertyName("is_compressed")]
+ public bool IsCompressed { get; set; }
+
+ [JsonPropertyName("main_exe")]
+ public string? MainExe { get; set; }
+
+ [JsonPropertyName("version")]
+ public string Version { get; set; } = default!;
}
\ No newline at end of file