// // 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 // #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; /// /// 问题反馈 /// 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"); } } /// /// 清空缓存 包括 cache 目录和 debug 目录 /// public static void ClearCache() { try { var clearedDirs = new List(); 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"); } } /// /// 生成日志压缩包 /// 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); } // ====== part01:config + resource + cache + debug 根目录文件 ====== List 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); } } // ====== part02:debug 子目录文件按 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 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"); } } /// /// 删除目录下的所有文件和子目录,排除指定的文件名。 /// private static void DeleteDirectoryContentsExcept(string dir, IEnumerable 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); } } /// /// 从 sourceDir 复制文件到 targetDir,支持过滤。 /// private static void CopyDirectoryIfExists(string? sourceDir, string targetDir, Func? 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); } }