mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
changed.优化了更新体验
This commit is contained in:
187
.github/workflows/release.yml
vendored
187
.github/workflows/release.yml
vendored
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示迁移提示窗口
|
||||
/// </summary>
|
||||
private async Task<MigrationResult> 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())
|
||||
|
||||
341
LanMountainDesktop.Launcher/Services/LegacyVersionDetector.cs
Normal file
341
LanMountainDesktop.Launcher/Services/LegacyVersionDetector.cs
Normal file
@@ -0,0 +1,341 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 老版本检测器 - 检测 0.8.x 及更早的单应用模式安装
|
||||
/// </summary>
|
||||
internal sealed class LegacyVersionDetector
|
||||
{
|
||||
private const string LegacyAppName = "LanMountainDesktop";
|
||||
private const string LegacyExeName = "LanMountainDesktop.exe";
|
||||
|
||||
/// <summary>
|
||||
/// 检测是否存在老版本安装
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从注册表检测安装信息
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从常见安装路径检测
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检测便携版安装
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找卸载程序
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件版本
|
||||
/// </summary>
|
||||
private static string? TryGetFileVersion(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var versionInfo = FileVersionInfo.GetVersionInfo(filePath);
|
||||
return versionInfo.FileVersion;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否为老版本(版本号 < 1.0.0)
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开卸载界面
|
||||
/// </summary>
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在资源管理器中显示老版本位置
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 老版本信息
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 老版本安装类型
|
||||
/// </summary>
|
||||
public enum LegacyInstallType
|
||||
{
|
||||
Registry, // 注册表安装版
|
||||
CommonPath, // 常见路径安装
|
||||
Portable // 便携版
|
||||
}
|
||||
149
LanMountainDesktop.Launcher/Views/MigrationPromptWindow.axaml
Normal file
149
LanMountainDesktop.Launcher/Views/MigrationPromptWindow.axaml
Normal file
@@ -0,0 +1,149 @@
|
||||
<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:views="clr-namespace:LanMountainDesktop.Launcher.Views"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="520"
|
||||
d:DesignHeight="360"
|
||||
x:Class="LanMountainDesktop.Launcher.Views.MigrationPromptWindow"
|
||||
x:DataType="views:MigrationPromptWindow"
|
||||
Title="阑山桌面 - 版本迁移"
|
||||
Width="520"
|
||||
Height="360"
|
||||
CanResize="False"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
|
||||
TransparencyLevelHint="None"
|
||||
Icon="/Assets/logo.ico">
|
||||
<Design.DataContext>
|
||||
<views:MigrationPromptWindow />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
<!-- 主内容区域 -->
|
||||
<Grid Grid.Row="0" Margin="24,24,24,16" ColumnDefinitions="Auto,*">
|
||||
|
||||
<!-- 左侧:信息图标 -->
|
||||
<Border Grid.Column="0"
|
||||
Width="48"
|
||||
Height="48"
|
||||
Margin="0,4,16,0"
|
||||
Background="{DynamicResource SystemFillColorCautionBackgroundBrush}"
|
||||
CornerRadius="24"
|
||||
VerticalAlignment="Top">
|
||||
<TextBlock Text=""
|
||||
FontSize="24"
|
||||
FontFamily="{DynamicResource SymbolThemeFontFamily}"
|
||||
Foreground="{DynamicResource SystemFillColorCautionBrush}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
|
||||
<!-- 右侧:内容 -->
|
||||
<StackPanel Grid.Column="1" Spacing="12">
|
||||
<!-- 标题 -->
|
||||
<TextBlock Text="检测到旧版本"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
TextWrapping="Wrap"/>
|
||||
|
||||
<!-- 说明文字 -->
|
||||
<TextBlock x:Name="DescriptionText"
|
||||
Text="检测到您的系统中安装了旧版本的阑山桌面(0.8.4)。新版本采用了全新的架构,建议卸载旧版本以获得更好的体验。"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
LineHeight="20"/>
|
||||
|
||||
<!-- 老版本信息卡片 -->
|
||||
<Border Margin="0,8,0,0"
|
||||
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
|
||||
CornerRadius="8"
|
||||
Padding="16,12">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="Auto,*">
|
||||
<!-- 版本号 -->
|
||||
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||
Text="版本:"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}"/>
|
||||
<TextBlock x:Name="VersionText"
|
||||
Grid.Row="0" Grid.Column="1"
|
||||
Text="0.8.4"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Margin="8,0,0,0"/>
|
||||
|
||||
<!-- 安装路径 -->
|
||||
<TextBlock Grid.Row="1" Grid.Column="0"
|
||||
Text="位置:"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
|
||||
Margin="0,4,0,0"/>
|
||||
<TextBlock x:Name="PathText"
|
||||
Grid.Row="1" Grid.Column="1"
|
||||
Text="C:\Program Files\LanMountainDesktop"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Margin="8,4,0,0"/>
|
||||
|
||||
<!-- 安装类型 -->
|
||||
<TextBlock Grid.Row="2" Grid.Column="0"
|
||||
Text="类型:"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
|
||||
Margin="0,4,0,0"/>
|
||||
<TextBlock x:Name="TypeText"
|
||||
Grid.Row="2" Grid.Column="1"
|
||||
Text="安装版"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Margin="8,4,0,0"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<TextBlock Text="卸载旧版本不会影响新版本的使用,您的个人数据将保留。"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 底部:按钮区域 -->
|
||||
<Border Grid.Row="1"
|
||||
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
|
||||
Padding="24,16">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<!-- 左侧:查看位置按钮 -->
|
||||
<Button x:Name="ShowLocationButton"
|
||||
Grid.Column="0"
|
||||
Content="查看位置"
|
||||
Width="100"
|
||||
Height="32"
|
||||
FontSize="13"
|
||||
HorizontalAlignment="Left"/>
|
||||
|
||||
<!-- 右侧:操作按钮 -->
|
||||
<StackPanel Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<Button x:Name="SkipButton"
|
||||
Content="暂不处理"
|
||||
Width="100"
|
||||
Height="32"
|
||||
FontSize="13"/>
|
||||
<Button x:Name="UninstallButton"
|
||||
Content="卸载旧版本"
|
||||
Width="100"
|
||||
Height="32"
|
||||
FontSize="13"
|
||||
Theme="{DynamicResource AccentButtonTheme}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
157
LanMountainDesktop.Launcher/Views/MigrationPromptWindow.axaml.cs
Normal file
157
LanMountainDesktop.Launcher/Views/MigrationPromptWindow.axaml.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using LanMountainDesktop.Launcher.Services;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 迁移提示窗口 - 提示用户卸载旧版本
|
||||
/// </summary>
|
||||
public partial class MigrationPromptWindow : Window
|
||||
{
|
||||
private readonly TaskCompletionSource<MigrationResult> _completionSource = new();
|
||||
private LegacyVersionInfo? _legacyInfo;
|
||||
|
||||
public MigrationPromptWindow()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
InitializeEventHandlers();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置老版本信息
|
||||
/// </summary>
|
||||
public void SetLegacyInfo(LegacyVersionInfo info)
|
||||
{
|
||||
_legacyInfo = info;
|
||||
|
||||
// 更新 UI
|
||||
var versionText = this.FindControl<TextBlock>("VersionText");
|
||||
var pathText = this.FindControl<TextBlock>("PathText");
|
||||
var typeText = this.FindControl<TextBlock>("TypeText");
|
||||
var descriptionText = this.FindControl<TextBlock>("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})。新版本采用了全新的架构,建议卸载旧版本以获得更好的体验。";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化事件处理程序
|
||||
/// </summary>
|
||||
private void InitializeEventHandlers()
|
||||
{
|
||||
var showLocationButton = this.FindControl<Button>("ShowLocationButton");
|
||||
var skipButton = this.FindControl<Button>("SkipButton");
|
||||
var uninstallButton = this.FindControl<Button>("UninstallButton");
|
||||
|
||||
if (showLocationButton != null)
|
||||
{
|
||||
showLocationButton.Click += OnShowLocationClick;
|
||||
}
|
||||
|
||||
if (skipButton != null)
|
||||
{
|
||||
skipButton.Click += OnSkipClick;
|
||||
}
|
||||
|
||||
if (uninstallButton != null)
|
||||
{
|
||||
uninstallButton.Click += OnUninstallClick;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查看位置按钮点击
|
||||
/// </summary>
|
||||
private void OnShowLocationClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_legacyInfo != null)
|
||||
{
|
||||
LegacyVersionDetector.ShowInExplorer(_legacyInfo.InstallPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 跳过按钮点击
|
||||
/// </summary>
|
||||
private void OnSkipClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_completionSource.TrySetResult(MigrationResult.Skipped);
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 卸载按钮点击
|
||||
/// </summary>
|
||||
private void OnUninstallClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_legacyInfo != null)
|
||||
{
|
||||
LegacyVersionDetector.OpenUninstallInterface(_legacyInfo);
|
||||
}
|
||||
|
||||
_completionSource.TrySetResult(MigrationResult.UninstallOpened);
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等待用户选择
|
||||
/// </summary>
|
||||
public Task<MigrationResult> WaitForChoiceAsync()
|
||||
{
|
||||
return _completionSource.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口关闭事件
|
||||
/// </summary>
|
||||
protected override void OnClosing(WindowClosingEventArgs e)
|
||||
{
|
||||
// 如果还没有完成,标记为跳过
|
||||
if (!_completionSource.Task.IsCompleted)
|
||||
{
|
||||
_completionSource.TrySetResult(MigrationResult.Skipped);
|
||||
}
|
||||
|
||||
base.OnClosing(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 迁移结果
|
||||
/// </summary>
|
||||
public enum MigrationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户选择跳过
|
||||
/// </summary>
|
||||
Skipped,
|
||||
|
||||
/// <summary>
|
||||
/// 已打开卸载界面
|
||||
/// </summary>
|
||||
UninstallOpened
|
||||
}
|
||||
@@ -82,6 +82,7 @@ public sealed class UpdateWorkflowService
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a GitHub Release contains delta update assets (files.json, files.json.sig, update.zip).
|
||||
/// Also supports versioned filenames like files-{version}.json, delta-{old}-to-{new}.zip
|
||||
/// </summary>
|
||||
public static bool IsDeltaUpdateAvailable(GitHubReleaseInfo release)
|
||||
{
|
||||
@@ -91,9 +92,67 @@ public sealed class UpdateWorkflowService
|
||||
}
|
||||
|
||||
var assetNames = release.Assets.Select(a => a.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
return assetNames.Contains(DeltaManifestFileName)
|
||||
&& assetNames.Contains(DeltaSignatureFileName)
|
||||
&& assetNames.Contains(DeltaArchiveFileName);
|
||||
|
||||
// Check for exact matches first (preferred)
|
||||
var hasExactManifest = assetNames.Contains(DeltaManifestFileName);
|
||||
var hasExactSignature = assetNames.Contains(DeltaSignatureFileName);
|
||||
var hasExactArchive = assetNames.Contains(DeltaArchiveFileName);
|
||||
|
||||
if (hasExactManifest && hasExactSignature && hasExactArchive)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for versioned filenames (e.g., files-1.0.0.json, delta-0.9.9-to-1.0.0.zip)
|
||||
var hasVersionedManifest = assetNames.Any(n => n.StartsWith("files-", StringComparison.OrdinalIgnoreCase)
|
||||
&& n.EndsWith(".json", StringComparison.OrdinalIgnoreCase));
|
||||
var hasVersionedSignature = assetNames.Any(n => n.StartsWith("files-", StringComparison.OrdinalIgnoreCase)
|
||||
&& n.EndsWith(".sig", StringComparison.OrdinalIgnoreCase));
|
||||
var hasVersionedArchive = assetNames.Any(n => n.StartsWith("delta-", StringComparison.OrdinalIgnoreCase)
|
||||
&& n.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return hasVersionedManifest && hasVersionedSignature && hasVersionedArchive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the best matching delta asset name from the release assets.
|
||||
/// Prefers exact matches, falls back to versioned filenames.
|
||||
/// </summary>
|
||||
private static string? FindDeltaAssetName(GitHubReleaseInfo release, string baseName)
|
||||
{
|
||||
if (release?.Assets is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try exact match first
|
||||
var exactMatch = release.Assets.FirstOrDefault(a =>
|
||||
string.Equals(a.Name, baseName, StringComparison.OrdinalIgnoreCase));
|
||||
if (exactMatch != null)
|
||||
{
|
||||
return exactMatch.Name;
|
||||
}
|
||||
|
||||
// Fall back to pattern matching
|
||||
return baseName.ToLowerInvariant() switch
|
||||
{
|
||||
"files.json" => release.Assets
|
||||
.Where(a => a.Name.StartsWith("files-", StringComparison.OrdinalIgnoreCase)
|
||||
&& a.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(a => a.Name.Length)
|
||||
.FirstOrDefault()?.Name,
|
||||
"files.json.sig" => release.Assets
|
||||
.Where(a => a.Name.StartsWith("files-", StringComparison.OrdinalIgnoreCase)
|
||||
&& a.Name.EndsWith(".sig", StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(a => a.Name.Length)
|
||||
.FirstOrDefault()?.Name,
|
||||
"update.zip" => release.Assets
|
||||
.Where(a => a.Name.StartsWith("delta-", StringComparison.OrdinalIgnoreCase)
|
||||
&& a.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(a => a.Name.Length)
|
||||
.FirstOrDefault()?.Name,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -132,6 +191,24 @@ public sealed class UpdateWorkflowService
|
||||
var downloadSource = state.UpdateDownloadSource;
|
||||
var downloadThreads = state.UpdateDownloadThreads;
|
||||
|
||||
// Find the actual asset names (support both exact and versioned filenames)
|
||||
var manifestAssetName = FindDeltaAssetName(checkResult.Release, DeltaManifestFileName);
|
||||
var signatureAssetName = FindDeltaAssetName(checkResult.Release, DeltaSignatureFileName);
|
||||
var archiveAssetName = FindDeltaAssetName(checkResult.Release, DeltaArchiveFileName);
|
||||
|
||||
if (manifestAssetName is null || signatureAssetName is null || archiveAssetName is null)
|
||||
{
|
||||
return new UpdateDownloadResult(false, null, "One or more delta assets not found in release.");
|
||||
}
|
||||
|
||||
// Build asset map with actual names from release
|
||||
var assetMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[DeltaManifestFileName] = manifestAssetName,
|
||||
[DeltaSignatureFileName] = signatureAssetName,
|
||||
[DeltaArchiveFileName] = archiveAssetName
|
||||
};
|
||||
|
||||
var requiredAssets = new Dictionary<string, GitHubReleaseAsset>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[DeltaManifestFileName] = null!,
|
||||
@@ -141,9 +218,14 @@ public sealed class UpdateWorkflowService
|
||||
|
||||
foreach (var asset in checkResult.Release.Assets)
|
||||
{
|
||||
if (requiredAssets.ContainsKey(asset.Name))
|
||||
// Match by actual asset name
|
||||
foreach (var (key, actualName) in assetMap)
|
||||
{
|
||||
requiredAssets[asset.Name] = asset;
|
||||
if (string.Equals(asset.Name, actualName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
requiredAssets[key] = asset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,10 +60,20 @@ function Get-FileManifest {
|
||||
}
|
||||
|
||||
Write-Host "扫描上一版本文件..." -ForegroundColor Yellow
|
||||
Write-Host " 目录: $PreviousDir" -ForegroundColor Gray
|
||||
if (-not (Test-Path $PreviousDir)) {
|
||||
throw "Previous directory does not exist: $PreviousDir"
|
||||
}
|
||||
$previousManifest = Get-FileManifest -RootDir $PreviousDir
|
||||
Write-Host " 找到 $($previousManifest.Count) 个文件" -ForegroundColor Gray
|
||||
|
||||
Write-Host "扫描当前版本文件..." -ForegroundColor Yellow
|
||||
Write-Host " 目录: $CurrentDir" -ForegroundColor Gray
|
||||
if (-not (Test-Path $CurrentDir)) {
|
||||
throw "Current directory does not exist: $CurrentDir"
|
||||
}
|
||||
$currentManifest = Get-FileManifest -RootDir $CurrentDir
|
||||
Write-Host " 找到 $($currentManifest.Count) 个文件" -ForegroundColor Gray
|
||||
|
||||
# 分析文件变更
|
||||
$changedFiles = @()
|
||||
@@ -125,6 +135,18 @@ Write-Host " 复用: $($reusedFiles.Count) 个文件"
|
||||
Write-Host " 删除: $($deletedFiles.Count) 个文件"
|
||||
Write-Host ""
|
||||
|
||||
# 显示前10个变更的文件(用于调试)
|
||||
if ($changedFiles.Count -gt 0) {
|
||||
Write-Host "变更的文件示例:" -ForegroundColor Cyan
|
||||
$changedFiles | Select-Object -First 10 | ForEach-Object {
|
||||
Write-Host " [$($_.Action)] $($_.Path)" -ForegroundColor Gray
|
||||
}
|
||||
if ($changedFiles.Count -gt 10) {
|
||||
Write-Host " ... 还有 $($changedFiles.Count - 10) 个文件" -ForegroundColor Gray
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# 创建临时目录用于打包
|
||||
$tempDir = Join-Path $OutputDir "temp_delta"
|
||||
if (Test-Path $tempDir) {
|
||||
@@ -146,20 +168,28 @@ foreach ($file in $changedFiles) {
|
||||
Copy-Item -Path $sourcePath -Destination $destPath -Force
|
||||
}
|
||||
|
||||
# 创建 delta.zip
|
||||
$deltaZipPath = Join-Path $OutputDir "delta-$PreviousVersion-to-$CurrentVersion.zip"
|
||||
Write-Host "创建增量包: $deltaZipPath" -ForegroundColor Yellow
|
||||
# 创建 update.zip (Launcher 期望的文件名)
|
||||
$updateZipPath = Join-Path $OutputDir "update.zip"
|
||||
Write-Host "创建增量包: $updateZipPath" -ForegroundColor Yellow
|
||||
|
||||
if (Test-Path $updateZipPath) {
|
||||
Remove-Item -Path $updateZipPath -Force
|
||||
}
|
||||
|
||||
Compress-Archive -Path "$tempDir\*" -DestinationPath $updateZipPath -CompressionLevel Optimal
|
||||
|
||||
# 同时创建带版本号的副本(用于发布到 GitHub Release)
|
||||
$deltaZipPath = Join-Path $OutputDir "delta-$PreviousVersion-to-$CurrentVersion.zip"
|
||||
Write-Host "创建带版本号的副本: $deltaZipPath" -ForegroundColor Yellow
|
||||
if (Test-Path $deltaZipPath) {
|
||||
Remove-Item -Path $deltaZipPath -Force
|
||||
}
|
||||
|
||||
Compress-Archive -Path "$tempDir\*" -DestinationPath $deltaZipPath -CompressionLevel Optimal
|
||||
Copy-Item -Path $updateZipPath -Destination $deltaZipPath -Force
|
||||
|
||||
# 清理临时目录
|
||||
Remove-Item -Path $tempDir -Recurse -Force
|
||||
|
||||
# 生成 files.json
|
||||
# 生成 files.json (Launcher 期望的文件名)
|
||||
$filesJson = @{
|
||||
FromVersion = $PreviousVersion
|
||||
ToVersion = $CurrentVersion
|
||||
@@ -167,18 +197,26 @@ $filesJson = @{
|
||||
Files = @($changedFiles + $reusedFiles + $deletedFiles)
|
||||
}
|
||||
|
||||
$filesJsonPath = Join-Path $OutputDir "files-$CurrentVersion.json"
|
||||
$filesJsonPath = Join-Path $OutputDir "files.json"
|
||||
Write-Host "生成文件清单: $filesJsonPath" -ForegroundColor Yellow
|
||||
|
||||
$filesJson | ConvertTo-Json -Depth 10 | Set-Content -Path $filesJsonPath -Encoding UTF8
|
||||
|
||||
# 同时创建带版本号的副本(用于发布到 GitHub Release)
|
||||
$versionedFilesJsonPath = Join-Path $OutputDir "files-$CurrentVersion.json"
|
||||
Write-Host "创建带版本号的副本: $versionedFilesJsonPath" -ForegroundColor Yellow
|
||||
Copy-Item -Path $filesJsonPath -Destination $versionedFilesJsonPath -Force
|
||||
|
||||
# 计算增量包大小
|
||||
$deltaSize = (Get-Item $deltaZipPath).Length
|
||||
$deltaSizeMB = [math]::Round($deltaSize / 1MB, 2)
|
||||
$updateSize = (Get-Item $updateZipPath).Length
|
||||
$updateSizeMB = [math]::Round($updateSize / 1MB, 2)
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== 完成 ===" -ForegroundColor Green
|
||||
Write-Host "增量包大小: $deltaSizeMB MB"
|
||||
Write-Host "输出文件:"
|
||||
Write-Host " - $deltaZipPath"
|
||||
Write-Host "增量包大小: $updateSizeMB MB"
|
||||
Write-Host "输出文件 (Launcher 使用):"
|
||||
Write-Host " - $updateZipPath"
|
||||
Write-Host " - $filesJsonPath"
|
||||
Write-Host "输出文件 (GitHub Release 发布):"
|
||||
Write-Host " - $deltaZipPath"
|
||||
Write-Host " - $versionedFilesJsonPath"
|
||||
|
||||
Reference in New Issue
Block a user