diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5a24016..ef6a4ef 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -219,8 +219,10 @@ jobs:
Move-Item -Path $newStructure -Destination $publishDir -Force
shell: pwsh
- - name: Install Inno Setup
- run: choco install innosetup -y --no-progress
+ - name: Install Inno Setup and 7z
+ run: |
+ choco install innosetup -y --no-progress
+ choco install 7zip -y --no-progress
shell: pwsh
- name: Build Installer
@@ -314,18 +316,41 @@ jobs:
Write-Host "Installer size: $([Math]::Round($installerFile.Length / 1MB, 2)) MB"
shell: pwsh
- - name: Generate Delta Package
+ - name: Create App Package
if: matrix.self_contained == true && matrix.arch == 'x64'
run: |
$version = "${{ needs.prepare.outputs.version }}"
- $publishDir = "publish/windows-${{ matrix.arch }}"
+ $arch = "${{ matrix.arch }}"
+ $publishDir = "publish/windows-$arch"
$appDir = "app-$version"
$currentAppPath = Join-Path $publishDir $appDir
$outputDir = "delta-output"
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
- # --- Determine previous version and download its update.zip for diff ---
+ # 创建 app-{version}-win-{arch}.zip 供后续版本作为旧版本对比
+ $appZipPath = Join-Path $outputDir "app-$version-win-$arch.zip"
+ Write-Host "Creating app-$version-win-$arch.zip..."
+ Compress-Archive -Path "$currentAppPath\*" -DestinationPath $appZipPath -CompressionLevel Optimal
+
+ $sizeMB = [Math]::Round((Get-Item $appZipPath).Length / 1MB, 2)
+ Write-Host "Created app-$version-win-$arch.zip: $sizeMB MB"
+ shell: pwsh
+
+ - name: Generate Delta Package
+ if: matrix.self_contained == true && matrix.arch == 'x64'
+ run: |
+ $version = "${{ needs.prepare.outputs.version }}"
+ $arch = "${{ matrix.arch }}"
+ $publishDir = "publish/windows-$arch"
+ $appDir = "app-$version"
+ $currentAppPath = Join-Path $publishDir $appDir
+ $outputDir = "delta-output"
+ $scriptPath = "scripts/Generate-DeltaPackage.ps1"
+
+ New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
+
+ # --- Determine previous version and download its app package for diff ---
$previousVersion = $null
$previousAppPath = $null
try {
@@ -336,128 +361,73 @@ jobs:
$previousVersion = $previousRelease.tag_name.TrimStart('v','V')
Write-Host "Previous release version: $previousVersion"
- # Try to download update.zip from previous release for diff
- $prevUpdateZip = $previousRelease.assets | Where-Object { $_.name -eq "update.zip" } | Select-Object -First 1
- if ($prevUpdateZip) {
- Write-Host "Found update.zip in previous release - extracting for diff..."
- $prevZipDest = Join-Path $outputDir "prev-update.zip"
- Invoke-WebRequest -Uri $prevUpdateZip.browser_download_url -OutFile $prevZipDest -Headers $headers
+ # 下载旧版本的 app-{version}-win-{arch}.zip
+ $prevAppZip = $previousRelease.assets | Where-Object { $_.name -eq "app-$previousVersion-win-$arch.zip" } | Select-Object -First 1
+ if ($prevAppZip) {
+ Write-Host "Found app-$previousVersion-win-$arch.zip in previous release - downloading for diff..."
+ $prevAppZipDest = Join-Path $outputDir "prev-app.zip"
+ Invoke-WebRequest -Uri $prevAppZip.browser_download_url -OutFile $prevAppZipDest -Headers $headers
+ # 解压 app-{version}.zip
$previousAppPath = Join-Path $outputDir "prev-app"
New-Item -ItemType Directory -Path $previousAppPath -Force | Out-Null
- Expand-Archive -Path $prevZipDest -DestinationPath $previousAppPath -Force
- Remove-Item -Path $prevZipDest -Force
+ Expand-Archive -Path $prevAppZipDest -DestinationPath $previousAppPath -Force
+ Remove-Item -Path $prevAppZipDest -Force -ErrorAction SilentlyContinue
- $prevFileCount = (Get-ChildItem -Path $previousAppPath -Recurse -File).Count
- Write-Host "Extracted $prevFileCount files from previous version for diff"
+ if ($previousAppPath -and (Test-Path $previousAppPath)) {
+ $prevFileCount = (Get-ChildItem -Path $previousAppPath -Recurse -File).Count
+ Write-Host "Extracted $prevFileCount files from previous version for diff"
+ }
} else {
- Write-Host "No update.zip found in previous release - will generate full package"
+ Write-Host "No app-$previousVersion-win-$arch.zip found in previous release - will generate full package"
+ Write-Host "This is expected for the first release after this fix."
}
}
} catch {
Write-Host "Could not fetch previous release: $_"
}
- # --- Generate file manifest with diff against previous version ---
- Write-Host "Generating update package for version $version..."
- $files = Get-ChildItem -Path $currentAppPath -Recurse -File
- $fileEntries = [System.Collections.ArrayList]::new()
- $changedFiles = [System.Collections.ArrayList]::new()
- $reusedCount = 0
- $addedCount = 0
- $replacedCount = 0
- $deletedCount = 0
+ # --- Generate delta package using the script ---
+ if ($previousAppPath -and (Test-Path $previousAppPath) -and $previousVersion) {
+ Write-Host "Generating delta package from $previousVersion to $version..."
+ & $scriptPath `
+ -PreviousVersion $previousVersion `
+ -CurrentVersion $version `
+ -PreviousDir $previousAppPath `
+ -CurrentDir $currentAppPath `
+ -OutputDir $outputDir
- # Build hash map of previous version files for quick lookup
- $prevHashMap = @{}
- if ($previousAppPath -and (Test-Path $previousAppPath)) {
- $prevFiles = Get-ChildItem -Path $previousAppPath -Recurse -File
- foreach ($pf in $prevFiles) {
- $relPath = $pf.FullName.Substring($previousAppPath.Length).TrimStart('\', '/').Replace('\', '/')
- if ($relPath -match '^\.(current|partial|destroy)$') { continue }
- $prevHashMap[$relPath] = (Get-FileHash -Path $pf.FullName -Algorithm SHA256).Hash.ToLower()
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error "Generate-DeltaPackage.ps1 failed"
+ exit 1
}
- Write-Host "Previous version has $($prevHashMap.Count) files for comparison"
- }
-
- foreach ($file in $files) {
- $relativePath = $file.FullName.Substring($currentAppPath.Length).TrimStart('\', '/')
- $relativePath = $relativePath.Replace('\', '/')
-
- # Skip deployment marker files
- if ($relativePath -match '^\.(current|partial|destroy)$') {
- continue
- }
-
- $hash = (Get-FileHash -Path $file.FullName -Algorithm SHA256).Hash.ToLower()
-
- if ($prevHashMap.ContainsKey($relativePath)) {
- $prevHash = $prevHashMap[$relativePath]
- if ($hash -eq $prevHash) {
- $fileEntries += @{ Path = $relativePath; Action = "reuse"; Sha256 = $hash }
- $reusedCount++
- } else {
- $fileEntries += @{ Path = $relativePath; Action = "replace"; Sha256 = $hash; ArchivePath = $relativePath }
- $changedFiles += $file
- $replacedCount++
- }
- $prevHashMap.Remove($relativePath)
- } else {
- $fileEntries += @{ Path = $relativePath; Action = "add"; Sha256 = $hash; ArchivePath = $relativePath }
- $changedFiles += $file
- $addedCount++
- }
- }
-
- # Files in previous version but not in current = deleted
- foreach ($deletedPath in $prevHashMap.Keys) {
- $fileEntries += @{ Path = $deletedPath; Action = "delete" }
- $deletedCount++
- }
-
- Write-Host "Delta summary: $reusedCount reused, $replacedCount replaced, $addedCount added, $deletedCount deleted"
- Write-Host "Changed files to include in update.zip: $($changedFiles.Count)"
-
- $filesJson = @{
- FromVersion = $previousVersion
- ToVersion = $version
- Platform = "windows"
- Arch = "x64"
- Files = $fileEntries
- } | ConvertTo-Json -Depth 10
-
- $filesJsonPath = Join-Path $outputDir "files.json"
- $filesJson | Set-Content -Path $filesJsonPath -Encoding UTF8
- Write-Host "Generated files.json with $($fileEntries.Count) entries"
-
- # Create update.zip with only changed files
- $tempDir = Join-Path $outputDir "temp_staging"
- New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
- foreach ($file in $changedFiles) {
- $relativePath = $file.FullName.Substring($currentAppPath.Length).TrimStart('\', '/')
- $destPath = Join-Path $tempDir $relativePath
- $destDir = Split-Path -Parent $destPath
- if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null }
- Copy-Item -Path $file.FullName -Destination $destPath -Force
- }
-
- $updateZipPath = Join-Path $outputDir "update.zip"
- if ($changedFiles.Count -gt 0) {
- Compress-Archive -Path "$tempDir\*" -DestinationPath $updateZipPath -CompressionLevel Optimal
} else {
- # No changed files - create a minimal zip
- $emptyMarker = Join-Path $tempDir ".no-changes"
- Set-Content -Path $emptyMarker -Value ""
- Compress-Archive -Path "$tempDir\*" -DestinationPath $updateZipPath -CompressionLevel Optimal
- }
- Remove-Item -Path $tempDir -Recurse -Force
+ Write-Host "No previous version available - generating full package..."
+ # Generate a "full" delta package (all files as "add")
+ & $scriptPath `
+ -PreviousVersion "0.0.0" `
+ -CurrentVersion $version `
+ -PreviousDir $currentAppPath `
+ -CurrentDir $currentAppPath `
+ -OutputDir $outputDir
- Write-Host "Created update.zip: $([Math]::Round((Get-Item $updateZipPath).Length / 1MB, 2)) MB"
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error "Generate-DeltaPackage.ps1 failed"
+ exit 1
+ }
+ }
# Clean up previous version extraction
if ($previousAppPath -and (Test-Path $previousAppPath)) {
Remove-Item -Path $previousAppPath -Recurse -Force -ErrorAction SilentlyContinue
}
+
+ # Display results
+ $updateZipPath = Join-Path $outputDir "update.zip"
+ if (Test-Path $updateZipPath) {
+ $sizeMB = [Math]::Round((Get-Item $updateZipPath).Length / 1MB, 2)
+ Write-Host "Created update.zip: $sizeMB MB"
+ }
shell: pwsh
- name: Sign File Map
@@ -512,6 +482,7 @@ jobs:
delta-output/files.json
delta-output/files.json.sig
delta-output/update.zip
+ delta-output/app-*.zip
if-no-files-found: error
retention-days: 90
@@ -912,6 +883,8 @@ jobs:
find artifacts -type f \( -name "*.exe" -o -name "*.deb" -o -name "*.dmg" \) -exec cp -v {} release-files/ \;
# Copy delta update files (files.json, files.json.sig, update.zip)
find artifacts -type f \( -name "files.json" -o -name "files.json.sig" -o -name "update.zip" \) -exec cp -v {} release-files/ \;
+ # Copy app package for future delta generation (app-{version}-win-{arch}.zip)
+ find artifacts -type f -name "app-*.zip" -exec cp -v {} release-files/ \;
echo ""
echo "Files ready for release:"
ls -lh release-files/ || echo "No files found in release-files"
diff --git a/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs b/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs
index cd0ef15..11e2822 100644
--- a/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs
+++ b/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs
@@ -41,6 +41,18 @@ internal sealed class LauncherFlowCoordinator
// 清理旧版本,保留至少3个版本
_deploymentLocator.CleanupOldDeployments(minVersionsToKeep: 3);
+ // 检测老版本安装(首次运行时)
+ if (_oobeStateService.IsFirstRun())
+ {
+ var legacyInfo = LegacyVersionDetector.DetectLegacyInstallation();
+ if (legacyInfo != null)
+ {
+ var migrationResult = await ShowMigrationPromptAsync(legacyInfo);
+ // 无论用户选择什么,都继续启动流程
+ Console.WriteLine($"[LauncherFlowCoordinator] Migration prompt result: {migrationResult}");
+ }
+ }
+
// 使用传入的 Splash 窗口或创建新的
var splashWindow = existingSplashWindow ?? await Dispatcher.UIThread.InvokeAsync(() =>
{
@@ -341,6 +353,69 @@ internal sealed class LauncherFlowCoordinator
return (result, customPath);
}
+ ///
+ /// 显示迁移提示窗口
+ ///
+ private async Task ShowMigrationPromptAsync(LegacyVersionInfo legacyInfo)
+ {
+ MigrationPromptWindow? migrationWindow = null;
+
+ // 在 UI 线程创建并显示迁移提示窗口
+ await Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ try
+ {
+ migrationWindow = new MigrationPromptWindow();
+ migrationWindow.SetLegacyInfo(legacyInfo);
+ migrationWindow.Show();
+ Console.WriteLine("[LauncherFlowCoordinator] MigrationPromptWindow shown");
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[LauncherFlowCoordinator] Failed to show MigrationPromptWindow: {ex.Message}");
+ }
+ });
+
+ if (migrationWindow is null)
+ {
+ Console.Error.WriteLine("[LauncherFlowCoordinator] MigrationPromptWindow is null, skipping migration prompt");
+ return MigrationResult.Skipped;
+ }
+
+ // 等待用户选择
+ MigrationResult result;
+
+ try
+ {
+ result = await migrationWindow.WaitForChoiceAsync();
+ Console.WriteLine($"[LauncherFlowCoordinator] MigrationPromptWindow result: {result}");
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[LauncherFlowCoordinator] Error waiting for migration choice: {ex.Message}");
+ result = MigrationResult.Skipped;
+ }
+
+ // 安全关闭窗口
+ await Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ try
+ {
+ if (migrationWindow.IsVisible && migrationWindow.IsLoaded)
+ {
+ migrationWindow.Close();
+ Console.WriteLine("[LauncherFlowCoordinator] MigrationPromptWindow closed successfully");
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[LauncherFlowCoordinator] Error closing MigrationPromptWindow: {ex.Message}");
+ }
+ });
+
+ return result;
+ }
+
private static void EnsureExecutable(string path)
{
if (OperatingSystem.IsWindows())
diff --git a/LanMountainDesktop.Launcher/Services/LegacyVersionDetector.cs b/LanMountainDesktop.Launcher/Services/LegacyVersionDetector.cs
new file mode 100644
index 0000000..010edf3
--- /dev/null
+++ b/LanMountainDesktop.Launcher/Services/LegacyVersionDetector.cs
@@ -0,0 +1,341 @@
+using System.Diagnostics;
+using Microsoft.Win32;
+
+namespace LanMountainDesktop.Launcher.Services;
+
+///
+/// 老版本检测器 - 检测 0.8.x 及更早的单应用模式安装
+///
+internal sealed class LegacyVersionDetector
+{
+ private const string LegacyAppName = "LanMountainDesktop";
+ private const string LegacyExeName = "LanMountainDesktop.exe";
+
+ ///
+ /// 检测是否存在老版本安装
+ ///
+ public static LegacyVersionInfo? DetectLegacyInstallation()
+ {
+ // 1. 检查注册表(安装版)
+ var registryInfo = DetectFromRegistry();
+ if (registryInfo != null)
+ {
+ return registryInfo;
+ }
+
+ // 2. 检查常见安装目录
+ var commonPaths = DetectFromCommonPaths();
+ if (commonPaths != null)
+ {
+ return commonPaths;
+ }
+
+ // 3. 检查便携版位置
+ var portableInfo = DetectPortableInstallation();
+ if (portableInfo != null)
+ {
+ return portableInfo;
+ }
+
+ return null;
+ }
+
+ ///
+ /// 从注册表检测安装信息
+ ///
+ private static LegacyVersionInfo? DetectFromRegistry()
+ {
+ try
+ {
+ // 检查 HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall
+ using var key = Registry.LocalMachine.OpenSubKey(
+ @$"Software\Microsoft\Windows\CurrentVersion\Uninstall\{LegacyAppName}");
+
+ if (key != null)
+ {
+ var installLocation = key.GetValue("InstallLocation") as string;
+ var displayVersion = key.GetValue("DisplayVersion") as string;
+ var uninstallString = key.GetValue("UninstallString") as string;
+
+ if (!string.IsNullOrWhiteSpace(installLocation) &&
+ File.Exists(Path.Combine(installLocation, LegacyExeName)))
+ {
+ return new LegacyVersionInfo
+ {
+ Version = displayVersion ?? "0.8.x",
+ InstallPath = installLocation,
+ UninstallCommand = uninstallString,
+ InstallType = LegacyInstallType.Registry
+ };
+ }
+ }
+
+ // 检查 HKCU(用户级安装)
+ using var userKey = Registry.CurrentUser.OpenSubKey(
+ @$"Software\Microsoft\Windows\CurrentVersion\Uninstall\{LegacyAppName}");
+
+ if (userKey != null)
+ {
+ var installLocation = userKey.GetValue("InstallLocation") as string;
+ var displayVersion = userKey.GetValue("DisplayVersion") as string;
+ var uninstallString = userKey.GetValue("UninstallString") as string;
+
+ if (!string.IsNullOrWhiteSpace(installLocation) &&
+ File.Exists(Path.Combine(installLocation, LegacyExeName)))
+ {
+ return new LegacyVersionInfo
+ {
+ Version = displayVersion ?? "0.8.x",
+ InstallPath = installLocation,
+ UninstallCommand = uninstallString,
+ InstallType = LegacyInstallType.Registry
+ };
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[LegacyVersionDetector] Registry detection failed: {ex.Message}");
+ }
+
+ return null;
+ }
+
+ ///
+ /// 从常见安装路径检测
+ ///
+ private static LegacyVersionInfo? DetectFromCommonPaths()
+ {
+ var commonPaths = new[]
+ {
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), LegacyAppName),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), LegacyAppName),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), LegacyAppName),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), LegacyAppName),
+ };
+
+ foreach (var path in commonPaths)
+ {
+ try
+ {
+ if (Directory.Exists(path))
+ {
+ // 检查是否存在老版本的特征文件(没有 app-* 目录)
+ var exePath = Path.Combine(path, LegacyExeName);
+ var hasAppDirs = Directory.GetDirectories(path, "app-*").Length > 0;
+
+ if (File.Exists(exePath) && !hasAppDirs)
+ {
+ // 尝试读取版本信息
+ var version = TryGetFileVersion(exePath);
+
+ return new LegacyVersionInfo
+ {
+ Version = version ?? "0.8.x",
+ InstallPath = path,
+ UninstallCommand = FindUninstaller(path),
+ InstallType = LegacyInstallType.CommonPath
+ };
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[LegacyVersionDetector] Path detection failed for {path}: {ex.Message}");
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 检测便携版安装
+ ///
+ private static LegacyVersionInfo? DetectPortableInstallation()
+ {
+ try
+ {
+ // 检查启动器所在目录的父目录(便携版常见布局)
+ var launcherDir = AppContext.BaseDirectory;
+ var parentDir = Path.GetFullPath(Path.Combine(launcherDir, ".."));
+
+ if (Directory.Exists(parentDir))
+ {
+ var exePath = Path.Combine(parentDir, LegacyExeName);
+ var hasAppDirs = Directory.GetDirectories(parentDir, "app-*").Length > 0;
+
+ // 如果存在 exe 且没有 app-* 目录,可能是老版本
+ if (File.Exists(exePath) && !hasAppDirs)
+ {
+ var version = TryGetFileVersion(exePath);
+
+ // 检查是否真的是老版本(通过文件版本或特定标记)
+ if (IsLegacyVersion(version))
+ {
+ return new LegacyVersionInfo
+ {
+ Version = version ?? "0.8.x",
+ InstallPath = parentDir,
+ UninstallCommand = null, // 便携版没有卸载程序
+ InstallType = LegacyInstallType.Portable
+ };
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[LegacyVersionDetector] Portable detection failed: {ex.Message}");
+ }
+
+ return null;
+ }
+
+ ///
+ /// 查找卸载程序
+ ///
+ private static string? FindUninstaller(string installPath)
+ {
+ try
+ {
+ // 常见的卸载程序命名
+ var uninstallerNames = new[] { "unins000.exe", "uninstall.exe", "Uninstall.exe" };
+
+ foreach (var name in uninstallerNames)
+ {
+ var path = Path.Combine(installPath, name);
+ if (File.Exists(path))
+ {
+ return path;
+ }
+ }
+ }
+ catch { }
+
+ return null;
+ }
+
+ ///
+ /// 获取文件版本
+ ///
+ private static string? TryGetFileVersion(string filePath)
+ {
+ try
+ {
+ var versionInfo = FileVersionInfo.GetVersionInfo(filePath);
+ return versionInfo.FileVersion;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// 判断是否为老版本(版本号 < 1.0.0)
+ ///
+ private static bool IsLegacyVersion(string? version)
+ {
+ if (string.IsNullOrWhiteSpace(version))
+ {
+ return true; // 无法确定版本时,保守认为是老版本
+ }
+
+ if (Version.TryParse(version.Split(' ')[0], out var v))
+ {
+ return v.Major < 1;
+ }
+
+ return true;
+ }
+
+ ///
+ /// 打开卸载界面
+ ///
+ public static void OpenUninstallInterface(LegacyVersionInfo info)
+ {
+ try
+ {
+ if (!string.IsNullOrWhiteSpace(info.UninstallCommand))
+ {
+ // 有卸载命令,直接执行
+ var parts = info.UninstallCommand.Split(new[] { ' ' }, 2);
+ var fileName = parts[0].Trim('"');
+ var arguments = parts.Length > 1 ? parts[1] : "";
+
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = fileName,
+ Arguments = arguments,
+ UseShellExecute = true,
+ Verb = "runas" // 请求管理员权限
+ });
+ }
+ else
+ {
+ // 没有卸载命令,打开系统卸载面板
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = "appwiz.cpl",
+ UseShellExecute = true
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[LegacyVersionDetector] Failed to open uninstall: {ex.Message}");
+
+ // 兜底:打开系统卸载面板
+ try
+ {
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = "appwiz.cpl",
+ UseShellExecute = true
+ });
+ }
+ catch { }
+ }
+ }
+
+ ///
+ /// 在资源管理器中显示老版本位置
+ ///
+ public static void ShowInExplorer(string path)
+ {
+ try
+ {
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = "explorer.exe",
+ Arguments = $"/select,\"{path}\"",
+ UseShellExecute = false
+ });
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[LegacyVersionDetector] Failed to show in explorer: {ex.Message}");
+ }
+ }
+}
+
+///
+/// 老版本信息
+///
+public class LegacyVersionInfo
+{
+ public string Version { get; set; } = "0.8.x";
+ public string InstallPath { get; set; } = "";
+ public string? UninstallCommand { get; set; }
+ public LegacyInstallType InstallType { get; set; }
+}
+
+///
+/// 老版本安装类型
+///
+public enum LegacyInstallType
+{
+ Registry, // 注册表安装版
+ CommonPath, // 常见路径安装
+ Portable // 便携版
+}
diff --git a/LanMountainDesktop.Launcher/Views/MigrationPromptWindow.axaml b/LanMountainDesktop.Launcher/Views/MigrationPromptWindow.axaml
new file mode 100644
index 0000000..25969e8
--- /dev/null
+++ b/LanMountainDesktop.Launcher/Views/MigrationPromptWindow.axaml
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop.Launcher/Views/MigrationPromptWindow.axaml.cs b/LanMountainDesktop.Launcher/Views/MigrationPromptWindow.axaml.cs
new file mode 100644
index 0000000..a3a97be
--- /dev/null
+++ b/LanMountainDesktop.Launcher/Views/MigrationPromptWindow.axaml.cs
@@ -0,0 +1,157 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using LanMountainDesktop.Launcher.Services;
+
+namespace LanMountainDesktop.Launcher.Views;
+
+///
+/// 迁移提示窗口 - 提示用户卸载旧版本
+///
+public partial class MigrationPromptWindow : Window
+{
+ private readonly TaskCompletionSource _completionSource = new();
+ private LegacyVersionInfo? _legacyInfo;
+
+ public MigrationPromptWindow()
+ {
+ AvaloniaXamlLoader.Load(this);
+ InitializeEventHandlers();
+ }
+
+ ///
+ /// 设置老版本信息
+ ///
+ public void SetLegacyInfo(LegacyVersionInfo info)
+ {
+ _legacyInfo = info;
+
+ // 更新 UI
+ var versionText = this.FindControl("VersionText");
+ var pathText = this.FindControl("PathText");
+ var typeText = this.FindControl("TypeText");
+ var descriptionText = this.FindControl("DescriptionText");
+
+ if (versionText != null)
+ {
+ versionText.Text = info.Version;
+ }
+
+ if (pathText != null)
+ {
+ pathText.Text = info.InstallPath;
+ }
+
+ if (typeText != null)
+ {
+ typeText.Text = info.InstallType switch
+ {
+ LegacyInstallType.Registry => "安装版",
+ LegacyInstallType.Portable => "便携版",
+ _ => "未知"
+ };
+ }
+
+ if (descriptionText != null)
+ {
+ descriptionText.Text = $"检测到您的系统中安装了旧版本的阑山桌面({info.Version})。新版本采用了全新的架构,建议卸载旧版本以获得更好的体验。";
+ }
+ }
+
+ ///
+ /// 初始化事件处理程序
+ ///
+ private void InitializeEventHandlers()
+ {
+ var showLocationButton = this.FindControl