Skip to content

Commit 7929025

Browse files
committed
feat(beta): add diagnostic exporter and GitHub issue template for feedback loop
1 parent cff2bad commit 7929025

6 files changed

Lines changed: 309 additions & 1 deletion

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Bug Report
2+
description: 提交一个缺陷报告,帮助我们快速定位并修复问题
3+
title: "[Bug]: "
4+
labels:
5+
- bug
6+
body:
7+
- type: checkboxes
8+
id: precheck
9+
attributes:
10+
label: 提交前确认
11+
description: 请在提交前完成以下自检项
12+
options:
13+
- label: 我已复现该问题。
14+
required: true
15+
- label: 我已使用“一键导出诊断包”功能获取了 `.zip` 诊断文件。
16+
required: true
17+
18+
- type: dropdown
19+
id: module
20+
attributes:
21+
label: 发生问题的模块
22+
description: 请选择问题发生的功能区域
23+
options:
24+
- WeChat
25+
- QQ
26+
- Chrome
27+
- 系统清理
28+
- UI界面
29+
- 其他
30+
validations:
31+
required: true
32+
33+
- type: textarea
34+
id: steps
35+
attributes:
36+
label: 复现步骤 (Steps to Reproduce)
37+
description: 请按顺序描述操作步骤,便于稳定复现
38+
placeholder: |
39+
1. 打开 ...
40+
2. 点击 ...
41+
3. 观察到 ...
42+
validations:
43+
required: true
44+
45+
- type: markdown
46+
attributes:
47+
value: "> ⚠️ 请务必将导出的诊断包 (.zip) 拖拽上传至下方区域,否则我们将无法排查您的问题!"
48+
49+
- type: textarea
50+
id: additional
51+
attributes:
52+
label: 其他补充信息
53+
description: 可附上截图、日志片段、系统版本等信息
54+
placeholder: 可将截图拖拽到此处并填写补充说明
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
using System;
2+
using System.IO;
3+
using System.IO.Compression;
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
7+
namespace CDriveMaster.Core.Services;
8+
9+
public sealed class DiagnosticExporter
10+
{
11+
private readonly Func<string> outputDirectoryProvider;
12+
private readonly Func<string> appBaseDirectoryProvider;
13+
private readonly Func<DateTime> nowProvider;
14+
15+
public DiagnosticExporter(
16+
Func<string>? outputDirectoryProvider = null,
17+
Func<string>? appBaseDirectoryProvider = null,
18+
Func<DateTime>? nowProvider = null)
19+
{
20+
this.outputDirectoryProvider = outputDirectoryProvider
21+
?? (() => Environment.GetFolderPath(Environment.SpecialFolder.Desktop));
22+
this.appBaseDirectoryProvider = appBaseDirectoryProvider
23+
?? (() => AppContext.BaseDirectory);
24+
this.nowProvider = nowProvider ?? (() => DateTime.Now);
25+
}
26+
27+
public async Task<string> ExportAsync()
28+
{
29+
string outputDirectory = outputDirectoryProvider();
30+
Directory.CreateDirectory(outputDirectory);
31+
32+
string zipPath = Path.Combine(outputDirectory, $"CDriveMaster_Diag_{nowProvider():yyyyMMdd_HHmmss}.zip");
33+
34+
string tempPath = Path.GetTempFileName();
35+
if (File.Exists(tempPath))
36+
{
37+
File.Delete(tempPath);
38+
}
39+
40+
Directory.CreateDirectory(tempPath);
41+
42+
try
43+
{
44+
string sysInfoPath = Path.Combine(tempPath, "sysinfo.txt");
45+
string sysInfo = string.Join(Environment.NewLine, new[]
46+
{
47+
$"Timestamp: {nowProvider():yyyy-MM-dd HH:mm:ss}",
48+
$"OS Version: {Environment.OSVersion.VersionString}",
49+
$"Is64BitOperatingSystem: {Environment.Is64BitOperatingSystem}",
50+
$"IsElevated: {PlatformProbe.IsElevated}"
51+
});
52+
await File.WriteAllTextAsync(sysInfoPath, sysInfo);
53+
54+
string appBase = appBaseDirectoryProvider();
55+
string logsPath = Path.Combine(appBase, "Logs");
56+
if (Directory.Exists(logsPath))
57+
{
58+
string tempLogs = Path.Combine(tempPath, "Logs");
59+
Directory.CreateDirectory(tempLogs);
60+
61+
var jsonFiles = Directory
62+
.EnumerateFiles(logsPath, "*.json", SearchOption.AllDirectories)
63+
.ToList();
64+
65+
foreach (var file in jsonFiles)
66+
{
67+
try
68+
{
69+
string relativePath = Path.GetRelativePath(logsPath, file);
70+
string target = Path.Combine(tempLogs, relativePath);
71+
string? targetDir = Path.GetDirectoryName(target);
72+
if (!string.IsNullOrWhiteSpace(targetDir))
73+
{
74+
Directory.CreateDirectory(targetDir);
75+
}
76+
77+
File.Copy(file, target, overwrite: true);
78+
}
79+
catch (IOException)
80+
{
81+
// Ignore a single locked/corrupted file and continue collecting diagnostics.
82+
}
83+
catch (UnauthorizedAccessException)
84+
{
85+
// Ignore a single inaccessible file and continue collecting diagnostics.
86+
}
87+
}
88+
}
89+
90+
string rulesPath = Path.Combine(appBase, "Rules");
91+
if (Directory.Exists(rulesPath))
92+
{
93+
string tempRules = Path.Combine(tempPath, "Rules");
94+
CopyDirectory(rulesPath, tempRules);
95+
}
96+
97+
if (File.Exists(zipPath))
98+
{
99+
File.Delete(zipPath);
100+
}
101+
102+
ZipFile.CreateFromDirectory(tempPath, zipPath);
103+
return zipPath;
104+
}
105+
finally
106+
{
107+
try
108+
{
109+
if (Directory.Exists(tempPath))
110+
{
111+
Directory.Delete(tempPath, recursive: true);
112+
}
113+
}
114+
catch
115+
{
116+
// Best effort cleanup only.
117+
}
118+
}
119+
}
120+
121+
private static void CopyDirectory(string sourceDir, string targetDir)
122+
{
123+
Directory.CreateDirectory(targetDir);
124+
125+
foreach (var file in Directory.EnumerateFiles(sourceDir))
126+
{
127+
string target = Path.Combine(targetDir, Path.GetFileName(file));
128+
File.Copy(file, target, overwrite: true);
129+
}
130+
131+
foreach (var dir in Directory.EnumerateDirectories(sourceDir))
132+
{
133+
string targetSubDir = Path.Combine(targetDir, Path.GetFileName(dir));
134+
CopyDirectory(dir, targetSubDir);
135+
}
136+
}
137+
}

