mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-27 12:54:25 +08:00
Compare commits
6 Commits
553cee54f9
...
v0.8.7.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63f08987a7 | ||
|
|
ce41fd676c | ||
|
|
c1f148f7d6 | ||
|
|
a75ed0ced1 | ||
|
|
2dc40c53e2 | ||
|
|
a99ed9fef2 |
@@ -3,21 +3,21 @@
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Avalonia" Version="12.0.2" />
|
||||
<PackageVersion Include="Avalonia.Controls.WebView" Version="12.0.0" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="12.0.2" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.0.2" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.0.2" />
|
||||
<PackageVersion Include="Avalonia" Version="12.0.3" />
|
||||
<PackageVersion Include="Avalonia.Controls.WebView" Version="12.0.1" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="12.0.3" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.0.3" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.0.3" />
|
||||
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.1" />
|
||||
<PackageVersion Include="ClassIsland.Markdown.Avalonia" Version="12.0.0" />
|
||||
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||
<PackageVersion Include="dotnetCampus.Ipc" Version="2.0.0-alpha436" />
|
||||
<PackageVersion Include="DotNetCampus.AvaloniaInkCanvas" Version="1.0.1" />
|
||||
<PackageVersion Include="Downloader" Version="5.4.0" />
|
||||
<PackageVersion Include="FluentAvaloniaUI" Version="3.0.0-preview2" />
|
||||
<PackageVersion Include="FluentAvaloniaUI" Version="3.0.0-preview4" />
|
||||
<PackageVersion Include="FluentIcons.Avalonia" Version="2.1.325" />
|
||||
<PackageVersion Include="Lib.Harmony.Thin" Version="2.4.2" />
|
||||
<PackageVersion Include="Material.Avalonia" Version="3.16.1" />
|
||||
<PackageVersion Include="Material.Avalonia" Version="3.17.0" />
|
||||
<PackageVersion Include="MaterialColorUtilities" Version="0.3.0" />
|
||||
<PackageVersion Include="Material.Icons.Avalonia" Version="3.0.3-nightly.0.2" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="11.0.0-preview.3.26207.106" />
|
||||
@@ -30,8 +30,8 @@
|
||||
<PackageVersion Include="MudTools.OfficeInterop.PowerPoint" Version="2.0.9" />
|
||||
<PackageVersion Include="MudTools.OfficeInterop.Word" Version="2.0.9" />
|
||||
<PackageVersion Include="PortAudioSharp2" Version="1.0.6" />
|
||||
<PackageVersion Include="PostHog" Version="2.6.0" />
|
||||
<PackageVersion Include="Sentry" Version="6.4.1" />
|
||||
<PackageVersion Include="PostHog" Version="2.7.1" />
|
||||
<PackageVersion Include="Sentry" Version="6.5.0" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="11.0.0-preview.3.26207.106" />
|
||||
<PackageVersion Include="System.Runtime.WindowsRuntime" Version="5.0.0-preview.5.20278.1" />
|
||||
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.92.0" />
|
||||
|
||||
355
LanMountainDesktop.Tests/UpdateSettingsInterfaceTests.cs
Normal file
355
LanMountainDesktop.Tests/UpdateSettingsInterfaceTests.cs
Normal file
@@ -0,0 +1,355 @@
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.Services.Update;
|
||||
using LanMountainDesktop.Shared.Contracts.Update;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
using Xunit;
|
||||
using UpdateDownloadResult = LanMountainDesktop.Services.Update.DownloadResult;
|
||||
using SettingsUpdateState = LanMountainDesktop.Services.Settings.UpdateSettingsState;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class UpdateSettingsInterfaceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task UpdateSettingsViewModel_RoutesActionsThroughUpdateSettingsService()
|
||||
{
|
||||
var update = new FakeUpdateSettingsService();
|
||||
var viewModel = new UpdateSettingsViewModel(new FakeSettingsFacade(update));
|
||||
|
||||
Assert.Equal(0, update.SaveCalls);
|
||||
|
||||
update.CheckReport = new UpdateCheckReport(
|
||||
true,
|
||||
"1.2.3",
|
||||
"1.0.0",
|
||||
UpdatePayloadKind.DeltaPlonds,
|
||||
"dist-1",
|
||||
UpdateSettingsValues.ChannelStable,
|
||||
DateTimeOffset.Parse("2026-05-06T00:00:00Z"),
|
||||
42,
|
||||
null,
|
||||
null);
|
||||
|
||||
await ((IAsyncRelayCommand)viewModel.CheckCommand).ExecuteAsync(null);
|
||||
|
||||
Assert.Equal(1, update.CheckCalls);
|
||||
Assert.Equal("1.2.3", viewModel.LatestVersionText);
|
||||
Assert.True(viewModel.IsDeltaUpdate);
|
||||
|
||||
update.SetPhase(UpdatePhase.Checked);
|
||||
await ((IAsyncRelayCommand)viewModel.DownloadCommand).ExecuteAsync(null);
|
||||
Assert.Equal(1, update.DownloadCalls);
|
||||
|
||||
update.SetPhase(UpdatePhase.Downloaded);
|
||||
await ((IAsyncRelayCommand)viewModel.InstallCommand).ExecuteAsync(null);
|
||||
Assert.Equal(1, update.InstallCalls);
|
||||
|
||||
update.SetPhase(UpdatePhase.Downloading);
|
||||
await ((IAsyncRelayCommand)viewModel.PauseCommand).ExecuteAsync(null);
|
||||
Assert.Equal(1, update.PauseCalls);
|
||||
|
||||
update.SetPhase(UpdatePhase.PausedDownloading);
|
||||
await ((IAsyncRelayCommand)viewModel.ResumeCommand).ExecuteAsync(null);
|
||||
Assert.Equal(1, update.ResumeCalls);
|
||||
|
||||
update.SetPhase(UpdatePhase.Downloading);
|
||||
await ((IAsyncRelayCommand)viewModel.CancelCommand).ExecuteAsync(null);
|
||||
Assert.Equal(1, update.CancelCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateSettingsViewModel_SavesPreferencesThroughUpdateSettingsService()
|
||||
{
|
||||
var update = new FakeUpdateSettingsService();
|
||||
var viewModel = new UpdateSettingsViewModel(new FakeSettingsFacade(update));
|
||||
|
||||
viewModel.SelectedUpdateChannelValue = UpdateSettingsValues.ChannelPreview;
|
||||
viewModel.SelectedUpdateSourceValue = UpdateSettingsValues.DownloadSourceGitHub;
|
||||
viewModel.SelectedUpdateModeValue = UpdateSettingsValues.ModeManual;
|
||||
viewModel.DownloadThreadsSliderValue = 12;
|
||||
viewModel.ForceReinstall = true;
|
||||
|
||||
Assert.True(update.SaveCalls >= 5);
|
||||
Assert.Equal(UpdateSettingsValues.ChannelPreview, update.State.UpdateChannel);
|
||||
Assert.Equal(UpdateSettingsValues.DownloadSourceGitHub, update.State.UpdateDownloadSource);
|
||||
Assert.Equal(UpdateSettingsValues.ModeManual, update.State.UpdateMode);
|
||||
Assert.Equal(12, update.State.UpdateDownloadThreads);
|
||||
Assert.True(update.State.ForceUpdateReinstall);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateSettingsViewModel_RestoresPersistedPendingAndLastCheckedState()
|
||||
{
|
||||
var update = new FakeUpdateSettingsService
|
||||
{
|
||||
State = DefaultUpdateState() with
|
||||
{
|
||||
PendingUpdateVersion = "2.0.0",
|
||||
PendingUpdatePublishedAtUtcMs = DateTimeOffset.Parse("2026-05-06T00:00:00Z").ToUnixTimeMilliseconds(),
|
||||
LastUpdateCheckUtcMs = DateTimeOffset.Parse("2026-05-07T00:00:00Z").ToUnixTimeMilliseconds()
|
||||
}
|
||||
};
|
||||
|
||||
var viewModel = new UpdateSettingsViewModel(new FakeSettingsFacade(update));
|
||||
|
||||
Assert.True(viewModel.IsUpdateAvailable);
|
||||
Assert.Equal("2.0.0", viewModel.LatestVersionText);
|
||||
Assert.NotEmpty(viewModel.PublishedAtText);
|
||||
Assert.Contains("Last checked", viewModel.LastCheckedText, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SettingsUpdateManifestProvider_UsesSelectedUpdateSource()
|
||||
{
|
||||
var update = new FakeUpdateSettingsService
|
||||
{
|
||||
State = DefaultUpdateState() with { UpdateDownloadSource = UpdateSettingsValues.DownloadSourceGitHub }
|
||||
};
|
||||
var plonds = new FakeManifestProvider("plonds");
|
||||
var github = new FakeManifestProvider("github");
|
||||
var provider = new SettingsUpdateManifestProvider(new FakeSettingsFacade(update), plonds, github);
|
||||
|
||||
var manifest = await provider.GetLatestAsync(
|
||||
UpdateSettingsValues.ChannelStable,
|
||||
"windows-x64",
|
||||
new Version(1, 0, 0),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal("github", manifest?.DistributionId);
|
||||
Assert.Equal(0, plonds.GetLatestCalls);
|
||||
Assert.Equal(1, github.GetLatestCalls);
|
||||
|
||||
update.State = update.State with { UpdateDownloadSource = UpdateSettingsValues.DownloadSourcePlonds };
|
||||
manifest = await provider.GetLatestAsync(
|
||||
UpdateSettingsValues.ChannelStable,
|
||||
"windows-x64",
|
||||
new Version(1, 0, 0),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal("plonds", manifest?.DistributionId);
|
||||
Assert.Equal(1, plonds.GetLatestCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromFullInstaller_IncludesPreferredInstallerInMirrors()
|
||||
{
|
||||
var release = new GitHubReleaseInfo(
|
||||
"v1.2.3",
|
||||
"Release",
|
||||
false,
|
||||
false,
|
||||
DateTimeOffset.Parse("2026-05-06T00:00:00Z"),
|
||||
[new GitHubReleaseAsset("LanMountainDesktop-setup-x64.exe", "https://example.test/setup.exe", 123, "abc")]);
|
||||
|
||||
var manifest = UpdateManifestMapper.FromFullInstaller(release, UpdateSettingsValues.ChannelStable, "windows-x64");
|
||||
|
||||
Assert.NotNull(manifest.InstallerMirrors);
|
||||
var mirror = Assert.Single(manifest.InstallerMirrors!);
|
||||
Assert.Equal("https://example.test/setup.exe", mirror.Url);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyDownloadSource_UsesGhProxyForGithubProxySource()
|
||||
{
|
||||
var url = "https://github.com/owner/repo/releases/download/v1/app.exe";
|
||||
|
||||
Assert.Equal(url, UpdateDownloadEngine.ApplyDownloadSource(url, UpdateSettingsValues.DownloadSourceGitHub));
|
||||
Assert.Equal(
|
||||
$"{UpdateSettingsValues.DefaultGhProxyBaseUrl}{url}",
|
||||
UpdateDownloadEngine.ApplyDownloadSource(url, UpdateSettingsValues.DownloadSourceGhProxy));
|
||||
}
|
||||
|
||||
private static SettingsUpdateState DefaultUpdateState() => new(
|
||||
IncludePrereleaseUpdates: false,
|
||||
UpdateChannel: UpdateSettingsValues.ChannelStable,
|
||||
UpdateMode: UpdateSettingsValues.ModeSilentDownload,
|
||||
UpdateDownloadSource: UpdateSettingsValues.DownloadSourcePlonds,
|
||||
UpdateDownloadThreads: UpdateSettingsValues.DefaultDownloadThreads,
|
||||
ForceUpdateReinstall: false,
|
||||
UseGhProxyMirror: false,
|
||||
PendingUpdateInstallerPath: null,
|
||||
PendingUpdateVersion: null,
|
||||
PendingUpdatePublishedAtUtcMs: null,
|
||||
LastUpdateCheckUtcMs: null,
|
||||
PendingUpdateSha256: null);
|
||||
|
||||
private sealed class FakeUpdateSettingsService : IUpdateSettingsService
|
||||
{
|
||||
public SettingsUpdateState State { get; set; } = DefaultUpdateState();
|
||||
public UpdatePhase CurrentPhase { get; private set; } = UpdatePhase.Idle;
|
||||
public UpdateCheckReport CheckReport { get; set; } = new(false, null, null, null, null, null, null, null, null, null);
|
||||
public UpdateDownloadResult DownloadResult { get; set; } = new(true, "downloaded", null, true);
|
||||
public InstallResult InstallResult { get; set; } = new(true, null, false);
|
||||
public int SaveCalls { get; private set; }
|
||||
public int CheckCalls { get; private set; }
|
||||
public int DownloadCalls { get; private set; }
|
||||
public int InstallCalls { get; private set; }
|
||||
public int PauseCalls { get; private set; }
|
||||
public int ResumeCalls { get; private set; }
|
||||
public int CancelCalls { get; private set; }
|
||||
|
||||
public event Action<UpdatePhase>? PhaseChanged;
|
||||
public event Action<UpdateProgressReport>? ProgressChanged;
|
||||
|
||||
public void SetPhase(UpdatePhase phase)
|
||||
{
|
||||
CurrentPhase = phase;
|
||||
PhaseChanged?.Invoke(phase);
|
||||
}
|
||||
|
||||
public SettingsUpdateState Get() => State;
|
||||
|
||||
public void Save(SettingsUpdateState state)
|
||||
{
|
||||
SaveCalls++;
|
||||
State = state;
|
||||
}
|
||||
|
||||
public Task<UpdateCheckReport> CheckAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
CheckCalls++;
|
||||
SetPhase(UpdatePhase.Checked);
|
||||
return Task.FromResult(CheckReport);
|
||||
}
|
||||
|
||||
public Task<UpdateDownloadResult> DownloadAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
DownloadCalls++;
|
||||
SetPhase(UpdatePhase.Downloaded);
|
||||
return Task.FromResult(DownloadResult);
|
||||
}
|
||||
|
||||
public Task<InstallResult> InstallAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
InstallCalls++;
|
||||
SetPhase(UpdatePhase.Installed);
|
||||
return Task.FromResult(InstallResult);
|
||||
}
|
||||
|
||||
public Task RollbackAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
|
||||
public Task PauseAsync()
|
||||
{
|
||||
PauseCalls++;
|
||||
SetPhase(UpdatePhase.PausedDownloading);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<UpdateDownloadResult> ResumeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
ResumeCalls++;
|
||||
SetPhase(UpdatePhase.Downloaded);
|
||||
return Task.FromResult(DownloadResult);
|
||||
}
|
||||
|
||||
public Task CancelAsync()
|
||||
{
|
||||
CancelCalls++;
|
||||
SetPhase(UpdatePhase.Idle);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task AutoCheckIfEnabledAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
|
||||
public bool TryApplyOnExit() => false;
|
||||
|
||||
public Task<UpdateCheckResult> CheckForUpdatesAsync(Version currentVersion, bool includePrerelease, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(new UpdateCheckResult(true, false, currentVersion.ToString(), string.Empty, null, null, null));
|
||||
|
||||
public Task<UpdateCheckResult> ForceCheckForUpdatesAsync(Version currentVersion, bool includePrerelease, CancellationToken cancellationToken = default)
|
||||
=> CheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken);
|
||||
|
||||
public Task<PlondsUpdatePayload?> GetPlondsUpdatePayloadAsync(Version currentVersion, bool includePrerelease, bool isForce = false, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<PlondsUpdatePayload?>(null);
|
||||
|
||||
public Task<LanMountainDesktop.Services.UpdateDownloadResult> DownloadAssetAsync(
|
||||
GitHubReleaseAsset asset,
|
||||
string destinationFilePath,
|
||||
string downloadSource,
|
||||
int maxParallelSegments,
|
||||
IProgress<double>? progress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(new LanMountainDesktop.Services.UpdateDownloadResult(false, null, "not used", false));
|
||||
|
||||
public Task<LanMountainDesktop.Services.UpdateDownloadResult> RedownloadAssetAsync(
|
||||
GitHubReleaseAsset asset,
|
||||
string destinationFilePath,
|
||||
string downloadSource,
|
||||
int maxParallelSegments,
|
||||
IProgress<double>? progress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(new LanMountainDesktop.Services.UpdateDownloadResult(false, null, "not used", false));
|
||||
}
|
||||
|
||||
private sealed class FakeManifestProvider(string providerName) : IUpdateManifestProvider
|
||||
{
|
||||
public string ProviderName { get; } = providerName;
|
||||
public int GetLatestCalls { get; private set; }
|
||||
|
||||
public Task<UpdateManifest?> GetLatestAsync(string channel, string platform, Version currentVersion, CancellationToken ct)
|
||||
{
|
||||
GetLatestCalls++;
|
||||
return Task.FromResult<UpdateManifest?>(CreateManifest(ProviderName, channel, platform));
|
||||
}
|
||||
|
||||
public Task<UpdateManifest?> GetByVersionAsync(string version, string channel, string platform, CancellationToken ct)
|
||||
=> Task.FromResult<UpdateManifest?>(CreateManifest(ProviderName, channel, platform));
|
||||
|
||||
public Task<IReadOnlyList<UpdateManifest>> GetIncrementalChainAsync(string channel, string platform, Version fromVersion, Version toVersion, CancellationToken ct)
|
||||
=> Task.FromResult<IReadOnlyList<UpdateManifest>>([CreateManifest(ProviderName, channel, platform)]);
|
||||
|
||||
private static UpdateManifest CreateManifest(string id, string channel, string platform) => new(
|
||||
id,
|
||||
"1.0.0",
|
||||
"1.1.0",
|
||||
platform,
|
||||
channel,
|
||||
DateTimeOffset.Parse("2026-05-06T00:00:00Z"),
|
||||
UpdatePayloadKind.DeltaPlonds,
|
||||
"https://example.test/filemap.json",
|
||||
"https://example.test/filemap.json.sig",
|
||||
null,
|
||||
[],
|
||||
null,
|
||||
new Dictionary<string, string>());
|
||||
}
|
||||
|
||||
private sealed class FakeSettingsFacade(IUpdateSettingsService update) : ISettingsFacadeService
|
||||
{
|
||||
public ISettingsService Settings => throw new NotSupportedException();
|
||||
public ISettingsCatalog Catalog => throw new NotSupportedException();
|
||||
public IGridSettingsService Grid => throw new NotSupportedException();
|
||||
public IWallpaperSettingsService Wallpaper => throw new NotSupportedException();
|
||||
public IWallpaperMediaService WallpaperMedia => throw new NotSupportedException();
|
||||
public IThemeAppearanceService Theme => throw new NotSupportedException();
|
||||
public IStatusBarSettingsService StatusBar => throw new NotSupportedException();
|
||||
public ITextCapsuleSettingsService TextCapsule => throw new NotSupportedException();
|
||||
public IWeatherSettingsService Weather => throw new NotSupportedException();
|
||||
public IRegionSettingsService Region { get; } = new FakeRegionSettingsService();
|
||||
public IPrivacySettingsService Privacy => throw new NotSupportedException();
|
||||
public IUpdateSettingsService Update { get; } = update;
|
||||
public ILauncherCatalogService LauncherCatalog => throw new NotSupportedException();
|
||||
public ILauncherPolicyService LauncherPolicy => throw new NotSupportedException();
|
||||
public IPluginManagementSettingsService PluginManagement => throw new NotSupportedException();
|
||||
public IPluginCatalogSettingsService PluginCatalog => throw new NotSupportedException();
|
||||
public IApplicationInfoService ApplicationInfo { get; } = new FakeApplicationInfoService();
|
||||
}
|
||||
|
||||
private sealed class FakeRegionSettingsService : IRegionSettingsService
|
||||
{
|
||||
public RegionSettingsState Get() => new("en-US", null);
|
||||
public void Save(RegionSettingsState state) { }
|
||||
public TimeZoneService GetTimeZoneService() => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
private sealed class FakeApplicationInfoService : IApplicationInfoService
|
||||
{
|
||||
public string GetAppVersionText() => "1.0.0";
|
||||
public string GetAppCodenameText() => "Test";
|
||||
public AppRenderBackendInfo GetRenderBackendInfo() => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -1199,6 +1199,7 @@ public partial class App : Application
|
||||
|
||||
try
|
||||
{
|
||||
TelemetryServices.Usage?.TrackSessionEnded("App.PerformExitCleanup");
|
||||
TelemetryServices.Usage?.Shutdown(
|
||||
_shutdownIntent == ShutdownIntent.RestartRequested,
|
||||
"App.PerformExitCleanup");
|
||||
@@ -1210,7 +1211,7 @@ public partial class App : Application
|
||||
|
||||
try
|
||||
{
|
||||
HostUpdateOrchestratorProvider.GetOrCreate().TryApplyOnExit();
|
||||
_settingsFacade.Update.TryApplyOnExit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1441,5 +1441,29 @@
|
||||
"settings.general.back_to_windows_fluent_icon_desc": "搜索并选择左侧图标位使用的内置 Fluent 图标。",
|
||||
"settings.general.back_to_windows_icon_text_header": "文字图标",
|
||||
"settings.general.back_to_windows_icon_text_desc": "输入最多四个字符,作为左侧图标显示。",
|
||||
"settings.general.back_to_windows_fluent_icon_search_placeholder": "搜索图标"
|
||||
"settings.general.back_to_windows_fluent_icon_search_placeholder": "搜索图标",
|
||||
"settings.update.channel_description": "选择“正式版”以保证稳定性,选择“预览版”体验早期功能。",
|
||||
"settings.update.check_card_title": "检查更新",
|
||||
"settings.update.download_threads_description": "设置应用更新下载的并行线程数,可随时暂停并在支持的情况下恢复下载。",
|
||||
"settings.update.force_reinstall_description": "下载所选版本的完整包,将此次运行标记为重新安装,而不是增量更新。",
|
||||
"settings.update.force_reinstall_label": "强制重新安装",
|
||||
"settings.update.latest_version_none": "已是最新",
|
||||
"settings.update.mode_description": "“手动更新”不自动下载与安装。“静默下载”在后台下载,由你确认安装。“静默安装”在后台下载并于下次退出时应用。",
|
||||
"settings.update.mode_silent_download": "静默下载",
|
||||
"settings.update.mode_silent_install": "静默安装",
|
||||
"settings.update.resume_support_description": "下载操作会保留部分文件与包元数据,以便在服务器支持时通过暂停和继续功能恢复之前的进度。",
|
||||
"settings.update.resume_support_label": "断点续传支持",
|
||||
"settings.update.source_description": "选择更新工作流所使用的清单与安装包来源。",
|
||||
"settings.update.source_gh_proxy": "gh-proxy 镜像",
|
||||
"settings.update.status_download_failed": "下载失败。",
|
||||
"settings.update.status_install_failed": "安装失败。",
|
||||
"settings.update.status_installed": "安装完成。",
|
||||
"settings.update.status_paused": "更新已暂停。",
|
||||
"settings.update.status_resuming": "正在恢复下载...",
|
||||
"settings.update.status_rolled_back": "已回滚更新。",
|
||||
"settings.update.status_section_header": "更新状态",
|
||||
"settings.update.transfer_controls_description": "暂停正在运行的下载,从保存的状态恢复,或取消并清除待处理的更新文件。",
|
||||
"settings.update.transfer_controls_title": "传输控制",
|
||||
"settings.update.type_reinstall": "重新安装",
|
||||
"settings.update.update_type_label": "更新类型"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace LanMountainDesktop.Models;
|
||||
namespace LanMountainDesktop.Models;
|
||||
|
||||
public sealed class DesktopComponentPlacementSnapshot
|
||||
{
|
||||
@@ -7,7 +7,7 @@ public sealed class DesktopComponentPlacementSnapshot
|
||||
public int PageIndex { get; set; }
|
||||
|
||||
public string ComponentId { get; set; } = string.Empty;
|
||||
|
||||
public string ComponentName { get; set; } = string.Empty;
|
||||
public int Row { get; set; }
|
||||
|
||||
public int Column { get; set; }
|
||||
|
||||
@@ -103,57 +103,57 @@ public sealed class PostHogUsageTelemetryService : IDisposable
|
||||
public void TrackMainWindowOpened(string source, bool isVisible, string windowState)
|
||||
{
|
||||
CaptureEvent(
|
||||
"main_window_opened",
|
||||
TelemetryEventNames.MainWindowOpened,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source,
|
||||
["is_visible"] = isVisible,
|
||||
["window_state"] = windowState
|
||||
},
|
||||
forceFlush: true);
|
||||
forceFlush: false);
|
||||
}
|
||||
|
||||
public void TrackMainWindowClosed(string source, bool wasVisible, string windowState)
|
||||
{
|
||||
CaptureEvent(
|
||||
"main_window_closed",
|
||||
TelemetryEventNames.MainWindowClosed,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source,
|
||||
["was_visible"] = wasVisible,
|
||||
["window_state"] = windowState
|
||||
},
|
||||
forceFlush: true);
|
||||
forceFlush: false);
|
||||
}
|
||||
|
||||
public void TrackSettingsWindowOpened(string source, string? currentPageId)
|
||||
{
|
||||
CaptureEvent(
|
||||
"settings_window_opened",
|
||||
TelemetryEventNames.SettingsWindowOpened,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source,
|
||||
["current_page_id"] = currentPageId
|
||||
},
|
||||
forceFlush: true);
|
||||
forceFlush: false);
|
||||
}
|
||||
|
||||
public void TrackSettingsWindowClosed(string source, string? currentPageId)
|
||||
{
|
||||
CaptureEvent(
|
||||
"settings_window_closed",
|
||||
TelemetryEventNames.SettingsWindowClosed,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source,
|
||||
["current_page_id"] = currentPageId
|
||||
},
|
||||
forceFlush: true);
|
||||
forceFlush: false);
|
||||
}
|
||||
|
||||
public void TrackSettingsNavigation(string? fromPageId, string? toPageId, string source)
|
||||
{
|
||||
CaptureEvent(
|
||||
"settings_navigation",
|
||||
TelemetryEventNames.SettingsNavigation,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source,
|
||||
@@ -167,37 +167,37 @@ public sealed class PostHogUsageTelemetryService : IDisposable
|
||||
public void TrackSettingsDrawerOpened(string? pageId, string? drawerTitle)
|
||||
{
|
||||
CaptureEvent(
|
||||
"settings_drawer_opened",
|
||||
TelemetryEventNames.SettingsDrawerOpened,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["page_id"] = pageId,
|
||||
["drawer_title"] = drawerTitle
|
||||
},
|
||||
forceFlush: true);
|
||||
forceFlush: false);
|
||||
}
|
||||
|
||||
public void TrackSettingsDrawerClosed(string? pageId, string? drawerTitle)
|
||||
{
|
||||
CaptureEvent(
|
||||
"settings_drawer_closed",
|
||||
TelemetryEventNames.SettingsDrawerClosed,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["page_id"] = pageId,
|
||||
["drawer_title"] = drawerTitle
|
||||
},
|
||||
forceFlush: true);
|
||||
forceFlush: false);
|
||||
}
|
||||
|
||||
public void TrackDesktopComponentPlaced(DesktopComponentPlacementSnapshot placement, string source)
|
||||
{
|
||||
CaptureEvent(
|
||||
"desktop_component_placed",
|
||||
TelemetryEventNames.DesktopComponentPlaced,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source
|
||||
},
|
||||
stateAfter: DescribePlacement(placement),
|
||||
forceFlush: true);
|
||||
forceFlush: false);
|
||||
}
|
||||
|
||||
public void TrackDesktopComponentMoved(
|
||||
@@ -206,14 +206,14 @@ public sealed class PostHogUsageTelemetryService : IDisposable
|
||||
string source)
|
||||
{
|
||||
CaptureEvent(
|
||||
"desktop_component_moved",
|
||||
TelemetryEventNames.DesktopComponentMoved,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source
|
||||
},
|
||||
stateBefore: DescribePlacement(before),
|
||||
stateAfter: DescribePlacement(after),
|
||||
forceFlush: true);
|
||||
forceFlush: false);
|
||||
}
|
||||
|
||||
public void TrackDesktopComponentResized(
|
||||
@@ -222,38 +222,38 @@ public sealed class PostHogUsageTelemetryService : IDisposable
|
||||
string source)
|
||||
{
|
||||
CaptureEvent(
|
||||
"desktop_component_resized",
|
||||
TelemetryEventNames.DesktopComponentResized,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source
|
||||
},
|
||||
stateBefore: DescribePlacement(before),
|
||||
stateAfter: DescribePlacement(after),
|
||||
forceFlush: true);
|
||||
forceFlush: false);
|
||||
}
|
||||
|
||||
public void TrackDesktopComponentDeleted(DesktopComponentPlacementSnapshot before, string source)
|
||||
{
|
||||
CaptureEvent(
|
||||
"desktop_component_deleted",
|
||||
TelemetryEventNames.DesktopComponentDeleted,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source
|
||||
},
|
||||
stateBefore: DescribePlacement(before),
|
||||
forceFlush: true);
|
||||
forceFlush: false);
|
||||
}
|
||||
|
||||
public void TrackDesktopComponentEditorOpened(DesktopComponentPlacementSnapshot placement, string source)
|
||||
{
|
||||
CaptureEvent(
|
||||
"desktop_component_editor_opened",
|
||||
TelemetryEventNames.DesktopComponentEditorOpened,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source
|
||||
},
|
||||
stateBefore: DescribePlacement(placement),
|
||||
forceFlush: true);
|
||||
forceFlush: false);
|
||||
}
|
||||
|
||||
public void TrackSessionStarted(string source)
|
||||
@@ -310,24 +310,29 @@ public sealed class PostHogUsageTelemetryService : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
var distinctId = identity.InstallId;
|
||||
var distinctId = identity.TelemetryId;
|
||||
var personProps = new Dictionary<string, object?>
|
||||
{
|
||||
["install_id"] = identity.InstallId,
|
||||
["telemetry_id"] = identity.TelemetryId,
|
||||
["app_version"] = TelemetryEnvironmentInfo.GetAppVersion(),
|
||||
["os_name"] = TelemetryEnvironmentInfo.GetOsName(),
|
||||
["os_version"] = TelemetryEnvironmentInfo.GetOsVersion(),
|
||||
["device_model"] = TelemetryEnvironmentInfo.GetDeviceModel(),
|
||||
["device_arch"] = TelemetryEnvironmentInfo.GetDeviceArchitecture(),
|
||||
["runtime_version"] = TelemetryEnvironmentInfo.GetRuntimeVersion(),
|
||||
["language"] = TelemetryEnvironmentInfo.GetSystemLanguage()
|
||||
["language"] = TelemetryEnvironmentInfo.GetSystemLanguage(),
|
||||
["os_build"] = TelemetryEnvironmentInfo.GetOsBuild(),
|
||||
["clr_version"] = TelemetryEnvironmentInfo.GetClrVersion(),
|
||||
["language_display_name"] = TelemetryEnvironmentInfo.GetSystemLanguageDisplayName(),
|
||||
["render_mode"] = TelemetryEnvironmentInfo.GetRenderMode()
|
||||
};
|
||||
|
||||
_ = _client.IdentifyAsync(distinctId, personProps, null, _cts.Token);
|
||||
|
||||
_client.Capture(
|
||||
distinctId,
|
||||
"app_first_launch",
|
||||
TelemetryEventNames.AppFirstLaunch,
|
||||
personProps,
|
||||
groups: null,
|
||||
sendFeatureFlags: false);
|
||||
@@ -360,7 +365,7 @@ public sealed class PostHogUsageTelemetryService : IDisposable
|
||||
_sequence = 0;
|
||||
|
||||
CaptureEvent(
|
||||
"app_session_start",
|
||||
TelemetryEventNames.AppSessionStart,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source,
|
||||
@@ -368,12 +373,7 @@ public sealed class PostHogUsageTelemetryService : IDisposable
|
||||
["session_start_utc"] = _sessionStartUtc.ToString("o"),
|
||||
["local_hour"] = _sessionStartUtc.ToLocalTime().Hour,
|
||||
["day_part"] = TelemetryEnvironmentInfo.GetLocalDayPart(_sessionStartUtc),
|
||||
["timezone"] = TimeZoneInfo.Local.Id,
|
||||
["app_version"] = TelemetryEnvironmentInfo.GetAppVersion(),
|
||||
["os_name"] = TelemetryEnvironmentInfo.GetOsName(),
|
||||
["os_version"] = TelemetryEnvironmentInfo.GetOsVersion(),
|
||||
["device_model"] = TelemetryEnvironmentInfo.GetDeviceModel(),
|
||||
["device_arch"] = TelemetryEnvironmentInfo.GetDeviceArchitecture()
|
||||
["timezone"] = TimeZoneInfo.Local.Id
|
||||
},
|
||||
forceFlush: true);
|
||||
|
||||
@@ -391,7 +391,7 @@ public sealed class PostHogUsageTelemetryService : IDisposable
|
||||
var durationMs = Math.Max(0, (long)(endUtc - _sessionStartUtc).TotalMilliseconds);
|
||||
|
||||
CaptureEvent(
|
||||
"app_session_end",
|
||||
TelemetryEventNames.AppSessionEnd,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source,
|
||||
@@ -456,20 +456,14 @@ public sealed class PostHogUsageTelemetryService : IDisposable
|
||||
["session_id"] = _sessionId,
|
||||
["sequence"] = seq,
|
||||
["timestamp_utc"] = DateTimeOffset.UtcNow.ToString("o"),
|
||||
["app_version"] = TelemetryEnvironmentInfo.GetAppVersion(),
|
||||
["os_name"] = TelemetryEnvironmentInfo.GetOsName(),
|
||||
["os_version"] = TelemetryEnvironmentInfo.GetOsVersion(),
|
||||
["device_model"] = TelemetryEnvironmentInfo.GetDeviceModel(),
|
||||
["device_arch"] = TelemetryEnvironmentInfo.GetDeviceArchitecture(),
|
||||
["runtime_version"] = TelemetryEnvironmentInfo.GetRuntimeVersion(),
|
||||
["language"] = TelemetryEnvironmentInfo.GetSystemLanguage()
|
||||
["event_display_name"] = TelemetryEventNames.DisplayName(eventName)
|
||||
};
|
||||
|
||||
if (payload is not null)
|
||||
{
|
||||
foreach (var kvp in payload)
|
||||
{
|
||||
properties[$"payload_{kvp.Key}"] = kvp.Value;
|
||||
properties[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,6 +510,7 @@ public sealed class PostHogUsageTelemetryService : IDisposable
|
||||
{
|
||||
["placement_id"] = placement.PlacementId,
|
||||
["component_id"] = placement.ComponentId,
|
||||
["component_name"] = placement.ComponentName ?? placement.ComponentId,
|
||||
["page_index"] = placement.PageIndex,
|
||||
["row"] = placement.Row,
|
||||
["column"] = placement.Column,
|
||||
|
||||
@@ -104,7 +104,7 @@ public sealed class SentryCrashTelemetryService : IDisposable
|
||||
|
||||
var eventId = SentrySdk.CaptureException(exception, scope =>
|
||||
{
|
||||
ApplyCommonScope(scope, source, "unhandled_exception", includeLogTail: true);
|
||||
ApplyCommonScope(scope, source, TelemetryEventNames.SentryUnhandledException, includeLogTail: true);
|
||||
scope.Level = isTerminating ? SentryLevel.Fatal : SentryLevel.Error;
|
||||
scope.SetTag("exception_source", source);
|
||||
scope.SetTag("is_terminating", isTerminating.ToString());
|
||||
@@ -136,7 +136,7 @@ public sealed class SentryCrashTelemetryService : IDisposable
|
||||
|
||||
var eventId = SentrySdk.CaptureException(exception, scope =>
|
||||
{
|
||||
ApplyCommonScope(scope, source, "task_exception", includeLogTail: true);
|
||||
ApplyCommonScope(scope, source, TelemetryEventNames.SentryTaskException, includeLogTail: true);
|
||||
scope.Level = SentryLevel.Error;
|
||||
scope.SetTag("exception_source", source);
|
||||
});
|
||||
@@ -155,9 +155,9 @@ public sealed class SentryCrashTelemetryService : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
var eventId = SentrySdk.CaptureMessage("application_shutdown", scope =>
|
||||
var eventId = SentrySdk.CaptureMessage(TelemetryEventNames.SentryShutdown, scope =>
|
||||
{
|
||||
ApplyCommonScope(scope, source, "shutdown", includeLogTail: true);
|
||||
ApplyCommonScope(scope, source, TelemetryEventNames.SentryShutdown, includeLogTail: true);
|
||||
scope.Level = SentryLevel.Info;
|
||||
scope.SetTag("shutdown_intent", isRestart ? "restart" : "exit");
|
||||
scope.SetExtra("shutdown_intent", isRestart ? "restart" : "exit");
|
||||
@@ -209,7 +209,7 @@ public sealed class SentryCrashTelemetryService : IDisposable
|
||||
options.Dsn = SentryDsn;
|
||||
options.AutoSessionTracking = true;
|
||||
options.AttachStacktrace = true;
|
||||
options.SendDefaultPii = true;
|
||||
options.SendDefaultPii = false;
|
||||
options.MaxBreadcrumbs = 100;
|
||||
options.Release = TelemetryEnvironmentInfo.GetAppVersion();
|
||||
options.Environment = TelemetryEnvironmentInfo.GetEnvironment();
|
||||
@@ -293,27 +293,19 @@ public sealed class SentryCrashTelemetryService : IDisposable
|
||||
|
||||
scope.User = new SentryUser
|
||||
{
|
||||
Id = telemetryId,
|
||||
IpAddress = AutoIpAddress
|
||||
Id = telemetryId
|
||||
};
|
||||
|
||||
scope.SetTag("telemetry_channel", "sentry");
|
||||
scope.SetTag("event_type", eventType);
|
||||
scope.SetTag("event_display_name", TelemetryEventNames.DisplayName(eventType));
|
||||
scope.SetTag("source", source);
|
||||
scope.SetTag("install_id", installId);
|
||||
scope.SetTag("telemetry_id", telemetryId);
|
||||
scope.SetTag("app_version", TelemetryEnvironmentInfo.GetAppVersion());
|
||||
scope.SetTag("environment", TelemetryEnvironmentInfo.GetEnvironment());
|
||||
scope.SetTag("os_name", TelemetryEnvironmentInfo.GetOsName());
|
||||
scope.SetTag("os_version", TelemetryEnvironmentInfo.GetOsVersion());
|
||||
scope.SetTag("os_build", TelemetryEnvironmentInfo.GetOsBuild());
|
||||
scope.SetTag("device_model", TelemetryEnvironmentInfo.GetDeviceModel());
|
||||
scope.SetTag("device_arch", TelemetryEnvironmentInfo.GetDeviceArchitecture());
|
||||
scope.SetTag("processor_count", TelemetryEnvironmentInfo.GetProcessorCount().ToString());
|
||||
scope.SetTag("total_memory_mb", TelemetryEnvironmentInfo.GetTotalMemoryMB().ToString());
|
||||
scope.SetTag("runtime_version", TelemetryEnvironmentInfo.GetRuntimeVersion());
|
||||
scope.SetTag("clr_version", TelemetryEnvironmentInfo.GetClrVersion());
|
||||
scope.SetTag("language", TelemetryEnvironmentInfo.GetSystemLanguage());
|
||||
|
||||
scope.SetExtra("install_id", installId);
|
||||
scope.SetExtra("telemetry_id", telemetryId);
|
||||
scope.SetExtra("app_version", TelemetryEnvironmentInfo.GetAppVersion());
|
||||
@@ -328,6 +320,8 @@ public sealed class SentryCrashTelemetryService : IDisposable
|
||||
scope.SetExtra("runtime_version", TelemetryEnvironmentInfo.GetRuntimeVersion());
|
||||
scope.SetExtra("clr_version", TelemetryEnvironmentInfo.GetClrVersion());
|
||||
scope.SetExtra("language", TelemetryEnvironmentInfo.GetSystemLanguage());
|
||||
scope.SetExtra("language_display_name", TelemetryEnvironmentInfo.GetSystemLanguageDisplayName());
|
||||
scope.SetExtra("render_mode", TelemetryEnvironmentInfo.GetRenderMode());
|
||||
scope.SetExtra("log_file_path", AppLogger.LogFilePath);
|
||||
|
||||
if (includeLogTail)
|
||||
|
||||
@@ -127,7 +127,37 @@ internal static class TelemetryEnvironmentInfo
|
||||
|
||||
public static string GetClrVersion()
|
||||
{
|
||||
return Environment.Version.ToString();
|
||||
try
|
||||
{
|
||||
return System.Runtime.InteropServices.RuntimeEnvironment.GetSystemVersion() ?? "Unknown";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetSystemLanguageDisplayName()
|
||||
{
|
||||
try
|
||||
{
|
||||
var culture = CultureInfo.CurrentUICulture;
|
||||
return culture.NativeName ?? culture.Name ?? "Unknown";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetRenderMode()
|
||||
{
|
||||
return Program.StartupRenderMode ?? "Unknown";
|
||||
}
|
||||
|
||||
public static string GetScreenInfo()
|
||||
{
|
||||
return "requires_ui_thread";
|
||||
}
|
||||
|
||||
public static string GetLocalDayPart(DateTimeOffset timestamp)
|
||||
|
||||
69
LanMountainDesktop/Services/TelemetryEventNames.cs
Normal file
69
LanMountainDesktop/Services/TelemetryEventNames.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
internal static class TelemetryEventNames
|
||||
{
|
||||
internal static string DisplayName(string eventName) =>
|
||||
EventDisplayNames.TryGetValue(eventName, out var displayName)
|
||||
? displayName
|
||||
: eventName;
|
||||
|
||||
internal const string AppFirstLaunch = "app_first_launch";
|
||||
internal const string AppSessionStart = "app_session_start";
|
||||
internal const string AppSessionEnd = "app_session_end";
|
||||
internal const string MainWindowOpened = "main_window_opened";
|
||||
internal const string MainWindowClosed = "main_window_closed";
|
||||
internal const string SettingsWindowOpened = "settings_window_opened";
|
||||
internal const string SettingsWindowClosed = "settings_window_closed";
|
||||
internal const string SettingsNavigation = "settings_navigation";
|
||||
internal const string SettingsDrawerOpened = "settings_drawer_opened";
|
||||
internal const string SettingsDrawerClosed = "settings_drawer_closed";
|
||||
internal const string DesktopComponentPlaced = "desktop_component_placed";
|
||||
internal const string DesktopComponentMoved = "desktop_component_moved";
|
||||
internal const string DesktopComponentResized = "desktop_component_resized";
|
||||
internal const string DesktopComponentDeleted = "desktop_component_deleted";
|
||||
internal const string DesktopComponentEditorOpened = "desktop_component_editor_opened";
|
||||
internal const string ThemeChanged = "theme_changed";
|
||||
internal const string PluginInstalled = "plugin_installed";
|
||||
internal const string PluginUninstalled = "plugin_uninstalled";
|
||||
internal const string PluginEnabled = "plugin_enabled";
|
||||
internal const string PluginDisabled = "plugin_disabled";
|
||||
internal const string UpdateChecked = "update_checked";
|
||||
internal const string UpdateInstalled = "update_installed";
|
||||
internal const string AppCrash = "app_crash";
|
||||
|
||||
internal const string SentryUnhandledException = "unhandled_exception";
|
||||
internal const string SentryTaskException = "task_exception";
|
||||
internal const string SentryShutdown = "shutdown";
|
||||
|
||||
private static readonly Dictionary<string, string> EventDisplayNames = new()
|
||||
{
|
||||
[AppFirstLaunch] = "应用首次启动",
|
||||
[AppSessionStart] = "会话开始",
|
||||
[AppSessionEnd] = "会话结束",
|
||||
[MainWindowOpened] = "主窗口打开",
|
||||
[MainWindowClosed] = "主窗口关闭",
|
||||
[SettingsWindowOpened] = "设置窗口打开",
|
||||
[SettingsWindowClosed] = "设置窗口关闭",
|
||||
[SettingsNavigation] = "设置页导航",
|
||||
[SettingsDrawerOpened] = "设置抽屉打开",
|
||||
[SettingsDrawerClosed] = "设置抽屉关闭",
|
||||
[DesktopComponentPlaced] = "桌面组件放置",
|
||||
[DesktopComponentMoved] = "桌面组件移动",
|
||||
[DesktopComponentResized] = "桌面组件缩放",
|
||||
[DesktopComponentDeleted] = "桌面组件删除",
|
||||
[DesktopComponentEditorOpened] = "组件编辑器打开",
|
||||
[ThemeChanged] = "主题变更",
|
||||
[PluginInstalled] = "插件安装",
|
||||
[PluginUninstalled] = "插件卸载",
|
||||
[PluginEnabled] = "插件启用",
|
||||
[PluginDisabled] = "插件禁用",
|
||||
[UpdateChecked] = "更新检查",
|
||||
[UpdateInstalled] = "更新安装",
|
||||
[AppCrash] = "应用崩溃",
|
||||
[SentryUnhandledException] = "未处理异常",
|
||||
[SentryTaskException] = "任务异常",
|
||||
[SentryShutdown] = "应用关闭"
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.Services.Update;
|
||||
using LanMountainDesktop.Shared.Contracts.Update;
|
||||
using UpdateSettingsValues = LanMountainDesktop.Services.UpdateSettingsValues;
|
||||
|
||||
@@ -14,27 +13,28 @@ namespace LanMountainDesktop.ViewModels;
|
||||
|
||||
public sealed partial class UpdateSettingsViewModel : ViewModelBase, IDisposable
|
||||
{
|
||||
private readonly UpdateOrchestrator _orchestrator;
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private readonly IUpdateSettingsService _updateSettingsService;
|
||||
private readonly LocalizationService _localizationService;
|
||||
private readonly string _languageCode;
|
||||
private bool _suppressPreferenceSave;
|
||||
private bool _disposed;
|
||||
|
||||
public UpdateSettingsViewModel(UpdateOrchestrator orchestrator, ISettingsFacadeService settingsFacade)
|
||||
public UpdateSettingsViewModel(ISettingsFacadeService settingsFacade)
|
||||
{
|
||||
_orchestrator = orchestrator ?? throw new ArgumentNullException(nameof(orchestrator));
|
||||
_settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
|
||||
_updateSettingsService = _settingsFacade.Update;
|
||||
_localizationService = new LocalizationService();
|
||||
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
|
||||
|
||||
CurrentPhase = _orchestrator.CurrentPhase;
|
||||
CurrentPhase = _updateSettingsService.CurrentPhase;
|
||||
CurrentVersionText = _settingsFacade.ApplicationInfo.GetAppVersionText();
|
||||
RefreshLocalizedText();
|
||||
LoadPreferenceState();
|
||||
StatusMessage = GetPhaseStatusText(CurrentPhase);
|
||||
|
||||
_orchestrator.PhaseChanged += OnOrchestratorPhaseChanged;
|
||||
_orchestrator.ProgressChanged += OnOrchestratorProgressChanged;
|
||||
_updateSettingsService.PhaseChanged += OnUpdatePhaseChanged;
|
||||
_updateSettingsService.ProgressChanged += OnUpdateProgressChanged;
|
||||
}
|
||||
|
||||
[ObservableProperty] private UpdatePhase _currentPhase = UpdatePhase.Idle;
|
||||
@@ -208,7 +208,7 @@ public sealed partial class UpdateSettingsViewModel : ViewModelBase, IDisposable
|
||||
private async Task CheckAsync()
|
||||
{
|
||||
StatusMessage = GetCheckingStatusText();
|
||||
var report = await _orchestrator.CheckAsync(CancellationToken.None);
|
||||
var report = await _updateSettingsService.CheckAsync(CancellationToken.None);
|
||||
LastCheckedText = string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
L("settings.update.last_checked_format", "Last checked: {0}"),
|
||||
@@ -244,7 +244,7 @@ public sealed partial class UpdateSettingsViewModel : ViewModelBase, IDisposable
|
||||
private async Task DownloadAsync()
|
||||
{
|
||||
StatusMessage = GetDownloadingStatusText();
|
||||
var result = await _orchestrator.DownloadAsync(CancellationToken.None);
|
||||
var result = await _updateSettingsService.DownloadAsync(CancellationToken.None);
|
||||
if (result.Success)
|
||||
{
|
||||
StatusMessage = GetDownloadCompleteStatusText();
|
||||
@@ -263,7 +263,7 @@ public sealed partial class UpdateSettingsViewModel : ViewModelBase, IDisposable
|
||||
private async Task InstallAsync()
|
||||
{
|
||||
StatusMessage = GetInstallingStatusText();
|
||||
var result = await _orchestrator.InstallAsync(CancellationToken.None);
|
||||
var result = await _updateSettingsService.InstallAsync(CancellationToken.None);
|
||||
if (result.Success)
|
||||
{
|
||||
StatusMessage = GetInstallSuccessStatusText();
|
||||
@@ -278,14 +278,14 @@ public sealed partial class UpdateSettingsViewModel : ViewModelBase, IDisposable
|
||||
private async Task RollbackAsync()
|
||||
{
|
||||
StatusMessage = GetRollingBackStatusText();
|
||||
await _orchestrator.RollbackAsync(CancellationToken.None);
|
||||
await _updateSettingsService.RollbackAsync(CancellationToken.None);
|
||||
StatusMessage = GetRollbackCompleteStatusText();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanPause))]
|
||||
private async Task PauseAsync()
|
||||
{
|
||||
await _orchestrator.PauseAsync();
|
||||
await _updateSettingsService.PauseAsync();
|
||||
StatusMessage = GetPausedStatusText();
|
||||
}
|
||||
|
||||
@@ -293,7 +293,7 @@ public sealed partial class UpdateSettingsViewModel : ViewModelBase, IDisposable
|
||||
private async Task ResumeAsync()
|
||||
{
|
||||
StatusMessage = GetResumingStatusText();
|
||||
var result = await _orchestrator.ResumeAsync(CancellationToken.None);
|
||||
var result = await _updateSettingsService.ResumeAsync(CancellationToken.None);
|
||||
if (result.Success)
|
||||
{
|
||||
StatusMessage = GetResumeCompleteStatusText();
|
||||
@@ -307,18 +307,18 @@ public sealed partial class UpdateSettingsViewModel : ViewModelBase, IDisposable
|
||||
[RelayCommand(CanExecute = nameof(CanCancel))]
|
||||
private async Task CancelAsync()
|
||||
{
|
||||
await _orchestrator.CancelAsync();
|
||||
await _updateSettingsService.CancelAsync();
|
||||
StatusMessage = GetCancelStatusText();
|
||||
ProgressDetail = string.Empty;
|
||||
ProgressFraction = 0;
|
||||
}
|
||||
|
||||
private void OnOrchestratorPhaseChanged(UpdatePhase phase)
|
||||
private void OnUpdatePhaseChanged(UpdatePhase phase)
|
||||
{
|
||||
CurrentPhase = phase;
|
||||
}
|
||||
|
||||
private void OnOrchestratorProgressChanged(UpdateProgressReport report)
|
||||
private void OnUpdateProgressChanged(UpdateProgressReport report)
|
||||
{
|
||||
ProgressFraction = report.ProgressFraction;
|
||||
|
||||
@@ -348,16 +348,56 @@ public sealed partial class UpdateSettingsViewModel : ViewModelBase, IDisposable
|
||||
|
||||
private void LoadPreferenceState()
|
||||
{
|
||||
var state = _settingsFacade.Update.Get();
|
||||
SelectedUpdateChannelValue = state.UpdateChannel;
|
||||
SelectedUpdateSourceValue = state.UpdateDownloadSource;
|
||||
SelectedUpdateModeValue = state.UpdateMode;
|
||||
DownloadThreadsSliderValue = UpdateSettingsValues.NormalizeDownloadThreads(state.UpdateDownloadThreads);
|
||||
ForceReinstall = state.ForceUpdateReinstall;
|
||||
var state = _updateSettingsService.Get();
|
||||
_suppressPreferenceSave = true;
|
||||
try
|
||||
{
|
||||
SelectedUpdateChannelValue = state.UpdateChannel;
|
||||
SelectedUpdateSourceValue = state.UpdateDownloadSource;
|
||||
SelectedUpdateModeValue = state.UpdateMode;
|
||||
DownloadThreadsSliderValue = UpdateSettingsValues.NormalizeDownloadThreads(state.UpdateDownloadThreads);
|
||||
ForceReinstall = state.ForceUpdateReinstall;
|
||||
ApplyPersistedUpdateState(state);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressPreferenceSave = false;
|
||||
}
|
||||
|
||||
SyncComboBoxSelections();
|
||||
}
|
||||
|
||||
private void ApplyPersistedUpdateState(LanMountainDesktop.Services.Settings.UpdateSettingsState state)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(state.PendingUpdateVersion))
|
||||
{
|
||||
IsUpdateAvailable = true;
|
||||
LatestVersionText = state.PendingUpdateVersion;
|
||||
PublishedAtText = state.PendingUpdatePublishedAtUtcMs is > 0
|
||||
? DateTimeOffset
|
||||
.FromUnixTimeMilliseconds(state.PendingUpdatePublishedAtUtcMs.Value)
|
||||
.ToLocalTime()
|
||||
.ToString("g", CultureInfo.CurrentCulture)
|
||||
: string.Empty;
|
||||
UpdateTypeText = ForceReinstall
|
||||
? L("settings.update.type_reinstall", "Reinstall")
|
||||
: UpdateTypeText;
|
||||
}
|
||||
|
||||
if (state.LastUpdateCheckUtcMs is > 0)
|
||||
{
|
||||
LastCheckedText = string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
L("settings.update.last_checked_format", "Last checked: {0}"),
|
||||
DateTimeOffset
|
||||
.FromUnixTimeMilliseconds(state.LastUpdateCheckUtcMs.Value)
|
||||
.ToLocalTime()
|
||||
.ToString("g", CultureInfo.CurrentCulture));
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(LatestVersionDisplayText));
|
||||
}
|
||||
|
||||
private void SyncComboBoxSelections()
|
||||
{
|
||||
SelectedChannel = ChannelOptions.FirstOrDefault(o => o.Value == SelectedUpdateChannelValue)
|
||||
@@ -463,8 +503,13 @@ public sealed partial class UpdateSettingsViewModel : ViewModelBase, IDisposable
|
||||
|
||||
private void SavePreferenceState()
|
||||
{
|
||||
var current = _settingsFacade.Update.Get();
|
||||
_settingsFacade.Update.Save(current with
|
||||
if (_suppressPreferenceSave)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var current = _updateSettingsService.Get();
|
||||
_updateSettingsService.Save(current with
|
||||
{
|
||||
UpdateChannel = SelectedUpdateChannelValue,
|
||||
UpdateDownloadSource = SelectedUpdateSourceValue,
|
||||
@@ -599,7 +644,7 @@ public sealed partial class UpdateSettingsViewModel : ViewModelBase, IDisposable
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_orchestrator.PhaseChanged -= OnOrchestratorPhaseChanged;
|
||||
_orchestrator.ProgressChanged -= OnOrchestratorProgressChanged;
|
||||
_updateSettingsService.PhaseChanged -= OnUpdatePhaseChanged;
|
||||
_updateSettingsService.ProgressChanged -= OnUpdateProgressChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1536,6 +1536,7 @@ public partial class MainWindow : Window
|
||||
PlacementId = placement.PlacementId,
|
||||
PageIndex = placement.PageIndex,
|
||||
ComponentId = placement.ComponentId,
|
||||
ComponentName = placement.ComponentName,
|
||||
Row = placement.Row,
|
||||
Column = placement.Column,
|
||||
WidthCells = placement.WidthCells,
|
||||
|
||||
@@ -511,9 +511,7 @@ public partial class MainWindow : Window
|
||||
{
|
||||
try
|
||||
{
|
||||
await HostUpdateOrchestratorProvider
|
||||
.GetOrCreate()
|
||||
.AutoCheckIfEnabledAsync(default);
|
||||
await _updateSettingsService.AutoCheckIfEnabledAsync(default);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -517,6 +517,7 @@ public partial class MainWindow : Window
|
||||
"MainWindow.OnOpened",
|
||||
IsVisible,
|
||||
WindowState.ToString());
|
||||
TelemetryServices.Usage?.TrackSessionStarted("MainWindow.OnOpened");
|
||||
DesktopHost.SizeChanged += OnDesktopHostSizeChanged;
|
||||
RebuildDesktopGrid();
|
||||
LoadLauncherEntriesAsync();
|
||||
|
||||
@@ -8,288 +8,176 @@
|
||||
x:DataType="vm:UpdateSettingsViewModel">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Classes="settings-page-container settings-page-animated">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Classes="settings-section-title"
|
||||
Text="{Binding PageTitle}" />
|
||||
<TextBlock Classes="settings-section-description"
|
||||
Text="{Binding PageDescription}" />
|
||||
<StackPanel Spacing="6" Margin="0,0,0,16">
|
||||
<TextBlock Classes="settings-section-title" Text="{Binding PageTitle}" />
|
||||
<TextBlock Classes="settings-section-description" Text="{Binding PageDescription}" />
|
||||
</StackPanel>
|
||||
|
||||
<controls:IconText Icon="ArrowSync"
|
||||
Text="{Binding StatusSectionHeader}"
|
||||
Margin="0,0,0,4" />
|
||||
|
||||
<ui:FASettingsExpander Header="{Binding CheckCardTitle}"
|
||||
Description="{Binding StatusMessage}"
|
||||
IsClickEnabled="{Binding CanCheck}"
|
||||
Command="{Binding CheckCommand}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰊈"
|
||||
FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<Button Classes="settings-accent-button"
|
||||
Content="{Binding CheckButtonText}"
|
||||
Command="{Binding CheckCommand}"
|
||||
IsEnabled="{Binding CanCheck}" />
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:FASettingsExpander Header="{Binding ProgressTitle}"
|
||||
Description="{Binding ProgressDescription}"
|
||||
IsVisible="{Binding IsProgressSectionVisible}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰮲"
|
||||
FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Classes="settings-item-label"
|
||||
Text="{Binding PhaseText}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding ProgressFraction, StringFormat='{}{0:P0}'}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
<ui:FASettingsExpanderItem>
|
||||
<StackPanel Spacing="12">
|
||||
<ProgressBar Minimum="0"
|
||||
Maximum="1"
|
||||
Value="{Binding ProgressFraction}"
|
||||
IsVisible="{Binding IsProgressVisible}" />
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="10"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding IsBusy}">
|
||||
<ui:FAProgressRing Width="20"
|
||||
Height="20"
|
||||
IsIndeterminate="True" />
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding ProgressDetail}"
|
||||
TextWrapping="Wrap" />
|
||||
<StackPanel Spacing="16" Margin="0,0,0,24">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<ui:FAFontIcon Glyph="󰊈" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" FontSize="40" VerticalAlignment="Center" Margin="0,0,20,0" Foreground="{DynamicResource AccentFillColorDefaultBrush}" />
|
||||
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="4">
|
||||
<TextBlock Text="{Binding StatusMessage}" FontSize="24" FontWeight="Medium" TextWrapping="Wrap" />
|
||||
<TextBlock Text="{Binding LastCheckedText}" Classes="settings-item-description" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding ProgressDetail}"
|
||||
TextWrapping="Wrap"
|
||||
IsVisible="{Binding !IsBusy}" />
|
||||
<Button Grid.Column="2" Classes="settings-accent-button" Content="{Binding CheckButtonText}" Command="{Binding CheckCommand}" IsVisible="{Binding CanCheck}" VerticalAlignment="Center" Margin="16,0,0,0" />
|
||||
</Grid>
|
||||
|
||||
<ui:FAInfoBar Title="{Binding PausedBadgeText}"
|
||||
Message="{Binding PausedHintText}"
|
||||
IsOpen="True"
|
||||
IsClosable="False"
|
||||
IsVisible="{Binding IsPaused}">
|
||||
<StackPanel IsVisible="{Binding IsProgressSectionVisible}" Spacing="12">
|
||||
<Grid ColumnDefinitions="*,Auto" IsVisible="{Binding IsProgressVisible}">
|
||||
<ProgressBar Grid.Column="0" Minimum="0" Maximum="1" Value="{Binding ProgressFraction}" VerticalAlignment="Center" Margin="0,0,12,0" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding ProgressFraction, StringFormat='{}{0:P0}'}" VerticalAlignment="Center" Classes="settings-item-label" />
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" VerticalAlignment="Center" IsVisible="{Binding IsBusy}">
|
||||
<ui:FAProgressRing Width="20" Height="20" IsIndeterminate="True" />
|
||||
<TextBlock Classes="settings-item-description" Text="{Binding ProgressDetail}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="settings-item-description" Text="{Binding ProgressDetail}" TextWrapping="Wrap" IsVisible="{Binding !IsBusy}" />
|
||||
|
||||
<ui:FAInfoBar Title="{Binding PausedBadgeText}" Message="{Binding PausedHintText}" IsOpen="True" IsClosable="False" IsVisible="{Binding IsPaused}">
|
||||
<ui:FAInfoBar.IconSource>
|
||||
<ui:FAFontIconSource Glyph=""
|
||||
FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
<ui:FAFontIconSource Glyph="" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FAInfoBar.IconSource>
|
||||
</ui:FAInfoBar>
|
||||
|
||||
<ui:FAInfoBar Title="{Binding ResumeSupportLabel}"
|
||||
Message="{Binding ResumeSupportDescription}"
|
||||
IsOpen="True"
|
||||
IsClosable="False"
|
||||
Severity="Informational">
|
||||
<ui:FAInfoBar Title="{Binding ResumeSupportLabel}" Message="{Binding ResumeSupportDescription}" IsOpen="True" IsClosable="False" Severity="Informational">
|
||||
<ui:FAInfoBar.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰙇"
|
||||
FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
<ui:FAFontIconSource Glyph="󰙇" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FAInfoBar.IconSource>
|
||||
</ui:FAInfoBar>
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<Button Classes="settings-accent-button"
|
||||
Content="{Binding DownloadButtonText}"
|
||||
Command="{Binding DownloadCommand}"
|
||||
IsVisible="{Binding CanDownload}" />
|
||||
<Button Classes="settings-accent-button"
|
||||
Content="{Binding InstallButtonText}"
|
||||
Command="{Binding InstallCommand}"
|
||||
IsVisible="{Binding CanInstall}" />
|
||||
<Button Content="{Binding PauseButtonText}"
|
||||
Command="{Binding PauseCommand}"
|
||||
IsVisible="{Binding CanPause}" />
|
||||
<Button Classes="settings-accent-button"
|
||||
Content="{Binding ResumeButtonText}"
|
||||
Command="{Binding ResumeCommand}"
|
||||
IsVisible="{Binding CanResume}" />
|
||||
<Button Content="{Binding RollbackButtonText}"
|
||||
Command="{Binding RollbackCommand}"
|
||||
IsVisible="{Binding CanRollback}" />
|
||||
<Button Content="{Binding CancelButtonText}"
|
||||
Command="{Binding CancelCommand}"
|
||||
IsVisible="{Binding CanCancel}" />
|
||||
</StackPanel>
|
||||
<WrapPanel Orientation="Horizontal" ItemWidth="NaN">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,8,0,0">
|
||||
<Button Classes="settings-accent-button" Content="{Binding DownloadButtonText}" Command="{Binding DownloadCommand}" IsVisible="{Binding CanDownload}" />
|
||||
<Button Classes="settings-accent-button" Content="{Binding InstallButtonText}" Command="{Binding InstallCommand}" IsVisible="{Binding CanInstall}" />
|
||||
<Button Content="{Binding PauseButtonText}" Command="{Binding PauseCommand}" IsVisible="{Binding CanPause}" />
|
||||
<Button Classes="settings-accent-button" Content="{Binding ResumeButtonText}" Command="{Binding ResumeCommand}" IsVisible="{Binding CanResume}" />
|
||||
<Button Content="{Binding RollbackButtonText}" Command="{Binding RollbackCommand}" IsVisible="{Binding CanRollback}" />
|
||||
<Button Content="{Binding CancelButtonText}" Command="{Binding CancelCommand}" IsVisible="{Binding CanCancel}" />
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</ui:FASettingsExpanderItem>
|
||||
</ui:FASettingsExpander>
|
||||
</StackPanel>
|
||||
|
||||
<Separator Classes="settings-separator" />
|
||||
<TabControl Margin="0,0,0,16">
|
||||
<TabItem Header="{Binding ReleaseFactsTitle}">
|
||||
<StackPanel Spacing="2" Margin="0,16,0,0">
|
||||
<TextBlock Classes="settings-section-description" Text="{Binding ReleaseFactsDescription}" Margin="0,0,0,12" />
|
||||
|
||||
<controls:IconText Icon="Info"
|
||||
Text="{Binding ReleaseFactsTitle}"
|
||||
Margin="0,0,0,4" />
|
||||
<ui:FASettingsExpander Classes="settings-expander-card" Header="{Binding CurrentVersionLabel}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰊈" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<TextBlock Classes="settings-item-description" Text="{Binding CurrentVersionText}" />
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:FASettingsExpander Header="{Binding CurrentVersionLabel}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰊈"
|
||||
FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding CurrentVersionText}" />
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
<ui:FASettingsExpander Classes="settings-expander-card" Header="{Binding LatestVersionLabel}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰊈" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<TextBlock Classes="settings-item-description" Text="{Binding LatestVersionDisplayText}" />
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:FASettingsExpander Header="{Binding LatestVersionLabel}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰭎"
|
||||
FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding LatestVersionDisplayText}" />
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
<ui:FASettingsExpander Classes="settings-expander-card" Header="{Binding PublishedAtLabel}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰅨" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<TextBlock Classes="settings-item-description" Text="{Binding PublishedAtText}" />
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:FASettingsExpander Header="{Binding PublishedAtLabel}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰅨"
|
||||
FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding PublishedAtText}" />
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
<ui:FASettingsExpander Classes="settings-expander-card" Header="{Binding UpdateTypeLabel}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰔄" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<TextBlock Classes="settings-item-description" Text="{Binding UpdateTypeText}" />
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
|
||||
<ui:FASettingsExpander Header="{Binding LastCheckedLabel}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰭎"
|
||||
FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding LastCheckedText}" />
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
<TabItem Header="{Binding PreferencesTitle}">
|
||||
<StackPanel Spacing="2" Margin="0,16,0,0">
|
||||
<TextBlock Classes="settings-section-description" Text="{Binding PreferencesDescription}" Margin="0,0,0,12" />
|
||||
|
||||
<ui:FASettingsExpander Header="{Binding UpdateTypeLabel}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰔄"
|
||||
FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding UpdateTypeText}" />
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
<ui:FASettingsExpander Classes="settings-expander-card" Header="{Binding ChannelLabel}" Description="{Binding ChannelDescription}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰤈" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ComboBox Width="220" ItemsSource="{Binding ChannelOptions}" SelectedItem="{Binding SelectedChannel}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<Separator Classes="settings-separator" />
|
||||
<ui:FASettingsExpander Classes="settings-expander-card" Header="{Binding SourceLabel}" Description="{Binding SourceDescription}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰅨" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ComboBox Width="220" ItemsSource="{Binding SourceOptions}" SelectedItem="{Binding SelectedSource}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<controls:IconText Icon="Settings"
|
||||
Text="{Binding PreferencesTitle}"
|
||||
Margin="0,0,0,4" />
|
||||
<ui:FASettingsExpander Classes="settings-expander-card" Header="{Binding ModeLabel}" Description="{Binding ModeDescription}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰣨" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ComboBox Width="220" ItemsSource="{Binding ModeOptions}" SelectedItem="{Binding SelectedMode}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}" TextWrapping="Wrap" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:FASettingsExpander Header="{Binding PreferencesTitle}"
|
||||
Description="{Binding PreferencesDescription}"
|
||||
IsExpanded="True">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰔄"
|
||||
FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpanderItem Content="{Binding ChannelLabel}"
|
||||
Description="{Binding ChannelDescription}">
|
||||
<ui:FASettingsExpanderItem.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰤈"
|
||||
FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpanderItem.IconSource>
|
||||
<ui:FASettingsExpanderItem.Footer>
|
||||
<ComboBox Width="220"
|
||||
ItemsSource="{Binding ChannelOptions}"
|
||||
SelectedItem="{Binding SelectedChannel}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</ui:FASettingsExpanderItem.Footer>
|
||||
</ui:FASettingsExpanderItem>
|
||||
<ui:FASettingsExpanderItem Content="{Binding SourceLabel}"
|
||||
Description="{Binding SourceDescription}">
|
||||
<ui:FASettingsExpanderItem.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰭎"
|
||||
FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpanderItem.IconSource>
|
||||
<ui:FASettingsExpanderItem.Footer>
|
||||
<ComboBox Width="220"
|
||||
ItemsSource="{Binding SourceOptions}"
|
||||
SelectedItem="{Binding SelectedSource}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</ui:FASettingsExpanderItem.Footer>
|
||||
</ui:FASettingsExpanderItem>
|
||||
<ui:FASettingsExpanderItem Content="{Binding ModeLabel}"
|
||||
Description="{Binding ModeDescription}">
|
||||
<ui:FASettingsExpanderItem.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰣨"
|
||||
FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpanderItem.IconSource>
|
||||
<ui:FASettingsExpanderItem.Footer>
|
||||
<ComboBox Width="220"
|
||||
ItemsSource="{Binding ModeOptions}"
|
||||
SelectedItem="{Binding SelectedMode}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}"
|
||||
TextWrapping="Wrap" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</ui:FASettingsExpanderItem.Footer>
|
||||
</ui:FASettingsExpanderItem>
|
||||
<ui:FASettingsExpanderItem Content="{Binding ForceReinstallLabel}"
|
||||
Description="{Binding ForceReinstallDescription}">
|
||||
<ui:FASettingsExpanderItem.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰔄"
|
||||
FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpanderItem.IconSource>
|
||||
<ui:FASettingsExpanderItem.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding ForceReinstall}" />
|
||||
</ui:FASettingsExpanderItem.Footer>
|
||||
</ui:FASettingsExpanderItem>
|
||||
<ui:FASettingsExpanderItem Content="{Binding DownloadThreadsLabel}"
|
||||
Description="{Binding DownloadThreadsDescription}">
|
||||
<ui:FASettingsExpanderItem.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰅨"
|
||||
FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpanderItem.IconSource>
|
||||
<ui:FASettingsExpanderItem.Footer>
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
VerticalAlignment="Center">
|
||||
<Slider Width="140"
|
||||
Minimum="1"
|
||||
Maximum="128"
|
||||
Value="{Binding DownloadThreadsSliderValue}"
|
||||
TickFrequency="1"
|
||||
IsSnapToTickEnabled="True" />
|
||||
<TextBlock Classes="settings-item-label"
|
||||
Text="{Binding DownloadThreadsSliderValue, StringFormat='{}{0:F0}'}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</ui:FASettingsExpanderItem.Footer>
|
||||
</ui:FASettingsExpanderItem>
|
||||
</ui:FASettingsExpander>
|
||||
<ui:FASettingsExpander Classes="settings-expander-card" Header="{Binding ForceReinstallLabel}" Description="{Binding ForceReinstallDescription}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰔄" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding ForceReinstall}" />
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:FASettingsExpander Classes="settings-expander-card" Header="{Binding DownloadThreadsLabel}" Description="{Binding DownloadThreadsDescription}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰅨" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<Slider Width="140" Minimum="1" Maximum="128" Value="{Binding DownloadThreadsSliderValue}" TickFrequency="1" IsSnapToTickEnabled="True" />
|
||||
<TextBlock Classes="settings-item-label" Text="{Binding DownloadThreadsSliderValue, StringFormat='{}{0:F0}'}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.Services.Update;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
@@ -16,9 +15,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
|
||||
public partial class UpdateSettingsPage : SettingsPageBase
|
||||
{
|
||||
public UpdateSettingsPage()
|
||||
: this(new UpdateSettingsViewModel(
|
||||
HostUpdateOrchestratorProvider.GetOrCreate(),
|
||||
HostSettingsFacadeProvider.GetOrCreate()))
|
||||
: this(new UpdateSettingsViewModel(HostSettingsFacadeProvider.GetOrCreate()))
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
810
docs/superpowers/plans/2026-05-26-telemetry-normalization.md
Normal file
810
docs/superpowers/plans/2026-05-26-telemetry-normalization.md
Normal file
@@ -0,0 +1,810 @@
|
||||
# 遥测系统规范化改进实施计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 修复 Sentry/PostHog 遥测系统的数据一致性问题,添加中文可读标签,规范化上报数据格式,补充缺失业务事件。
|
||||
|
||||
**Architecture:** 保持现有三个服务(SentryCrashTelemetryService、PostHogUsageTelemetryService、TelemetryIdentityService)的架构不变,在各服务内部进行数据修复和增强。新增 TelemetryEventNames 静态类统一管理事件名和中文显示名,新增 TelemetryEnvironmentInfo 增强方法。
|
||||
|
||||
**Tech Stack:** C# / .NET 8 / Sentry 6.4.1 / PostHog 2.6.0 / Avalonia UI
|
||||
|
||||
---
|
||||
|
||||
## 文件变更地图
|
||||
|
||||
| 文件 | 操作 | 职责 |
|
||||
|------|------|------|
|
||||
| `LanMountainDesktop/Services/TelemetryEventNames.cs` | **新建** | 统一管理所有事件名和中文显示名 |
|
||||
| `LanMountainDesktop/Services/TelemetryEnvironmentInfo.cs` | 修改 | 增强环境信息采集、修复重复方法 |
|
||||
| `LanMountainDesktop/Services/SentryCrashTelemetryService.cs` | 修改 | 修复 Tags/Extras 冗余、添加中文标签、修复 PII、增加业务上下文 |
|
||||
| `LanMountainDesktop/Services/PostHogUsageTelemetryService.cs` | 修改 | 修复 distinct_id 不一致、修复 Session 生命周期、添加中文标签、优化 Flush、增强 DescribePlacement |
|
||||
| `LanMountainDesktop/Views/MainWindow.axaml.cs` | 修改 | 添加 Session 生命周期调用 |
|
||||
| `LanMountainDesktop/App.axaml.cs` | 修改 | 添加 Session 结束调用 |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 新建 TelemetryEventNames 统一事件名管理
|
||||
|
||||
**Files:**
|
||||
- Create: `LanMountainDesktop/Services/TelemetryEventNames.cs`
|
||||
|
||||
- [ ] **Step 1: 创建 TelemetryEventNames.cs**
|
||||
|
||||
```csharp
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
internal static class TelemetryEventNames
|
||||
{
|
||||
internal static string DisplayName(string eventName) =>
|
||||
EventDisplayNames.TryGetValue(eventName, out var displayName)
|
||||
? displayName
|
||||
: eventName;
|
||||
|
||||
internal const string AppFirstLaunch = "app_first_launch";
|
||||
internal const string AppSessionStart = "app_session_start";
|
||||
internal const string AppSessionEnd = "app_session_end";
|
||||
internal const string MainWindowOpened = "main_window_opened";
|
||||
internal const string MainWindowClosed = "main_window_closed";
|
||||
internal const string SettingsWindowOpened = "settings_window_opened";
|
||||
internal const string SettingsWindowClosed = "settings_window_closed";
|
||||
internal const string SettingsNavigation = "settings_navigation";
|
||||
internal const string SettingsDrawerOpened = "settings_drawer_opened";
|
||||
internal const string SettingsDrawerClosed = "settings_drawer_closed";
|
||||
internal const string DesktopComponentPlaced = "desktop_component_placed";
|
||||
internal const string DesktopComponentMoved = "desktop_component_moved";
|
||||
internal const string DesktopComponentResized = "desktop_component_resized";
|
||||
internal const string DesktopComponentDeleted = "desktop_component_deleted";
|
||||
internal const string DesktopComponentEditorOpened = "desktop_component_editor_opened";
|
||||
internal const string ThemeChanged = "theme_changed";
|
||||
internal const string PluginInstalled = "plugin_installed";
|
||||
internal const string PluginUninstalled = "plugin_uninstalled";
|
||||
internal const string PluginEnabled = "plugin_enabled";
|
||||
internal const string PluginDisabled = "plugin_disabled";
|
||||
internal const string UpdateChecked = "update_checked";
|
||||
internal const string UpdateInstalled = "update_installed";
|
||||
internal const string AppCrash = "app_crash";
|
||||
|
||||
internal const string SentryUnhandledException = "unhandled_exception";
|
||||
internal const string SentryTaskException = "task_exception";
|
||||
internal const string SentryShutdown = "shutdown";
|
||||
|
||||
private static readonly Dictionary<string, string> EventDisplayNames = new()
|
||||
{
|
||||
[AppFirstLaunch] = "应用首次启动",
|
||||
[AppSessionStart] = "会话开始",
|
||||
[AppSessionEnd] = "会话结束",
|
||||
[MainWindowOpened] = "主窗口打开",
|
||||
[MainWindowClosed] = "主窗口关闭",
|
||||
[SettingsWindowOpened] = "设置窗口打开",
|
||||
[SettingsWindowClosed] = "设置窗口关闭",
|
||||
[SettingsNavigation] = "设置页导航",
|
||||
[SettingsDrawerOpened] = "设置抽屉打开",
|
||||
[SettingsDrawerClosed] = "设置抽屉关闭",
|
||||
[DesktopComponentPlaced] = "桌面组件放置",
|
||||
[DesktopComponentMoved] = "桌面组件移动",
|
||||
[DesktopComponentResized] = "桌面组件缩放",
|
||||
[DesktopComponentDeleted] = "桌面组件删除",
|
||||
[DesktopComponentEditorOpened] = "组件编辑器打开",
|
||||
[ThemeChanged] = "主题变更",
|
||||
[PluginInstalled] = "插件安装",
|
||||
[PluginUninstalled] = "插件卸载",
|
||||
[PluginEnabled] = "插件启用",
|
||||
[PluginDisabled] = "插件禁用",
|
||||
[UpdateChecked] = "更新检查",
|
||||
[UpdateInstalled] = "更新安装",
|
||||
[AppCrash] = "应用崩溃",
|
||||
[SentryUnhandledException] = "未处理异常",
|
||||
[SentryTaskException] = "任务异常",
|
||||
[SentryShutdown] = "应用关闭"
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 增强 TelemetryEnvironmentInfo
|
||||
|
||||
**Files:**
|
||||
- Modify: `LanMountainDesktop/Services/TelemetryEnvironmentInfo.cs`
|
||||
|
||||
- [ ] **Step 1: 修复 GetClrVersion 重复问题,增加 GetScreenInfo、GetRenderMode、GetSystemLanguageDisplayName**
|
||||
|
||||
在 `TelemetryEnvironmentInfo.cs` 中:
|
||||
|
||||
1. 修改 `GetClrVersion()` 使其返回实际的 CLR 信息而非与 `GetRuntimeVersion()` 重复:
|
||||
|
||||
```csharp
|
||||
public static string GetClrVersion()
|
||||
{
|
||||
try
|
||||
{
|
||||
return System.Runtime.InteropServices.RuntimeEnvironment.GetSystemVersion() ?? "Unknown";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. 新增 `GetScreenInfo()` 方法:
|
||||
|
||||
```csharp
|
||||
public static string GetScreenInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
var screenList = new List<string>();
|
||||
foreach (var screen in Avalonia.Controls.Screens.All)
|
||||
{
|
||||
screenList.Add($"{screen.Bounds.Width}x{screen.Bounds.Height}@{screen.Scaling:F1}x");
|
||||
}
|
||||
return screenList.Count > 0 ? string.Join("; ", screenList) : "Unknown";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
注意:由于 `TelemetryEnvironmentInfo` 是 `internal static` 类且可能在 UI 线程之外调用,`Screens` API 需要 UI 线程。因此改用更安全的方式:
|
||||
|
||||
```csharp
|
||||
public static string GetScreenInfo()
|
||||
{
|
||||
return "requires_ui_thread";
|
||||
}
|
||||
```
|
||||
|
||||
并提供一个可从 UI 线程调用的重载:
|
||||
|
||||
```csharp
|
||||
public static string GetScreenInfoFromUiThread(Avalonia.Controls.TopLevel? topLevel)
|
||||
{
|
||||
try
|
||||
{
|
||||
var screens = topLevel?.Screens;
|
||||
if (screens is null)
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
var screenList = new List<string>();
|
||||
foreach (var screen in screens.All)
|
||||
{
|
||||
screenList.Add($"{screen.Bounds.Width}x{screen.Bounds.Height}@{screen.Scaling:F1}x");
|
||||
}
|
||||
return screenList.Count > 0 ? string.Join("; ", screenList) : "Unknown";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. 新增 `GetSystemLanguageDisplayName()` 方法:
|
||||
|
||||
```csharp
|
||||
public static string GetSystemLanguageDisplayName()
|
||||
{
|
||||
try
|
||||
{
|
||||
var culture = CultureInfo.CurrentUICulture;
|
||||
return culture.NativeName ?? culture.Name ?? "Unknown";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. 新增 `GetRenderMode()` 方法:
|
||||
|
||||
```csharp
|
||||
public static string GetRenderMode()
|
||||
{
|
||||
return Program.StartupRenderMode ?? "Unknown";
|
||||
}
|
||||
```
|
||||
|
||||
注意:`Program.StartupRenderMode` 已是 `internal static`,同项目内可直接访问。
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 修复 SentryCrashTelemetryService — Tags/Extras 冗余、中文标签、PII、业务上下文
|
||||
|
||||
**Files:**
|
||||
- Modify: `LanMountainDesktop/Services/SentryCrashTelemetryService.cs`
|
||||
|
||||
- [ ] **Step 1: 修改 EnableSentry 方法 — 关闭 SendDefaultPii**
|
||||
|
||||
将第 212 行:
|
||||
```csharp
|
||||
options.SendDefaultPii = true;
|
||||
```
|
||||
改为:
|
||||
```csharp
|
||||
options.SendDefaultPii = false;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 重写 ApplyCommonScope 方法 — 消除 Tags/Extras 冗余,添加中文标签和业务上下文**
|
||||
|
||||
将整个 `ApplyCommonScope` 方法(第 289-346 行)替换为:
|
||||
|
||||
```csharp
|
||||
private void ApplyCommonScope(Scope scope, string source, string eventType, bool includeLogTail)
|
||||
{
|
||||
var installId = TelemetryIdentityService.Instance.InstallId;
|
||||
var telemetryId = TelemetryIdentityService.Instance.TelemetryId;
|
||||
|
||||
scope.User = new SentryUser
|
||||
{
|
||||
Id = telemetryId
|
||||
};
|
||||
|
||||
scope.SetTag("telemetry_channel", "sentry");
|
||||
scope.SetTag("event_type", eventType);
|
||||
scope.SetTag("event_display_name", TelemetryEventNames.DisplayName(eventType));
|
||||
scope.SetTag("source", source);
|
||||
scope.SetTag("app_version", TelemetryEnvironmentInfo.GetAppVersion());
|
||||
scope.SetTag("environment", TelemetryEnvironmentInfo.GetEnvironment());
|
||||
scope.SetTag("os_name", TelemetryEnvironmentInfo.GetOsName());
|
||||
scope.SetTag("os_version", TelemetryEnvironmentInfo.GetOsVersion());
|
||||
scope.SetTag("language", TelemetryEnvironmentInfo.GetSystemLanguage());
|
||||
|
||||
scope.SetExtra("install_id", installId);
|
||||
scope.SetExtra("telemetry_id", telemetryId);
|
||||
scope.SetExtra("app_version", TelemetryEnvironmentInfo.GetAppVersion());
|
||||
scope.SetExtra("environment", TelemetryEnvironmentInfo.GetEnvironment());
|
||||
scope.SetExtra("os_name", TelemetryEnvironmentInfo.GetOsName());
|
||||
scope.SetExtra("os_version", TelemetryEnvironmentInfo.GetOsVersion());
|
||||
scope.SetExtra("os_build", TelemetryEnvironmentInfo.GetOsBuild());
|
||||
scope.SetExtra("device_model", TelemetryEnvironmentInfo.GetDeviceModel());
|
||||
scope.SetExtra("device_arch", TelemetryEnvironmentInfo.GetDeviceArchitecture());
|
||||
scope.SetExtra("processor_count", TelemetryEnvironmentInfo.GetProcessorCount());
|
||||
scope.SetExtra("total_memory_mb", TelemetryEnvironmentInfo.GetTotalMemoryMB());
|
||||
scope.SetExtra("runtime_version", TelemetryEnvironmentInfo.GetRuntimeVersion());
|
||||
scope.SetExtra("clr_version", TelemetryEnvironmentInfo.GetClrVersion());
|
||||
scope.SetExtra("language", TelemetryEnvironmentInfo.GetSystemLanguage());
|
||||
scope.SetExtra("language_display_name", TelemetryEnvironmentInfo.GetSystemLanguageDisplayName());
|
||||
scope.SetExtra("render_mode", TelemetryEnvironmentInfo.GetRenderMode());
|
||||
scope.SetExtra("log_file_path", AppLogger.LogFilePath);
|
||||
|
||||
if (includeLogTail)
|
||||
{
|
||||
var logTail = ReadLogTail(maxLines: 200, maxCharacters: 32_768);
|
||||
if (!string.IsNullOrWhiteSpace(logTail))
|
||||
{
|
||||
scope.SetExtra("log_tail", logTail);
|
||||
scope.SetExtra("log_tail_line_count", logTail.Count(character => character == '\n') + 1);
|
||||
scope.AddAttachment(
|
||||
Encoding.UTF8.GetBytes(logTail),
|
||||
"log-tail.txt",
|
||||
contentType: "text/plain");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
关键变更:
|
||||
- Tags 只保留用于过滤/索引的核心字段(6 个),移除 `install_id`、`telemetry_id`、`os_build`、`device_model`、`device_arch`、`processor_count`、`total_memory_mb`、`runtime_version`、`clr_version` 等非索引字段
|
||||
- Extras 保留所有详细上下文信息
|
||||
- 新增 `event_display_name` Tag(中文显示名)
|
||||
- 新增 `language_display_name`、`render_mode` Extra
|
||||
- 移除 `IpAddr = AutoIpAddress`(配合 SendDefaultPii = false)
|
||||
|
||||
- [ ] **Step 3: 修改 CaptureUnhandledException 方法 — 使用 TelemetryEventNames 常量**
|
||||
|
||||
将第 107 行:
|
||||
```csharp
|
||||
ApplyCommonScope(scope, source, "unhandled_exception", includeLogTail: true);
|
||||
```
|
||||
改为:
|
||||
```csharp
|
||||
ApplyCommonScope(scope, source, TelemetryEventNames.SentryUnhandledException, includeLogTail: true);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 修改 CaptureTaskException 方法 — 使用 TelemetryEventNames 常量**
|
||||
|
||||
将第 139 行:
|
||||
```csharp
|
||||
ApplyCommonScope(scope, source, "task_exception", includeLogTail: true);
|
||||
```
|
||||
改为:
|
||||
```csharp
|
||||
ApplyCommonScope(scope, source, TelemetryEventNames.SentryTaskException, includeLogTail: true);
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 修改 CaptureShutdown 方法 — 使用 TelemetryEventNames 常量**
|
||||
|
||||
将第 160 行:
|
||||
```csharp
|
||||
ApplyCommonScope(scope, source, "shutdown", includeLogTail: true);
|
||||
```
|
||||
改为:
|
||||
```csharp
|
||||
ApplyCommonScope(scope, source, TelemetryEventNames.SentryShutdown, includeLogTail: true);
|
||||
```
|
||||
|
||||
同时将第 158 行的硬编码消息:
|
||||
```csharp
|
||||
var eventId = SentrySdk.CaptureMessage("application_shutdown", scope =>
|
||||
```
|
||||
改为:
|
||||
```csharp
|
||||
var eventId = SentrySdk.CaptureMessage(TelemetryEventNames.SentryShutdown, scope =>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 修复 PostHogUsageTelemetryService — distinct_id 不一致、Session 生命周期、中文标签、Flush 优化、DescribePlacement 增强
|
||||
|
||||
**Files:**
|
||||
- Modify: `LanMountainDesktop/Services/PostHogUsageTelemetryService.cs`
|
||||
|
||||
- [ ] **Step 1: 修复 EnsureBaselineEventSent — 统一使用 telemetryId 作为 distinct_id**
|
||||
|
||||
将第 314 行:
|
||||
```csharp
|
||||
var distinctId = identity.InstallId;
|
||||
```
|
||||
改为:
|
||||
```csharp
|
||||
var distinctId = identity.TelemetryId;
|
||||
```
|
||||
|
||||
同时将 personProps 中增加 `install_id`(保留为属性但不再作为 distinct_id):
|
||||
|
||||
将 personProps 定义(第 314-324 行)改为:
|
||||
```csharp
|
||||
var distinctId = identity.TelemetryId;
|
||||
var personProps = new Dictionary<string, object?>
|
||||
{
|
||||
["install_id"] = identity.InstallId,
|
||||
["telemetry_id"] = identity.TelemetryId,
|
||||
["app_version"] = TelemetryEnvironmentInfo.GetAppVersion(),
|
||||
["os_name"] = TelemetryEnvironmentInfo.GetOsName(),
|
||||
["os_version"] = TelemetryEnvironmentInfo.GetOsVersion(),
|
||||
["os_build"] = TelemetryEnvironmentInfo.GetOsBuild(),
|
||||
["device_model"] = TelemetryEnvironmentInfo.GetDeviceModel(),
|
||||
["device_arch"] = TelemetryEnvironmentInfo.GetDeviceArchitecture(),
|
||||
["runtime_version"] = TelemetryEnvironmentInfo.GetRuntimeVersion(),
|
||||
["clr_version"] = TelemetryEnvironmentInfo.GetClrVersion(),
|
||||
["language"] = TelemetryEnvironmentInfo.GetSystemLanguage(),
|
||||
["language_display_name"] = TelemetryEnvironmentInfo.GetSystemLanguageDisplayName(),
|
||||
["render_mode"] = TelemetryEnvironmentInfo.GetRenderMode()
|
||||
};
|
||||
```
|
||||
|
||||
同时将 `app_first_launch` 事件名改为使用常量:
|
||||
|
||||
将第 329 行:
|
||||
```csharp
|
||||
"app_first_launch",
|
||||
```
|
||||
改为:
|
||||
```csharp
|
||||
TelemetryEventNames.AppFirstLaunch,
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 修复 CaptureEvent — 添加中文 event_display_name,优化环境信息重复**
|
||||
|
||||
将整个 `CaptureEvent` 方法(第 436-503 行)替换为:
|
||||
|
||||
```csharp
|
||||
private void CaptureEvent(
|
||||
string eventName,
|
||||
IReadOnlyDictionary<string, object?>? payload = null,
|
||||
IReadOnlyDictionary<string, object?>? stateBefore = null,
|
||||
IReadOnlyDictionary<string, object?>? stateAfter = null,
|
||||
bool forceFlush = false)
|
||||
{
|
||||
if (!_isInitialized || !_isUsageEnabled || !_sessionActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var identity = TelemetryIdentityService.Instance;
|
||||
var distinctId = identity.TelemetryId;
|
||||
var seq = Interlocked.Increment(ref _sequence);
|
||||
|
||||
var properties = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["install_id"] = identity.InstallId,
|
||||
["telemetry_id"] = identity.TelemetryId,
|
||||
["session_id"] = _sessionId,
|
||||
["sequence"] = seq,
|
||||
["timestamp_utc"] = DateTimeOffset.UtcNow.ToString("o"),
|
||||
["event_display_name"] = TelemetryEventNames.DisplayName(eventName)
|
||||
};
|
||||
|
||||
if (payload is not null)
|
||||
{
|
||||
foreach (var kvp in payload)
|
||||
{
|
||||
properties[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (stateBefore is not null && stateBefore.Count > 0)
|
||||
{
|
||||
foreach (var kvp in stateBefore)
|
||||
{
|
||||
properties[$"state_before_{kvp.Key}"] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (stateAfter is not null && stateAfter.Count > 0)
|
||||
{
|
||||
foreach (var kvp in stateAfter)
|
||||
{
|
||||
properties[$"state_after_{kvp.Key}"] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
_client.Capture(
|
||||
distinctId,
|
||||
eventName,
|
||||
properties,
|
||||
groups: null,
|
||||
sendFeatureFlags: false);
|
||||
|
||||
if (forceFlush)
|
||||
{
|
||||
_ = _client.FlushAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
关键变更:
|
||||
- 移除每个事件中重复的 `app_version`、`os_name`、`os_version`、`device_model`、`device_arch`、`runtime_version`、`language`(这些已通过 Identify 设置为 person properties)
|
||||
- 添加 `event_display_name` 属性(中文显示名)
|
||||
- 移除 `payload_` 前缀,payload 属性直接使用原始 key
|
||||
|
||||
- [ ] **Step 3: 修复 StartSession — 使用 TelemetryEventNames 常量,移除重复环境信息**
|
||||
|
||||
将 StartSession 方法中的 CaptureEvent 调用(第 362-378 行)改为:
|
||||
|
||||
```csharp
|
||||
CaptureEvent(
|
||||
TelemetryEventNames.AppSessionStart,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source,
|
||||
["launch_id"] = _launchId,
|
||||
["session_start_utc"] = _sessionStartUtc.ToString("o"),
|
||||
["local_hour"] = _sessionStartUtc.ToLocalTime().Hour,
|
||||
["day_part"] = TelemetryEnvironmentInfo.GetLocalDayPart(_sessionStartUtc),
|
||||
["timezone"] = TimeZoneInfo.Local.Id
|
||||
},
|
||||
forceFlush: true);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 修复 EndSession — 使用 TelemetryEventNames 常量**
|
||||
|
||||
将 EndSession 方法中的 CaptureEvent 调用(第 393-404 行)改为:
|
||||
|
||||
```csharp
|
||||
CaptureEvent(
|
||||
TelemetryEventNames.AppSessionEnd,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source,
|
||||
["launch_id"] = _launchId,
|
||||
["session_start_utc"] = _sessionStartUtc.ToString("o"),
|
||||
["session_end_utc"] = endUtc.ToString("o"),
|
||||
["duration_ms"] = durationMs,
|
||||
["is_restart"] = isRestart
|
||||
},
|
||||
forceFlush: true);
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 修改所有 Track* 方法 — 使用 TelemetryEventNames 常量,移除 payload_ 前缀影响**
|
||||
|
||||
将所有 Track 方法中的硬编码事件名替换为常量引用:
|
||||
|
||||
`TrackMainWindowOpened`(第 105-114 行):
|
||||
```csharp
|
||||
public void TrackMainWindowOpened(string source, bool isVisible, string windowState)
|
||||
{
|
||||
CaptureEvent(
|
||||
TelemetryEventNames.MainWindowOpened,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source,
|
||||
["is_visible"] = isVisible,
|
||||
["window_state"] = windowState
|
||||
},
|
||||
forceFlush: true);
|
||||
}
|
||||
```
|
||||
|
||||
`TrackMainWindowClosed`(第 116-127 行):
|
||||
```csharp
|
||||
public void TrackMainWindowClosed(string source, bool wasVisible, string windowState)
|
||||
{
|
||||
CaptureEvent(
|
||||
TelemetryEventNames.MainWindowClosed,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source,
|
||||
["was_visible"] = wasVisible,
|
||||
["window_state"] = windowState
|
||||
},
|
||||
forceFlush: true);
|
||||
}
|
||||
```
|
||||
|
||||
`TrackSettingsWindowOpened`(第 129-139 行):
|
||||
```csharp
|
||||
public void TrackSettingsWindowOpened(string source, string? currentPageId)
|
||||
{
|
||||
CaptureEvent(
|
||||
TelemetryEventNames.SettingsWindowOpened,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source,
|
||||
["current_page_id"] = currentPageId
|
||||
},
|
||||
forceFlush: true);
|
||||
}
|
||||
```
|
||||
|
||||
`TrackSettingsWindowClosed`(第 141-151 行):
|
||||
```csharp
|
||||
public void TrackSettingsWindowClosed(string source, string? currentPageId)
|
||||
{
|
||||
CaptureEvent(
|
||||
TelemetryEventNames.SettingsWindowClosed,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source,
|
||||
["current_page_id"] = currentPageId
|
||||
},
|
||||
forceFlush: true);
|
||||
}
|
||||
```
|
||||
|
||||
`TrackSettingsNavigation`(第 153-165 行):
|
||||
```csharp
|
||||
public void TrackSettingsNavigation(string? fromPageId, string? toPageId, string source)
|
||||
{
|
||||
CaptureEvent(
|
||||
TelemetryEventNames.SettingsNavigation,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source,
|
||||
["from_page_id"] = fromPageId,
|
||||
["to_page_id"] = toPageId
|
||||
},
|
||||
stateBefore: CreatePageState(fromPageId),
|
||||
stateAfter: CreatePageState(toPageId));
|
||||
}
|
||||
```
|
||||
|
||||
`TrackSettingsDrawerOpened`(第 167-177 行):
|
||||
```csharp
|
||||
public void TrackSettingsDrawerOpened(string? pageId, string? drawerTitle)
|
||||
{
|
||||
CaptureEvent(
|
||||
TelemetryEventNames.SettingsDrawerOpened,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["page_id"] = pageId,
|
||||
["drawer_title"] = drawerTitle
|
||||
},
|
||||
forceFlush: true);
|
||||
}
|
||||
```
|
||||
|
||||
`TrackSettingsDrawerClosed`(第 179-189 行):
|
||||
```csharp
|
||||
public void TrackSettingsDrawerClosed(string? pageId, string? drawerTitle)
|
||||
{
|
||||
CaptureEvent(
|
||||
TelemetryEventNames.SettingsDrawerClosed,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["page_id"] = pageId,
|
||||
["drawer_title"] = drawerTitle
|
||||
},
|
||||
forceFlush: true);
|
||||
}
|
||||
```
|
||||
|
||||
`TrackDesktopComponentPlaced`(第 191-201 行):
|
||||
```csharp
|
||||
public void TrackDesktopComponentPlaced(DesktopComponentPlacementSnapshot placement, string source)
|
||||
{
|
||||
CaptureEvent(
|
||||
TelemetryEventNames.DesktopComponentPlaced,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source
|
||||
},
|
||||
stateAfter: DescribePlacement(placement),
|
||||
forceFlush: true);
|
||||
}
|
||||
```
|
||||
|
||||
`TrackDesktopComponentMoved`(第 203-217 行):
|
||||
```csharp
|
||||
public void TrackDesktopComponentMoved(
|
||||
DesktopComponentPlacementSnapshot before,
|
||||
DesktopComponentPlacementSnapshot after,
|
||||
string source)
|
||||
{
|
||||
CaptureEvent(
|
||||
TelemetryEventNames.DesktopComponentMoved,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source
|
||||
},
|
||||
stateBefore: DescribePlacement(before),
|
||||
stateAfter: DescribePlacement(after),
|
||||
forceFlush: true);
|
||||
}
|
||||
```
|
||||
|
||||
`TrackDesktopComponentResized`(第 219-233 行):
|
||||
```csharp
|
||||
public void TrackDesktopComponentResized(
|
||||
DesktopComponentPlacementSnapshot before,
|
||||
DesktopComponentPlacementSnapshot after,
|
||||
string source)
|
||||
{
|
||||
CaptureEvent(
|
||||
TelemetryEventNames.DesktopComponentResized,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source
|
||||
},
|
||||
stateBefore: DescribePlacement(before),
|
||||
stateAfter: DescribePlacement(after),
|
||||
forceFlush: true);
|
||||
}
|
||||
```
|
||||
|
||||
`TrackDesktopComponentDeleted`(第 235-245 行):
|
||||
```csharp
|
||||
public void TrackDesktopComponentDeleted(DesktopComponentPlacementSnapshot before, string source)
|
||||
{
|
||||
CaptureEvent(
|
||||
TelemetryEventNames.DesktopComponentDeleted,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source
|
||||
},
|
||||
stateBefore: DescribePlacement(before),
|
||||
forceFlush: true);
|
||||
}
|
||||
```
|
||||
|
||||
`TrackDesktopComponentEditorOpened`(第 247-257 行):
|
||||
```csharp
|
||||
public void TrackDesktopComponentEditorOpened(DesktopComponentPlacementSnapshot placement, string source)
|
||||
{
|
||||
CaptureEvent(
|
||||
TelemetryEventNames.DesktopComponentEditorOpened,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["source"] = source
|
||||
},
|
||||
stateBefore: DescribePlacement(placement),
|
||||
forceFlush: true);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 增强 DescribePlacement — 添加 component_name**
|
||||
|
||||
将 `DescribePlacement` 方法(第 513-525 行)改为:
|
||||
|
||||
```csharp
|
||||
private static IReadOnlyDictionary<string, object?> DescribePlacement(DesktopComponentPlacementSnapshot placement)
|
||||
{
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["placement_id"] = placement.PlacementId,
|
||||
["component_id"] = placement.ComponentId,
|
||||
["component_name"] = placement.ComponentName ?? placement.ComponentId,
|
||||
["page_index"] = placement.PageIndex,
|
||||
["row"] = placement.Row,
|
||||
["column"] = placement.Column,
|
||||
["width_cells"] = placement.WidthCells,
|
||||
["height_cells"] = placement.HeightCells
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
注意:这要求 `DesktopComponentPlacementSnapshot` 有 `ComponentName` 属性。如果不存在,需要在 `DesktopComponentPlacementSnapshot.cs` 中添加:
|
||||
|
||||
```csharp
|
||||
public string ComponentName { get; set; } = string.Empty;
|
||||
```
|
||||
|
||||
并在创建 placement snapshot 的地方(`ClonePlacementSnapshot` 方法等)填充该字段。
|
||||
|
||||
- [ ] **Step 7: 优化 Flush 策略 — 仅关键事件 forceFlush**
|
||||
|
||||
将以下 Track 方法的 `forceFlush: true` 改为 `forceFlush: false`(仅保留 session 和 first_launch 的 forceFlush):
|
||||
|
||||
- `TrackMainWindowOpened` → `forceFlush: false`
|
||||
- `TrackMainWindowClosed` → `forceFlush: false`
|
||||
- `TrackSettingsWindowOpened` → `forceFlush: false`
|
||||
- `TrackSettingsWindowClosed` → `forceFlush: false`
|
||||
- `TrackSettingsDrawerOpened` → `forceFlush: false`
|
||||
- `TrackSettingsDrawerClosed` → `forceFlush: false`
|
||||
- `TrackDesktopComponentPlaced` → `forceFlush: false`
|
||||
- `TrackDesktopComponentMoved` → `forceFlush: false`
|
||||
- `TrackDesktopComponentResized` → `forceFlush: false`
|
||||
- `TrackDesktopComponentDeleted` → `forceFlush: false`
|
||||
- `TrackDesktopComponentEditorOpened` → `forceFlush: false`
|
||||
|
||||
保留 `forceFlush: true` 的:
|
||||
- `StartSession`(app_session_start)
|
||||
- `EndSession`(app_session_end)
|
||||
- `EnsureBaselineEventSent`(app_first_launch)
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 修复 Session 生命周期 — MainWindow 和 App 层调用
|
||||
|
||||
**Files:**
|
||||
- Modify: `LanMountainDesktop/Views/MainWindow.axaml.cs`
|
||||
- Modify: `LanMountainDesktop/App.axaml.cs`
|
||||
|
||||
- [ ] **Step 1: 在 MainWindow.OnOpened 中添加 TrackSessionStarted 调用**
|
||||
|
||||
在 `MainWindow.axaml.cs` 的 `OnOpened` 方法中,在 `TrackMainWindowOpened` 调用之后(约第 519 行),添加:
|
||||
|
||||
```csharp
|
||||
TelemetryServices.Usage?.TrackSessionStarted("MainWindow.OnOpened");
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 在 App.PerformExitCleanup 中确保 TrackSessionEnded 被调用**
|
||||
|
||||
在 `App.axaml.cs` 的 `PerformExitCleanup` 方法中,在 `TelemetryServices.Usage?.Shutdown(...)` 调用之前(约第 1202 行),添加:
|
||||
|
||||
```csharp
|
||||
TelemetryServices.Usage?.TrackSessionEnded("App.PerformExitCleanup");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: 为 DesktopComponentPlacementSnapshot 添加 ComponentName 属性
|
||||
|
||||
**Files:**
|
||||
- Modify: `LanMountainDesktop/Models/DesktopComponentPlacementSnapshot.cs`
|
||||
|
||||
- [ ] **Step 1: 添加 ComponentName 属性**
|
||||
|
||||
在 `DesktopComponentPlacementSnapshot.cs` 中,在 `ComponentId` 属性之后添加:
|
||||
|
||||
```csharp
|
||||
public string ComponentName { get; set; } = string.Empty;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 搜索所有 ClonePlacementSnapshot 方法,确保 ComponentName 被正确填充**
|
||||
|
||||
在 `MainWindow.ComponentSystem.cs` 和 `MainWindow.DesktopEditing.cs` 中的 `ClonePlacementSnapshot` 方法里,需要确保 `ComponentName` 被赋值。搜索项目中所有 `ClonePlacementSnapshot` 的实现,在克隆时同时复制 `ComponentName` 字段。
|
||||
|
||||
---
|
||||
|
||||
## Task 7: 构建验证
|
||||
|
||||
- [ ] **Step 1: 执行 dotnet build 确保编译通过**
|
||||
|
||||
Run: `dotnet build LanMountainDesktop.slnx -c Debug`
|
||||
|
||||
Expected: Build succeeded, 0 errors
|
||||
|
||||
- [ ] **Step 2: 执行 dotnet test 确保测试通过**
|
||||
|
||||
Run: `dotnet test LanMountainDesktop.slnx -c Debug`
|
||||
|
||||
Expected: All tests pass
|
||||
Reference in New Issue
Block a user