Merge remote-tracking branch 'origin/main' into Avalonia12

# Conflicts:
#	LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj
This commit is contained in:
lincube
2026-04-29 11:49:48 +08:00
11 changed files with 873 additions and 67 deletions

View File

@@ -39,4 +39,6 @@ namespace LanMountainDesktop.Launcher;
[JsonSerializable(typeof(GitHubAsset))]
[JsonSerializable(typeof(List<GitHubRelease>))]
[JsonSerializable(typeof(StartupAttemptRecord))]
[JsonSerializable(typeof(PrivacyConfig))]
[JsonSerializable(typeof(PrivacyAgreementState))]
internal sealed partial class AppJsonContext : JsonSerializerContext;

View File

@@ -22,12 +22,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" />
<PackageReference Include="Avalonia.Desktop" />
<PackageReference Include="FluentAvaloniaUI" />
<PackageReference Include="FluentIcons.Avalonia" />
<PackageReference Include="Avalonia.Fonts.Inter" />
<PackageReference Include="Tmds.DBus.Protocol" />
<PackageReference Include="Avalonia" Version="11.3.12" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
<PackageReference Include="FluentAvaloniaUI" Version="2.5.0" />
<PackageReference Include="FluentIcons.Avalonia" Version="1.1.250403001" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.12" />
<PackageReference Include="Tmds.DBus.Protocol" Version="0.92.0" />
</ItemGroup>
<!-- 资源文件 -->

View File