src/CDriveMaster.UI/App.xaml.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ private void ConfigureServices(IServiceCollection services)
2727
services.AddTransient<BucketBuilder>();
2828
services.AddTransient<RuleCatalog>();
2929
services.AddSingleton<AuditLogExporter>();
30+
services.AddSingleton<DiagnosticExporter>();
3031
services.AddSingleton<IDialogService, MessageBoxDialogService>();
3132
services.AddSingleton<IPreviewDialogService, WpfPreviewDialogService>();
3233
services.AddSingleton<PreflightGuard>();

src/CDriveMaster.UI/MainWindow.xaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,21 @@
7171
BorderThickness="1" />
7272
<Button Content="❓ 帮助文档"
7373
Command="{Binding OpenHelpDocsCommand}"
74+
Margin="0,0,8,0"
7475
FontSize="11"
7576
Padding="8,2"
7677
Background="Transparent"
7778
BorderBrush="#FFC8D0D8"
7879
BorderThickness="1" />
80+
<Button Content="📦 导出诊断包"
81+
Command="{Binding ExportDiagnosticsCommand}"
82+
FontSize="11"
83+
FontWeight="SemiBold"
84+
Padding="10,2"
85+
Background="#FFE8F4FF"
86+
Foreground="#FF0E4A7A"
87+
BorderBrush="#FF8AB8DD"
88+
BorderThickness="1" />
7989
</StackPanel>
8090
</Grid>
8191
</Border>

src/CDriveMaster.UI/ViewModels/MainViewModel.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,37 @@
11
using CommunityToolkit.Mvvm.ComponentModel;
22
using CommunityToolkit.Mvvm.Input;
3+
using CDriveMaster.Core.Services;
4+
using CDriveMaster.UI.Services;
35
using System;
46
using System.Diagnostics;
57
using System.IO;
68
using System.Reflection;
9+
using System.Threading.Tasks;
710

811
namespace CDriveMaster.UI.ViewModels;
912

