Files
MaaAssistantArknights/src/MaaWpfGui/ViewModels/UserControl/Settings/IssueReportUserControlModel.cs
2026-02-10 00:54:40 +08:00

325 lines
11 KiB
C#
Raw 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.
// <copyright file="IssueReportUserControlModel.cs" company="MaaAssistantArknights">
// Part of the MaaWpfGui project, maintained by the MaaAssistantArknights team (Maa Team)
// Copyright (C) 2021-2025 MaaAssistantArknights Contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License v3.0 only as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using HandyControl.Controls;
using HandyControl.Data;
using JetBrains.Annotations;
using MaaWpfGui.Constants;
using MaaWpfGui.Helper;
using Serilog;
using Stylet;
namespace MaaWpfGui.ViewModels.UserControl.Settings;
/// <summary>
/// 问题反馈
/// </summary>
public class IssueReportUserControlModel : PropertyChangedBase
{
static IssueReportUserControlModel()
{
Instance = new();
}
public static IssueReportUserControlModel Instance { get; }
public void OpenDebugFolder()
{
try
{
if (!Directory.Exists(PathsHelper.DebugDir))
{
Directory.CreateDirectory(PathsHelper.DebugDir);
}
Process.Start("explorer.exe", PathsHelper.DebugDir);
}
catch (Exception ex)
{
ToastNotification.ShowDirect($"Failed to open debug folder\n{ex.Message}");
Log.Error(ex, "Failed to open debug folder");
}
}
public void OpenReportsFolder()
{
try
{
if (!Directory.Exists(PathsHelper.ReportsDir))
{
Directory.CreateDirectory(PathsHelper.ReportsDir);
}
Process.Start("explorer.exe", PathsHelper.ReportsDir);
}
catch (Exception ex)
{
ToastNotification.ShowDirect($"Failed to open reports folder\n{ex.Message}");
Log.Error(ex, "Failed to open reports folder");
}
}
/// <summary>
/// 清空缓存 包括 cache 目录和 debug 目录
/// </summary>
public static void ClearCache()
{
try
{
var clearedDirs = new List<string>();
if (Directory.Exists(PathsHelper.CacheDir))
{
Directory.Delete(PathsHelper.CacheDir, recursive: true);
clearedDirs.Add("cache");
}
if (Directory.Exists(PathsHelper.DebugDir))
{
DeleteDirectoryContentsExcept(PathsHelper.DebugDir, ["asst.log", "gui.log"]);
clearedDirs.Add("debug");
}
if (clearedDirs.Count > 0)
{
ShowGrowl(LocalizationHelper.GetString("ClearCacheSuccessful"));
}
else
{
ShowGrowl(LocalizationHelper.GetString("ClearCacheAlreadyEmpty"));
}
}
catch (Exception ex)
{
ShowGrowl($"{LocalizationHelper.GetString("ClearCacheException")}\n{ex.Message}");
Log.Error(ex, "Failed to clear cache");
}
}
/// <summary>
/// 生成日志压缩包
/// </summary>
public void GenerateSupportPayload()
{
try
{
const int PartSize = 20 * 1024 * 1024; // 20 MB
string reportNameBase = $"report_{DateTimeOffset.Now:MM-dd_HH-mm-ss}";
string tempPath = Path.Combine(Path.GetTempPath(), $"maa-report-{Guid.NewGuid()}");
Directory.CreateDirectory(tempPath);
if (!Directory.Exists(PathsHelper.ReportsDir))
{
Directory.CreateDirectory(PathsHelper.ReportsDir);
}
// 复制文件
CopyDirectoryIfExists(PathsHelper.DebugDir, Path.Combine(tempPath, "debug"),
f => { return !Path.GetFileName(f).StartsWith("report", StringComparison.OrdinalIgnoreCase); });
CopyDirectoryIfExists(PathsHelper.ResourceDir, Path.Combine(tempPath, "resource"),
f => { return Path.GetFileName(f).Contains("_custom.", StringComparison.OrdinalIgnoreCase); });
CopyDirectoryIfExists(PathsHelper.ConfigDir, Path.Combine(tempPath, "config"));
CopyDirectoryIfExists(PathsHelper.CacheDir, Path.Combine(tempPath, "cache"));
string partsFolder = Path.Combine(PathsHelper.ReportsDir, $"{reportNameBase}_parts");
if (!Directory.Exists(partsFolder))
{
Directory.CreateDirectory(partsFolder);
}
// ====== part01config + resource + cache + debug 根目录文件 ======
List<string> part01Files = [];
string[] categories = ["config", "resource", "cache"];
foreach (string category in categories)
{
string categoryPath = Path.Combine(tempPath, category);
if (Directory.Exists(categoryPath))
{
part01Files.AddRange(Directory.EnumerateFiles(categoryPath, "*", SearchOption.AllDirectories));
}
}
string debugPath = Path.Combine(tempPath, "debug");
if (Directory.Exists(debugPath))
{
// 只取 debug 根目录文件
var debugRootFiles = Directory.EnumerateFiles(debugPath, "*", SearchOption.TopDirectoryOnly).ToList();
part01Files.AddRange(debugRootFiles);
}
if (part01Files.Count > 0)
{
string part01Path = Path.Combine(partsFolder, $"{reportNameBase}_part01.zip");
using var fs = new FileStream(part01Path, FileMode.Create);
using var archive = new ZipArchive(fs, ZipArchiveMode.Create);
foreach (var file in part01Files)
{
string entryName = Path.GetRelativePath(tempPath, file).Replace("\\", "/");
var entry = archive.CreateEntry(entryName, CompressionLevel.SmallestSize);
using var entryStream = entry.Open();
using var fileStream = File.OpenRead(file);
fileStream.CopyTo(entryStream);
}
}
// ====== part02debug 子目录文件按 PartSize 分卷 ======
var threeDaysAgo = DateTime.Now.AddDays(-3);
var debugSubFiles = Directory.EnumerateFiles(debugPath, "*", SearchOption.AllDirectories)
.Where(f => Path.GetDirectoryName(f) != debugPath)
.Where(f => new FileInfo(f).LastWriteTime >= threeDaysAgo)
.ToList();
int partNumber = 2;
while (debugSubFiles.Count > 0)
{
string partFileName = $"{reportNameBase}_part{partNumber:D2}.zip";
string partPath = Path.Combine(partsFolder, partFileName);
using (var fs = new FileStream(partPath, FileMode.Create))
{
using var archive = new ZipArchive(fs, ZipArchiveMode.Create);
long currentSize = 0;
List<string> processedFiles = [];
foreach (var file in debugSubFiles.ToList())
{
var fileInfo = new FileInfo(file);
if (currentSize + fileInfo.Length > PartSize && currentSize > 0)
{
break;
}
string entryName = Path.GetRelativePath(tempPath, file).Replace("\\", "/");
var entry = archive.CreateEntry(entryName, CompressionLevel.SmallestSize);
using (var entryStream = entry.Open())
{
using var fileStream = File.OpenRead(file);
fileStream.CopyTo(entryStream);
}
currentSize += fileInfo.Length;
processedFiles.Add(file);
}
debugSubFiles.RemoveAll(f => processedFiles.Contains(f));
}
partNumber++;
}
// ====== 生成完整压缩包 ======
string fullZipPath = Path.Combine(PathsHelper.ReportsDir, $"{reportNameBase}.zip");
ZipFile.CreateFromDirectory(tempPath, fullZipPath, CompressionLevel.SmallestSize, includeBaseDirectory: false);
// 清理临时目录
Directory.Delete(tempPath, recursive: true);
ShowGrowl($"{LocalizationHelper.GetString("GenerateSupportPayloadSuccessful")}\n{fullZipPath}");
OpenReportsFolder();
}
catch (Exception ex)
{
ShowGrowl($"{LocalizationHelper.GetString("GenerateSupportPayloadException")}\n{ex.Message}");
Log.Error(ex, "Failed to create support payload");
}
}
/// <summary>
/// 删除目录下的所有文件和子目录,排除指定的文件名。
/// </summary>
private static void DeleteDirectoryContentsExcept(string dir, IEnumerable<string> excludeFileNames)
{
var excludeSet = excludeFileNames.ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var file in Directory.EnumerateFiles(dir))
{
if (excludeSet.Contains(Path.GetFileName(file)))
{
continue;
}
File.Delete(file);
}
foreach (var subDir in Directory.EnumerateDirectories(dir))
{
Directory.Delete(subDir, recursive: true);
}
}
/// <summary>
/// 从 sourceDir 复制文件到 targetDir支持过滤。
/// </summary>
private static void CopyDirectoryIfExists(string? sourceDir, string targetDir, Func<string, bool>? filter = null)
{
if (string.IsNullOrWhiteSpace(sourceDir) || !Directory.Exists(sourceDir))
{
return;
}
foreach (var file in Directory.EnumerateFiles(sourceDir, "*", SearchOption.AllDirectories))
{
if (filter != null && !filter(file))
{
continue;
}
string relative = Path.GetRelativePath(sourceDir, file);
string dest = Path.Combine(targetDir, relative);
Directory.CreateDirectory(Path.GetDirectoryName(dest)!);
try
{
File.Copy(file, dest, true);
}
catch (IOException)
{
// 某些文件可能被占用,忽略复制失败
}
catch (UnauthorizedAccessException)
{
// 也忽略权限问题
}
}
}
// UI 绑定的方法
[UsedImplicitly]
public void SetAcknowledgedNightlyWarning()
{
// 其实不应该放这里,但懒得写一个新的方法,就塞到这里了
AchievementTrackerHelper.Instance.Unlock(AchievementIds.ProblemFeedback);
VersionUpdateSettingsUserControlModel.Instance.HasAcknowledgedNightlyWarning = true;
}
private static void ShowGrowl(string message)
{
var growlInfo = new GrowlInfo {
IsCustom = true,
Message = message,
IconKey = "HangoverGeometry",
IconBrushKey = "PallasBrush",
};
Growl.Info(growlInfo);
}
}