mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
Add privacy agreement UI, models, and service
Introduce privacy/telemetry support: add PrivacyConfig and PrivacyAgreementState models, and a PrivacyAgreementService that saves/validates agreement state with HMAC integrity protection (privacy-agreement.state.json). Update AppJsonContext to include new types. Extend OOBE UI (OobeWindow.axaml/.cs) with a Data Location redesign and a new Privacy step (telemetry toggles, telemetry ID, agreement checkbox) and wire up handlers to save privacy-config.json and agreement state. Add a PrivacyPolicyWindow using Markdown.Avalonia to display the privacy policy; add CommunityToolkit.Mvvm and Markdown.Avalonia package references.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
<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" />
|
||||
<!-- Markdown.Avalonia 支持 (兼容 Avalonia 11.x) -->
|
||||
<PackageReference Include="Markdown.Avalonia" Version="11.0.3-a1" />
|
||||
<!-- MVVM 支持 -->
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- 资源文件 -->
|
||||
|
||||
42
LanMountainDesktop.Launcher/Models/PrivacyAgreementState.cs
Normal file
42
LanMountainDesktop.Launcher/Models/PrivacyAgreementState.cs
Normal 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;
|
||||
}
|
||||
22
LanMountainDesktop.Launcher/Models/PrivacyConfig.cs
Normal file
22
LanMountainDesktop.Launcher/Models/PrivacyConfig.cs
Normal 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;
|
||||
}
|
||||
245
LanMountainDesktop.Launcher/Services/PrivacyAgreementService.cs
Normal file
245
LanMountainDesktop.Launcher/Services/PrivacyAgreementService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
<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">
|
||||
<StackPanel Grid.Column="1" Spacing="8">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<fi:SymbolIcon Symbol="Folder"
|
||||
FontSize="24"
|
||||
Foreground="{DynamicResource AccentFillColorDefaultBrush}" />
|
||||
<TextBlock Text="保存在系统用户目录(推荐)"
|
||||
FontSize="14"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="数据与当前 Windows 用户绑定,重装应用或更新后数据不会丢失"
|
||||
FontSize="12"
|
||||
</StackPanel>
|
||||
<TextBlock Text="数据与当前 Windows 用户绑定,重装应用或更新后数据不会丢失。适合大多数用户。"
|
||||
FontSize="13"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
<Border Background="{DynamicResource SolidBackgroundFillColorQuarternaryBrush}"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
|
||||
Padding="12,8"
|
||||
Margin="0,4,0,0">
|
||||
<TextBlock x:Name="SystemPathText"
|
||||
FontSize="11"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
|
||||
Margin="0,4,0,0" />
|
||||
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"
|
||||
<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}" />
|
||||
<TextBlock Text="便于携带,可随应用文件夹整体移动到其他电脑"
|
||||
FontSize="12"
|
||||
</StackPanel>
|
||||
<TextBlock Text="便于携带,可随应用文件夹整体移动到其他电脑。适合在多台电脑间使用或需要便携运行的场景。"
|
||||
FontSize="13"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
<Border Background="{DynamicResource SolidBackgroundFillColorQuarternaryBrush}"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
|
||||
Padding="12,8"
|
||||
Margin="0,4,0,0">
|
||||
<TextBlock x:Name="PortablePathText"
|
||||
FontSize="11"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
|
||||
Margin="0,4,0,0" />
|
||||
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"
|
||||
<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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
63
LanMountainDesktop.Launcher/Views/PrivacyPolicyWindow.axaml
Normal file
63
LanMountainDesktop.Launcher/Views/PrivacyPolicyWindow.axaml
Normal 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>
|
||||
121
LanMountainDesktop.Launcher/Views/PrivacyPolicyWindow.axaml.cs
Normal file
121
LanMountainDesktop.Launcher/Views/PrivacyPolicyWindow.axaml.cs
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user