1013
public partial class MainViewModel : ObservableObject
1114
{
1215
private readonly SystemMaintenanceAnalysisViewModel systemMaintenanceViewModel;
1316
private readonly GenericCleanupViewModel genericCleanupViewModel;
17+
private readonly DiagnosticExporter diagExporter;
18+
private readonly IDialogService dialogService;
1419

1520
[ObservableProperty]
1621
private object currentViewModel;
1722

1823
public string AppVersion { get; }
1924

20-
public MainViewModel(SystemMaintenanceAnalysisViewModel systemMaintenanceViewModel, GenericCleanupViewModel genericCleanupViewModel)
25+
public MainViewModel(
26+
SystemMaintenanceAnalysisViewModel systemMaintenanceViewModel,
27+
GenericCleanupViewModel genericCleanupViewModel,
28+
DiagnosticExporter diagExporter,
29+
IDialogService dialogService)
2130
{
2231
this.systemMaintenanceViewModel = systemMaintenanceViewModel;
2332
this.genericCleanupViewModel = genericCleanupViewModel;
33+
this.diagExporter = diagExporter;
34+
this.dialogService = dialogService;
2435

2536
var informationalVersion = Assembly.GetExecutingAssembly()
2637
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
@@ -32,6 +43,29 @@ public MainViewModel(SystemMaintenanceAnalysisViewModel systemMaintenanceViewMod
3243
CurrentViewModel = this.systemMaintenanceViewModel;
3344
}
3445

46+
[RelayCommand]
47+
private async Task ExportDiagnosticsAsync()
48+
{
49+
try
50+
{
51+
string zipPath = await diagExporter.ExportAsync();
52+
await dialogService.ShowInfoAsync(
53+
"导出成功",
54+
$"诊断包已保存至桌面:{Environment.NewLine}{zipPath}{Environment.NewLine}{Environment.NewLine}请在提交反馈时附带此文件。");
55+
56+
Process.Start(new ProcessStartInfo
57+
{
58+
FileName = "explorer.exe",
59+
Arguments = $"/select,\"{zipPath}\"",
60+
UseShellExecute = true
61+
});
62+
}
63+
catch (Exception ex)
64+
{
65+
await dialogService.ShowErrorAsync("导出失败", ex.Message);
66+
}
67+
}
68+
3569
[RelayCommand]
3670
private void NavigateToSystemMaintenance()
3771
{
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using System;
2+
using System.IO;
3+
using System.IO.Compression;
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
using CDriveMaster.Core.Services;
7+
using CDriveMaster.Tests.Helpers;
8+
using FluentAssertions;
9+
using Xunit;
10+
11+
namespace CDriveMaster.Tests.Services;
12+
13+
public sealed class DiagnosticExporterTests
14+
{
15+
[Fact]
16+
public async Task ExportAsync_WithLogsAndRules_ShouldCreateZipWithExpectedEntries()
17+
{
18+
using var outputSandbox = new TempSandbox("diag-output");
19+
using var appBaseSandbox = new TempSandbox("diag-appbase");
20+
21+
_ = appBaseSandbox.CreateFile(Path.Combine("Logs", "cleanup.json"), "{\"ok\":true}");
22+
_ = appBaseSandbox.CreateFile(Path.Combine("Logs", "nested", "audit.json"), "{\"nested\":true}");
23+
_ = appBaseSandbox.CreateFile(Path.Combine("Rules", "wechat.json"), "{\"app\":\"wechat\"}");
24+
25+
var fixedNow = new DateTime(2026, 03, 25, 10, 20, 30);
26+
var exporter = new DiagnosticExporter(
27+
outputDirectoryProvider: () => outputSandbox.RootPath,
28+
appBaseDirectoryProvider: () => appBaseSandbox.RootPath,
29+
nowProvider: () => fixedNow);
30+
31+
string zipPath = await exporter.ExportAsync();
32+
33+
zipPath.Should().StartWith(outputSandbox.RootPath);
34+
File.Exists(zipPath).Should().BeTrue();
35+
36+
using var archive = ZipFile.OpenRead(zipPath);
37+
archive.Entries.Select(e => e.FullName).Should().Contain("sysinfo.txt");
38+
archive.Entries.Select(e => e.FullName).Should().Contain("Logs/cleanup.json");
39+
archive.Entries.Select(e => e.FullName).Should().Contain("Logs/nested/audit.json");
40+
archive.Entries.Select(e => e.FullName).Should().Contain("Rules/wechat.json");
41+
42+
var sysInfoEntry = archive.GetEntry("sysinfo.txt");
43+
sysInfoEntry.Should().NotBeNull();
44+
using var reader = new StreamReader(sysInfoEntry!.Open());
45+
string sysInfo = await reader.ReadToEndAsync();
46+
sysInfo.Should().Contain("Timestamp: 2026-03-25 10:20:30");
47+
sysInfo.Should().Contain("OS Version:");
48+
sysInfo.Should().Contain("Is64BitOperatingSystem:");
49+
sysInfo.Should().Contain("IsElevated:");
50+
}
51+
52+
[Fact]
53+
public async Task ExportAsync_WithoutLogsAndRules_ShouldStillCreateZipWithSysInfo()
54+
{
55+
using var outputSandbox = new TempSandbox("diag-output-empty");
56+
using var appBaseSandbox = new TempSandbox("diag-appbase-empty");
57+
58+
var exporter = new DiagnosticExporter(
59+
outputDirectoryProvider: () => outputSandbox.RootPath,
60+
appBaseDirectoryProvider: () => appBaseSandbox.RootPath,
61+
nowProvider: () => DateTime.UtcNow.AddTicks(Guid.NewGuid().GetHashCode()));
62+
63+
string zipPath = await exporter.ExportAsync();
64+
65+
File.Exists(zipPath).Should().BeTrue();
66+
67+
using var archive = ZipFile.OpenRead(zipPath);
68+
archive.Entries.Select(e => e.FullName).Should().Contain("sysinfo.txt");
69+
archive.Entries.Select(e => e.FullName).Should().NotContain(e => e.StartsWith("Logs/"));
70+
archive.Entries.Select(e => e.FullName).Should().NotContain(e => e.StartsWith("Rules/"));
71+
}
72+
}

0 commit comments

Comments
 (0)