@@ -0,0 +1,42 @@
namespace LanMountainDesktop.Launcher.Models;
/// <summary>
/// 隐私协议同意状态模型(带防篡改保护)
/// </summary>
public class PrivacyAgreementState
{
/// <summary>
/// 用户是否同意隐私协议
/// </summary>
public bool IsAgreed { get; set; } = false;
/// <summary>
/// 同意时间UTC
/// </summary>
public DateTime AgreedAtUtc { get; set; }
/// <summary>
/// 同意的协议版本
/// </summary>
public string AgreementVersion { get; set; } = "1.0";
/// <summary>
/// 用户标识(匿名)
/// </summary>
public string UserId { get; set; } = string.Empty;
/// <summary>
/// 设备标识
/// </summary>
public string DeviceId { get; set; } = string.Empty;
/// <summary>
/// 数据完整性校验哈希HMAC-SHA256
/// </summary>
public string IntegrityHash { get; set; } = string.Empty;
/// <summary>
/// 用于生成哈希的随机盐值
/// </summary>
public string Salt { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,22 @@
namespace LanMountainDesktop.Launcher.Models;
/// <summary>
/// 隐私配置模型
/// </summary>
public class PrivacyConfig
{
/// <summary>
/// 是否启用崩溃报告遥测
/// </summary>
public bool CrashTelemetryEnabled { get; set; } = true;
/// <summary>
/// 是否启用使用统计遥测
/// </summary>
public bool UsageTelemetryEnabled { get; set; } = true;
/// <summary>
/// 隐私追踪 ID
/// </summary>
public string TelemetryId { get; set; } = string.Empty;
}

View File

@@ -141,7 +141,9 @@ internal sealed class DataLocationResolver
{
try
{
var configPath = ResolveConfigPath();
// 配置文件必须位于默认系统数据路径下的 Launcher 目录中
// 避免循环依赖:不能调用 ResolveConfigPath() -> ResolveLauncherDataPath() -> ResolveDataRoot() -> LoadConfig()
var configPath = Path.Combine(_defaultSystemDataPath, LauncherFolderName, ConfigFileName);
if (!File.Exists(configPath))
{
return null;

View File

@@ -0,0 +1,245 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using LanMountainDesktop.Launcher.Models;
namespace LanMountainDesktop.Launcher.Services;
/// <summary>
/// 隐私协议同意状态管理服务(带防篡改保护)
/// </summary>
internal sealed class PrivacyAgreementService
{
private readonly string _storagePath;
private readonly string _secretKey;
private const string ConfigFileName = "privacy-agreement.state.json";
private const string CurrentAgreementVersion = "1.0";
public PrivacyAgreementService(string launcherDataPath)
{
_storagePath = Path.Combine(launcherDataPath, ConfigFileName);
// 使用机器特定信息生成密钥,增加篡改难度
_secretKey = GenerateMachineSpecificKey();
}
/// <summary>
/// 检查用户是否已同意隐私协议
/// </summary>
public bool HasUserAgreed()
{
try
{
if (!File.Exists(_storagePath))
{
Logger.Info("[PrivacyAgreementService] 未找到隐私协议同意状态文件");
return false;
}
var json = File.ReadAllText(_storagePath);
var state = JsonSerializer.Deserialize(json, AppJsonContext.Default.PrivacyAgreementState);
if (state == null)
{
Logger.Warn("[PrivacyAgreementService] 无法解析隐私协议状态文件");
return false;
}
// 验证数据完整性
if (!VerifyIntegrity(state))
{
Logger.Warn("[PrivacyAgreementService] 隐私协议状态文件已被篡改!");
// 删除被篡改的文件
try
{
File.Delete(_storagePath);
}
catch (Exception ex)
{
Logger.Warn($"[PrivacyAgreementService] 删除被篡改文件失败: {ex.Message}");
}
return false;
}
// 检查协议版本是否匹配
if (state.AgreementVersion != CurrentAgreementVersion)
{
Logger.Info($"[PrivacyAgreementService] 隐私协议版本已更新: {state.AgreementVersion} -> {CurrentAgreementVersion}");
return false;
}
Logger.Info($"[PrivacyAgreementService] 用户已于 {state.AgreedAtUtc:yyyy-MM-dd HH:mm:ss} UTC 同意隐私协议");
return state.IsAgreed;
}
catch (Exception ex)
{
Logger.Warn($"[PrivacyAgreementService] 检查同意状态时出错: {ex.Message}");
return false;
}
}
/// <summary>
/// 保存用户同意隐私协议的状态
/// </summary>
public bool SaveAgreement(bool isAgreed, string userId, string deviceId)
{
try
{
// 确保目录存在
var directory = Path.GetDirectoryName(_storagePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// 生成随机盐值
var salt = GenerateRandomSalt();
var state = new PrivacyAgreementState
{
IsAgreed = isAgreed,
AgreedAtUtc = DateTime.UtcNow,
AgreementVersion = CurrentAgreementVersion,
UserId = userId,
DeviceId = deviceId,
Salt = salt
};
// 计算完整性哈希
state.IntegrityHash = CalculateIntegrityHash(state);
// 保存到文件
var json = JsonSerializer.Serialize(state, AppJsonContext.Default.PrivacyAgreementState);
File.WriteAllText(_storagePath, json);
Logger.Info($"[PrivacyAgreementService] 隐私协议同意状态已保存: IsAgreed={isAgreed}");
return true;
}
catch (Exception ex)
{
Logger.Warn($"[PrivacyAgreementService] 保存同意状态失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 获取当前的协议版本
/// </summary>
public string GetCurrentAgreementVersion() => CurrentAgreementVersion;
/// <summary>
/// 清除同意状态(用于测试或重置)
/// </summary>
public bool ClearAgreement()
{
try
{
if (File.Exists(_storagePath))
{
File.Delete(_storagePath);
Logger.Info("[PrivacyAgreementService] 隐私协议同意状态已清除");
}
return true;
}
catch (Exception ex)
{
Logger.Warn($"[PrivacyAgreementService] 清除同意状态失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 生成机器特定的密钥
/// </summary>
private string GenerateMachineSpecificKey()
{
try
{
// 组合多个机器特定信息生成密钥
var machineName = Environment.MachineName;
var userName = Environment.UserName;
var osVersion = Environment.OSVersion.Version.ToString();
var processorCount = Environment.ProcessorCount.ToString();
// 使用硬件信息(如果可用)
var hardwareId = GetHardwareIdentifier();
var keyData = $"{machineName}:{userName}:{osVersion}:{processorCount}:{hardwareId}:LanMountainDesktop";
// 使用 SHA-256 生成固定长度的密钥
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(keyData));
return Convert.ToHexString(hash);
}
catch
{
// 如果无法获取机器信息,使用备用密钥
return "LanMountainDesktop-Privacy-Agreement-Fallback-Key-2026";
}
}
/// <summary>
/// 获取硬件标识符
/// </summary>
private string GetHardwareIdentifier()
{
try
{
// 尝试使用系统目录创建时间作为硬件标识的一部分
var systemDir = Environment.SystemDirectory;
var dirInfo = new DirectoryInfo(systemDir);
return dirInfo.CreationTimeUtc.ToString("yyyyMMddHHmmss");
}
catch
{
return "Unknown";
}
}
/// <summary>
/// 生成随机盐值
/// </summary>
private string GenerateRandomSalt()
{
var saltBytes = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(saltBytes);
return Convert.ToHexString(saltBytes);
}
/// <summary>
/// 计算完整性哈希HMAC-SHA256
/// </summary>
private string CalculateIntegrityHash(PrivacyAgreementState state)
{
// 构建需要哈希的数据字符串
var dataToHash = $"{state.IsAgreed}:{state.AgreedAtUtc:o}:{state.AgreementVersion}:{state.UserId}:{state.DeviceId}:{state.Salt}";
// 使用 HMAC-SHA256 计算哈希
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_secretKey));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(dataToHash));
return Convert.ToHexString(hash);
}
/// <summary>
/// 验证数据完整性
/// </summary>
private bool VerifyIntegrity(PrivacyAgreementState state)
{
try
{
if (string.IsNullOrEmpty(state.IntegrityHash) || string.IsNullOrEmpty(state.Salt))
{
return false;
}
var expectedHash = CalculateIntegrityHash(state);
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(state.IntegrityHash),
Encoding.UTF8.GetBytes(expectedHash));
}
catch
{
return false;
}
}
}

View File

@@ -6,13 +6,13 @@
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia"
mc:Ignorable="d"
d:DesignWidth="700"
d:DesignHeight="500"
d:DesignWidth="850"
d:DesignHeight="650"
x:Class="LanMountainDesktop.Launcher.Views.OobeWindow"
x:DataType="views:OobeWindow"
Title="欢迎使用阑山桌面"
Width="700"
Height="500"
Width="850"
Height="650"
CanResize="False"
WindowStartupLocation="CenterScreen"
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
@@ -441,116 +441,137 @@
<!-- 步骤 3: 数据位置选择页面 -->
<Grid x:Name="DataLocationStep" Margin="48" RowDefinitions="Auto,*,Auto" IsVisible="False">
<StackPanel Grid.Row="0" Spacing="8" Margin="0,0,0,24">
<StackPanel Grid.Row="0" Spacing="8" Margin="0,0,0,32">
<TextBlock Text="选择数据保存位置"
FontSize="24"
FontSize="28"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="决定将应用数据保存在哪里,可随时在设置中更改"
FontSize="13"
FontSize="14"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
<StackPanel Grid.Row="1" Spacing="16">
<StackPanel Grid.Row="1" Spacing="20">
<Border x:Name="AdminWarningBanner"
Background="{DynamicResource SystemFillColorCriticalBackgroundBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="12,10"
Padding="16,12"
IsVisible="False">
<StackPanel Spacing="4">
<StackPanel Orientation="Horizontal" Spacing="6">
<PathIcon Data="M12,2 L1,21 L23,21 Z M11,9 L13,9 L13,15 L11,15 Z M11,17 L13,17 L13,19 L11,19 Z"
Width="16"
Height="16"
Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<StackPanel Spacing="6">
<StackPanel Orientation="Horizontal" Spacing="8">
<fi:SymbolIcon Symbol="ShieldError"
FontSize="20"
Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="无法保存到应用目录"
FontWeight="SemiBold"
FontSize="13"
FontSize="14"
Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
</StackPanel>
<TextBlock Text="当前安装目录需要管理员权限才能写入,数据将自动保存到系统用户目录。"
FontSize="12"
FontSize="13"
TextWrapping="Wrap"
Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
</StackPanel>
</Border>
<!-- 系统用户目录选项 -->
<Border x:Name="SystemOptionBorder"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
BorderThickness="2"
BorderBrush="{DynamicResource AccentFillColorDefaultBrush}"
Padding="16,14"
Padding="20,18"
Cursor="Hand">
<Grid ColumnDefinitions="Auto,*">
<RadioButton x:Name="SystemRadio"
Grid.Column="0"
VerticalAlignment="Top"
Margin="0,2,12,0"
VerticalAlignment="Center"
Margin="0,0,16,0"
GroupName="DataLocation"
IsChecked="True" />
<StackPanel Grid.Column="1" Spacing="4">
<TextBlock Text="保存在系统用户目录(推荐)"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="数据与当前 Windows 用户绑定,重装应用或更新后数据不会丢失"
FontSize="12"
<StackPanel Grid.Column="1" Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="8">
<fi:SymbolIcon Symbol="Folder"
FontSize="24"
Foreground="{DynamicResource AccentFillColorDefaultBrush}" />
<TextBlock Text="保存在系统用户目录(推荐)"
FontSize="16"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
</StackPanel>
<TextBlock Text="数据与当前 Windows 用户绑定,重装应用或更新后数据不会丢失。适合大多数用户。"
FontSize="13"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="SystemPathText"
FontSize="11"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Margin="0,4,0,0" />
<Border Background="{DynamicResource SolidBackgroundFillColorQuarternaryBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
Padding="12,8"
Margin="0,4,0,0">
<TextBlock x:Name="SystemPathText"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
FontFamily="Consolas, Monaco, monospace" />
</Border>
</StackPanel>
</Grid>
</Border>
<!-- 便携模式选项 -->
<Border x:Name="PortableOptionBorder"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
BorderThickness="1"
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
Padding="16,14"
Padding="20,18"
Cursor="Hand">
<Grid ColumnDefinitions="Auto,*">
<RadioButton x:Name="PortableRadio"
Grid.Column="0"
VerticalAlignment="Top"
Margin="0,2,12,0"
VerticalAlignment="Center"
Margin="0,0,16,0"
GroupName="DataLocation"
IsEnabled="False" />
<StackPanel Grid.Column="1" Spacing="4">
<TextBlock Text="保存在应用安装目录"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="便于携带,可随应用文件夹整体移动到其他电脑"
FontSize="12"
<StackPanel Grid.Column="1" Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="8">
<fi:SymbolIcon Symbol="Save"
FontSize="24"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock Text="保存在应用安装目录(便携模式)"
FontSize="16"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
</StackPanel>
<TextBlock Text="便于携带,可随应用文件夹整体移动到其他电脑。适合在多台电脑间使用或需要便携运行的场景。"
FontSize="13"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="PortablePathText"
FontSize="11"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Margin="0,4,0,0" />
<Border Background="{DynamicResource SolidBackgroundFillColorQuarternaryBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
Padding="12,8"
Margin="0,4,0,0">
<TextBlock x:Name="PortablePathText"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
FontFamily="Consolas, Monaco, monospace" />
</Border>
</StackPanel>
</Grid>
</Border>
<!-- 数据迁移提示 -->
<Border x:Name="MigrationInfoBorder"
Background="{DynamicResource SystemFillColorSuccessBackgroundBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="12,10"
Padding="16,12"
IsVisible="False">
<StackPanel Orientation="Horizontal" Spacing="6">
<PathIcon Data="M9,16.17 L4.83,12 L3.41,13.41 L9,19 L21,7 L19.59,5.59 Z"
Width="16"
Height="16"
Foreground="{DynamicResource SystemFillColorSuccessBrush}" />
<StackPanel Orientation="Horizontal" Spacing="8">
<fi:SymbolIcon Symbol="Checkmark"
FontSize="20"
Foreground="{DynamicResource SystemFillColorSuccessBrush}" />
<TextBlock x:Name="MigrationInfoText"
FontSize="12"
FontSize="13"
TextWrapping="Wrap"
Foreground="{DynamicResource SystemFillColorSuccessBrush}" />
</StackPanel>
@@ -561,17 +582,156 @@
Orientation="Horizontal"
HorizontalAlignment="Right"
Spacing="12"
Margin="0,24,0,0">
Margin="0,32,0,0">
<Button x:Name="DataLocationBackButton"
Content="返回"
Theme="{DynamicResource ButtonTheme}" />
Theme="{DynamicResource ButtonTheme}"
Width="100"
Height="36" />
<Button x:Name="DataLocationNextButton"
Content="下一步"
Theme="{DynamicResource AccentButtonTheme}"
Width="100"
Height="36" />
</StackPanel>
</Grid>
<!-- 步骤 4: 信息与隐私页面 -->
<Grid x:Name="PrivacyStep" Margin="48" RowDefinitions="Auto,*,Auto" IsVisible="False">
<StackPanel Grid.Row="0" Spacing="8" Margin="0,0,0,24">
<TextBlock Text="信息与隐私"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="选择是否参与遥测计划,查看隐私政策"
FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
<StackPanel Grid.Row="1" Spacing="16">
<!-- 崩溃报告 -->
<Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="16">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="发送匿名崩溃报告"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="帮助改进应用稳定性,不包含个人身份信息"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
<ToggleSwitch x:Name="CrashTelemetryToggle"
Grid.Column="1"
IsChecked="False"
IsEnabled="False"
VerticalAlignment="Center" />
</Grid>
</Border>
<!-- 使用统计 -->
<Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="16">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="发送匿名使用统计"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="帮助了解功能使用情况,优化产品体验"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
<ToggleSwitch x:Name="UsageTelemetryToggle"
Grid.Column="1"
IsChecked="False"
IsEnabled="False"
VerticalAlignment="Center" />
</Grid>
</Border>
<!-- 隐私追踪 ID -->
<Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="16">
<StackPanel Spacing="8">
<TextBlock Text="隐私追踪 ID"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="此 ID 用于匿名标识您的设备,不包含任何个人信息"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBox x:Name="TelemetryIdTextBox"
Text=""
IsReadOnly="True"
FontFamily="Consolas, Monaco, monospace"
FontSize="12"
HorizontalAlignment="Stretch" />
</StackPanel>
</Border>
<!-- 隐私协议同意区域 -->
<Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="16"
Margin="0,8,0,0">
<StackPanel Spacing="12">
<!-- 复选框和协议文本 -->
<StackPanel Orientation="Horizontal" Spacing="8">
<CheckBox x:Name="PrivacyAgreementCheckBox"
VerticalAlignment="Center" />
<StackPanel Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
<TextBlock Text="同意"
FontSize="13"
VerticalAlignment="Center"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<Button x:Name="ViewPrivacyPolicyButton"
Content="《阑山桌面遥测隐私数据收集协议》"
Background="Transparent"
BorderThickness="0"
Padding="0"
FontSize="13"
Foreground="{DynamicResource SystemAccentColor}">
<Button.Styles>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{DynamicResource SystemAccentColorDark1}" />
</Style>
</Button.Styles>
</Button>
</StackPanel>
</StackPanel>
<!-- 提示文本 -->
<TextBlock Text="您必须阅读并同意隐私协议后,才能开启遥测功能。遥测数据仅用于改进应用稳定性和优化产品体验,不包含任何个人身份信息。"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Border>
</StackPanel>
<StackPanel Grid.Row="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
Spacing="12"
Margin="0,24,0,0">
<Button x:Name="PrivacyBackButton"
Content="返回"
Theme="{DynamicResource ButtonTheme}" />
<Button x:Name="PrivacyNextButton"
Content="下一步"
Theme="{DynamicResource AccentButtonTheme}" />
</StackPanel>
</Grid>
<!-- 步骤 4: 欢迎完成页面 -->
<!-- 步骤 5: 欢迎完成页面 -->
<Grid x:Name="WelcomeStep" Margin="48" RowDefinitions="*,Auto" IsVisible="False">
<StackPanel Grid.Row="0"
VerticalAlignment="Center"

View File

@@ -51,6 +51,7 @@ public partial class OobeWindow : Window
private void OnWindowLoaded(object? sender, RoutedEventArgs e)
{
InitializeDataLocationStep();
InitializePrivacySettings();
SetupEventHandlers();
}
@@ -180,7 +181,29 @@ public partial class OobeWindow : Window
};
}
// 步骤 4: 欢迎完成页面
// 步骤 4: 隐私设置页面
if (this.FindControl<Button>("PrivacyBackButton") is { } privacyBackButton)
{
privacyBackButton.Click += OnPrivacyBackClick;
}
if (this.FindControl<Button>("PrivacyNextButton") is { } privacyNextButton)
{
privacyNextButton.Click += OnPrivacyNextClick;
}
if (this.FindControl<Button>("ViewPrivacyPolicyButton") is { } viewPrivacyPolicyButton)
{
viewPrivacyPolicyButton.Click += OnViewPrivacyPolicyClick;
}
// 隐私协议复选框 - 控制遥测开关
if (this.FindControl<CheckBox>("PrivacyAgreementCheckBox") is { } privacyCheckBox)
{
privacyCheckBox.IsCheckedChanged += OnPrivacyAgreementChanged;
}
// 步骤 5: 欢迎完成页面
if (this.FindControl<Button>("EnterButton") is { } enterButton)
{
enterButton.Click += OnEnterClick;
@@ -437,6 +460,60 @@ public partial class OobeWindow : Window
await NavigateToStep(4);
}
// 隐私设置页面按钮
private async void OnPrivacyBackClick(object? sender, RoutedEventArgs e)
{
if (_isTransitioning) return;
await NavigateToStep(3);
}
private async void OnPrivacyNextClick(object? sender, RoutedEventArgs e)
{
if (_isTransitioning) return;
// 保存隐私设置
SavePrivacySettings();
await NavigateToStep(5);
}
private void OnViewPrivacyPolicyClick(object? sender, RoutedEventArgs e)
{
// 打开隐私政策窗口
var privacyWindow = new PrivacyPolicyWindow
{
WindowStartupLocation = WindowStartupLocation.CenterOwner
};
privacyWindow.ShowDialog(this);
}
private void OnPrivacyAgreementChanged(object? sender, RoutedEventArgs e)
{
// 根据复选框状态控制遥测开关
if (this.FindControl<CheckBox>("PrivacyAgreementCheckBox") is { } checkBox &&
this.FindControl<ToggleSwitch>("CrashTelemetryToggle") is { } crashToggle &&
this.FindControl<ToggleSwitch>("UsageTelemetryToggle") is { } usageToggle)
{
var isAgreed = checkBox.IsChecked == true;
// 如果用户不同意协议,禁用遥测开关并关闭它们
crashToggle.IsEnabled = isAgreed;
usageToggle.IsEnabled = isAgreed;
if (!isAgreed)
{
crashToggle.IsChecked = false;
usageToggle.IsChecked = false;
}
else
{
// 用户同意协议后,默认开启遥测(用户可以在开关中手动关闭)
crashToggle.IsChecked = true;
usageToggle.IsChecked = true;
}
}
}
private async void OnEnterClick(object? sender, RoutedEventArgs e)
{
if (_isTransitioning) return;
@@ -635,7 +712,8 @@ public partial class OobeWindow : Window
1 => this.FindControl<Grid>("TypingStep"),
2 => this.FindControl<Grid>("ThemeStep"),
3 => this.FindControl<Grid>("DataLocationStep"),
4 => this.FindControl<Grid>("WelcomeStep"),
4 => this.FindControl<Grid>("PrivacyStep"),
5 => this.FindControl<Grid>("WelcomeStep"),
_ => null
};
@@ -645,7 +723,8 @@ public partial class OobeWindow : Window
1 => this.FindControl<Grid>("TypingStep"),
2 => this.FindControl<Grid>("ThemeStep"),
3 => this.FindControl<Grid>("DataLocationStep"),
4 => this.FindControl<Grid>("WelcomeStep"),
4 => this.FindControl<Grid>("PrivacyStep"),
5 => this.FindControl<Grid>("WelcomeStep"),
_ => null
};
@@ -698,6 +777,76 @@ public partial class OobeWindow : Window
var t1 = t - 1;
return 1 + c3 * Math.Pow(t1, 3) + c1 * Math.Pow(t1, 2);
}
private void InitializePrivacySettings()
{
// 生成隐私追踪 ID
var telemetryId = Guid.NewGuid().ToString("N");
if (this.FindControl<TextBox>("TelemetryIdTextBox") is { } telemetryIdTextBox)
{
telemetryIdTextBox.Text = telemetryId;
}
}
private void SavePrivacySettings()
{
try
{
var crashTelemetryEnabled = this.FindControl<ToggleSwitch>("CrashTelemetryToggle")?.IsChecked ?? true;
var usageTelemetryEnabled = this.FindControl<ToggleSwitch>("UsageTelemetryToggle")?.IsChecked ?? true;
var telemetryId = this.FindControl<TextBox>("TelemetryIdTextBox")?.Text ?? Guid.NewGuid().ToString("N");
// 保存到启动器配置
var privacyConfig = new PrivacyConfig
{
CrashTelemetryEnabled = crashTelemetryEnabled,
UsageTelemetryEnabled = usageTelemetryEnabled,
TelemetryId = telemetryId
};
var configPath = Path.Combine(_resolver.ResolveLauncherDataPath(), "privacy-config.json");
var json = System.Text.Json.JsonSerializer.Serialize(privacyConfig, AppJsonContext.Default.PrivacyConfig);
File.WriteAllText(configPath, json);
// 保存隐私协议同意状态(带防篡改保护)
var agreementService = new PrivacyAgreementService(_resolver.ResolveLauncherDataPath());
var isAgreed = this.FindControl<CheckBox>("PrivacyAgreementCheckBox")?.IsChecked ?? false;
// 生成用户ID和设备ID
var userId = telemetryId;
var deviceId = GetDeviceIdentifier();
agreementService.SaveAgreement(isAgreed, userId, deviceId);
Logger.Info($"[OobeWindow] 隐私设置已保存: Crash={crashTelemetryEnabled}, Usage={usageTelemetryEnabled}, Agreement={isAgreed}");
}
catch (Exception ex)
{
Logger.Warn($"[OobeWindow] 保存隐私设置失败: {ex.Message}");
}
}
/// <summary>
/// 获取设备标识符
/// </summary>
private string GetDeviceIdentifier()
{
try
{
// 使用机器名和用户名的组合作为设备标识
var machineName = Environment.MachineName;
var userName = Environment.UserName;
using var sha256 = System.Security.Cryptography.SHA256.Create();
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes($"{machineName}:{userName}"));
return Convert.ToHexString(hash).Substring(0, 16);
}
catch
{
return "UnknownDevice";
}
}
}
// 枚举定义(使用 Services 命名空间中的 ThemeMode

View File

@@ -0,0 +1,63 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:md="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
mc:Ignorable="d"
x:Class="LanMountainDesktop.Launcher.Views.PrivacyPolicyWindow"
x:DataType="views:PrivacyPolicyViewModel"
Title="阑山桌面遥测隐私数据收集协议"
Width="800"
Height="600"
MinWidth="600"
MinHeight="400"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
TransparencyLevelHint="None"
Icon="/Assets/logo.ico">
<Grid RowDefinitions="Auto,*,Auto">
<!-- 标题栏 -->
<Border Grid.Row="0"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
Padding="24,16">
<StackPanel Spacing="4">
<TextBlock Text="阑山桌面遥测隐私数据收集协议"
FontSize="20"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="请仔细阅读以下协议内容,了解我们如何收集、使用和保护您的数据"
FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Border>
<!-- Markdown 内容区域 -->
<Border Grid.Row="1"
Margin="24,16"
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}">
<md:MarkdownScrollViewer x:Name="MarkdownViewer"
Markdown="{Binding PrivacyPolicyMarkdown}"
HorizontalAlignment="Stretch" />
</Border>
<!-- 底部按钮 -->
<Border Grid.Row="2"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
BorderThickness="0,1,0,0"
Padding="24,16">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right"
Spacing="12">
<Button x:Name="CloseButton"
Content="关闭"
Theme="{DynamicResource AccentButtonTheme}"
Width="100" />
</StackPanel>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,121 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using CommunityToolkit.Mvvm.ComponentModel;
namespace LanMountainDesktop.Launcher.Views;
public partial class PrivacyPolicyWindow : Window
{
private readonly PrivacyPolicyViewModel _viewModel;
public PrivacyPolicyWindow()
{
InitializeComponent();
_viewModel = new PrivacyPolicyViewModel();
DataContext = _viewModel;
// 加载隐私政策内容
LoadPrivacyPolicy();
// 绑定关闭按钮事件
if (this.FindControl<Button>("CloseButton") is { } closeButton)
{
closeButton.Click += OnCloseClick;
}
}
private void OnCloseClick(object? sender, RoutedEventArgs e)
{
Close();
}
private void LoadPrivacyPolicy()
{
// 默认隐私政策内容Markdown 格式)
_viewModel.PrivacyPolicyMarkdown = @"# 阑山桌面遥测隐私数据收集协议
## 1. 概述
欢迎使用阑山桌面!本协议旨在向您说明我们在应用运行过程中收集哪些数据、如何使用这些数据以及如何保护您的隐私。
## 2. 我们收集的数据
### 2.1 崩溃报告(可选)
当应用发生崩溃时,我们可能会收集以下信息:
- **崩溃类型**:应用程序崩溃、无响应等异常情况的类型
- **错误堆栈**:导致崩溃的代码路径(不包含文件内容或个人数据)
- **设备信息**:操作系统版本、应用版本、.NET 运行时版本
- **匿名设备标识符**:一个随机生成的唯一标识符,用于统计崩溃频率
**注意**:崩溃报告不包含您的个人文件、桌面组件内容、浏览历史或任何可识别个人身份的信息。
### 2.2 使用统计(可选)
如果您启用了使用统计,我们可能会收集:
- **功能使用频率**:各功能模块的使用次数(如设置打开次数、组件添加次数)
- **性能指标**:应用启动时间、内存占用范围等性能数据
- **匿名设备标识符**:用于统计独立用户数量
**注意**:使用统计不包含您的组件配置、个人设置或任何敏感信息。
## 3. 我们不收集的数据
我们明确**不会**收集以下信息:
- ❌ 您的姓名、邮箱、电话号码等个人身份信息
- ❌ 您的桌面截图或壁纸内容
- ❌ 您添加的组件的具体内容或配置详情
- ❌ 您的文件系统浏览记录
- ❌ 您的网络活动或浏览历史
- ❌ 您的精确地理位置信息
## 4. 数据用途
我们收集的数据仅用于以下目的:
1. **改进应用稳定性**:通过分析崩溃报告,修复程序缺陷
2. **优化产品体验**:了解功能使用情况,优先改进常用功能
3. **性能优化**:识别性能瓶颈,提升应用运行效率
## 5. 数据存储与保护
- 所有数据通过**加密传输**HTTPS发送到我们的服务器
- 数据存储在安全的服务器环境中,访问受到严格控制
- 匿名设备标识符仅用于统计目的,无法关联到您的真实身份
- 我们**不会**将数据出售或共享给任何第三方用于商业目的
## 6. 您的控制权
您拥有以下权利:
- **随时开启或关闭**:您可以在 OOBE 向导或设置中随时更改遥测选项
- **数据删除**:如果您希望删除已收集的数据,请联系我们的支持团队
- **知情权**:您有权了解我们收集了哪些数据(通过本协议)
## 7. 协议更新
我们可能会不时更新本协议。重大变更时,我们会在应用内通知您。继续使用本应用即表示您同意修订后的协议。
## 8. 联系我们
如果您对本协议有任何疑问,请通过以下方式联系我们:
- 项目主页https://github.com/LanMountain/LanMountainDesktop
- 问题反馈:在 GitHub 仓库提交 Issue
---
**最后更新日期**2026年4月26日
感谢您信任并使用阑山桌面!";
}
}
public partial class PrivacyPolicyViewModel : ObservableObject
{
[ObservableProperty]
private string _privacyPolicyMarkdown = string.Empty;
}

0
get_git_log.py Normal file
View File