mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
Launcher (#4)
* 激进的更新 * 试试 * fix.可爱的我一直在修CI( * fix.启动器一定要能够启动 * feat.尝试弄了AOT的启动器。 * fix.修CI,好像是因为Linux那边有个问题,反正修就对了。 * fix.ci难修,为什么liunx跑不起来呢? * Update build.yml * Update LanMountainDesktop.csproj * changed.调整了启动逻辑,优化了更新页面。 * changed.优化了更新体验 * feat.依旧试增量更新这一块,看看velopack * fix.我们试验性地修复了启动器无法正常启动的问题,原因可能是这个画面没有启动,就GUI没显示。然后还把编译问题修了一下。 * fix.继续修ci,ci怎么天天炸 * changed.velopack,试试rust * fix.修ci,修融合桌面,修启动器 * fix.GitHub Action工作流怎么天天出问题 * feat.引入velopack,不好,是rust(至少内存很安全了。 * chore: migrate release pipeline to signed filemap and wire rainyun s3 * fix: make optional s3 upload step workflow-parse safe * fix: make delta pack generation robust for empty diffs and linux paths * chore: rotate launcher update public key for pdc signing * fix: restore stable launcher update public key * fix: sync launcher public key with update signing secret * fix: normalize PEM line endings in signing key validation * fix: rotate launcher public key to match ci signing secret * fix: compare signing keys by SPKI instead of PEM text * refactor update backend to host-managed PDC pipeline * fix release workflow env key collisions * relax publish-pdc precheck to require S3 only * set GH_TOKEN for PDCC installer step * ci: add local pdc mock fallback for release publish * ci: fix pdc mock process log redirection * ci: fallback pdcc signing key to update private key * ci: ensure pdcc signing passphrase env is always set * ci: create pdcc publish root before invoking client * ci: set pdcc version variable from release version * ci: decouple pdcc installer version from publish config version * ci: package pdcc subchannels with generated filemap and changelog * ci: make local pdc mock diff return empty for fast fallback * ci: fix pdcc variable mapping and pdc signing prechecks * Update App.axaml.cs * ci: wire aws cli credentials for rainyun s3 * ci: pin pdcc client version separately from app version * ci: harden local pdc mock transport handling * ci: publish pdcc subchannels in one pass * ci: add pdcc publish heartbeat and timeout * ci: fix pdcc publish workdir bootstrap * feat.Penguin Logistics Online Network Distribution System * ci: fix plonds s3 probe and signing fallback * ci: validate signing key and quiet missing baselines * ci: relax aws checksum mode for rainyun s3 * ci: avoid multipart uploads to rainyun s3 * ci: handle empty plonds baselines safely * ci.plonds * Rebuild release pipeline around PLONDS and DDSS * Fix Windows installer script path in release workflow
This commit is contained in:
805
.trae/documents/launcher_comprehensive_improvement_plan.md
Normal file
805
.trae/documents/launcher_comprehensive_improvement_plan.md
Normal file
@@ -0,0 +1,805 @@
|
||||
# LanMountainDesktop Launcher 全面改进计划
|
||||
|
||||
## 概述
|
||||
|
||||
本计划旨在将 LanMountainDesktop 的 Launcher 改进为符合原子化架构的独立启动器,参考 ClassIsland 的极简设计,同时保留阑山桌面的特色功能。
|
||||
|
||||
## 目标
|
||||
|
||||
1. **P0 (必须完成)**: 重写 Launcher 为极简模式,移除与主程序的耦合
|
||||
2. **P1 (应该完成)**: 将 OOBE、Splash、更新、插件管理迁移到主程序
|
||||
3. **P2 (推荐完成)**: 实现 Launcher 自更新机制
|
||||
4. **P3 (可选优化)**: 性能优化和代码清理
|
||||
5. **P4 (长期规划)**: 增强功能和可扩展性
|
||||
|
||||
## 当前问题
|
||||
|
||||
1. Launcher 是 Avalonia 应用,启动慢、内存占用高
|
||||
2. Launcher 引用了 PluginSdk,与主程序有耦合
|
||||
3. 主程序引用了 Launcher,构建关系复杂
|
||||
4. Launcher 职责过多(OOBE + Splash + 更新 + 插件 + 启动)
|
||||
5. 缺少 Launcher 自更新机制
|
||||
6. GitHub Actions 工作流需要适配新的目录结构
|
||||
|
||||
## 改进后架构
|
||||
|
||||
```
|
||||
安装根目录/
|
||||
├── LanMountainDesktop.exe ← 启动器(唯一入口,极简,~100行代码)
|
||||
├── app-1.0.0/ ← 版本目录
|
||||
│ ├── .current ← 当前版本标记
|
||||
│ ├── LanMountainDesktop.exe ← 主程序
|
||||
│ └── ... (所有依赖)
|
||||
└── .launcher/ ← 启动器数据(可选)
|
||||
└── snapshots/ ← 版本快照
|
||||
```
|
||||
|
||||
## 详细实施步骤
|
||||
|
||||
### P0: 基础架构重构
|
||||
|
||||
#### 1. 重写 Launcher 为极简模式
|
||||
|
||||
**文件**: `LanMountainDesktop.Launcher/Program.cs`
|
||||
|
||||
**目标**:
|
||||
- 代码量控制在 100 行以内
|
||||
- 零外部依赖(不使用 Avalonia)
|
||||
- 只负责:版本选择、启动主程序、清理旧版本
|
||||
|
||||
**完整实现代码**:
|
||||
|
||||
```csharp
|
||||
// LanMountainDesktop.Launcher/Program.cs
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace LanMountainDesktop.Launcher;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
private const string HostExecutableName = "LanMountainDesktop.exe";
|
||||
private const string HostExecutableNameLinux = "LanMountainDesktop";
|
||||
|
||||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
var rootDir = GetRootDirectory();
|
||||
|
||||
// 1. 查找最佳版本
|
||||
var installation = FindBestVersion(rootDir);
|
||||
if (installation == null)
|
||||
{
|
||||
ShowError("找不到有效的 LanMountainDesktop 版本,请重新安装。");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 2. 清理旧版本(异步,不阻塞)
|
||||
_ = Task.Run(() => CleanupOldVersions(rootDir));
|
||||
|
||||
// 3. 启动主程序
|
||||
return LaunchHost(installation, args);
|
||||
}
|
||||
|
||||
private static string GetRootDirectory()
|
||||
{
|
||||
return Path.GetFullPath(
|
||||
Path.GetDirectoryName(Environment.ProcessPath) ?? "");
|
||||
}
|
||||
|
||||
private static string? FindBestVersion(string rootDir)
|
||||
{
|
||||
var exeName = OperatingSystem.IsWindows()
|
||||
? HostExecutableName
|
||||
: HostExecutableNameLinux;
|
||||
|
||||
return Directory.GetDirectories(rootDir)
|
||||
.Where(x => IsValidVersionDirectory(x, exeName))
|
||||
.OrderBy(x => File.Exists(Path.Combine(x, ".current")) ? 0 : 1)
|
||||
.ThenByDescending(x => ParseVersion(Path.GetFileName(x)))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static bool IsValidVersionDirectory(string path, string exeName)
|
||||
{
|
||||
var dirName = Path.GetFileName(path);
|
||||
return dirName.StartsWith("app-") &&
|
||||
!File.Exists(Path.Combine(path, ".destroy")) &&
|
||||
!File.Exists(Path.Combine(path, ".partial")) &&
|
||||
File.Exists(Path.Combine(path, exeName));
|
||||
}
|
||||
|
||||
private static Version ParseVersion(string dirName)
|
||||
{
|
||||
// app-1.0.0 or app-1.0.0-123
|
||||
var parts = dirName.Split('-');
|
||||
if (parts.Length >= 2 && Version.TryParse(parts[1], out var v))
|
||||
return v;
|
||||
return new Version(0, 0);
|
||||
}
|
||||
|
||||
private static void CleanupOldVersions(string rootDir)
|
||||
{
|
||||
try
|
||||
{
|
||||
var oldVersions = Directory.GetDirectories(rootDir)
|
||||
.Where(x => File.Exists(Path.Combine(x, ".destroy")));
|
||||
|
||||
foreach (var dir in oldVersions)
|
||||
{
|
||||
try { Directory.Delete(dir, recursive: true); } catch { }
|
||||
}
|
||||
}
|
||||
catch { /* 忽略清理失败 */ }
|
||||
}
|
||||
|
||||
private static int LaunchHost(string installation, string[] args)
|
||||
{
|
||||
var exeName = OperatingSystem.IsWindows()
|
||||
? HostExecutableName
|
||||
: HostExecutableNameLinux;
|
||||
var exePath = Path.Combine(installation, exeName);
|
||||
|
||||
// Linux/macOS: 确保可执行权限
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
EnsureExecutable(exePath);
|
||||
}
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = exePath,
|
||||
WorkingDirectory = Path.GetDirectoryName(installation),
|
||||
UseShellExecute = true
|
||||
};
|
||||
|
||||
foreach (var arg in args)
|
||||
startInfo.ArgumentList.Add(arg);
|
||||
|
||||
// 传递环境变量
|
||||
startInfo.EnvironmentVariables["LMD_PACKAGE_ROOT"] =
|
||||
Path.GetDirectoryName(installation);
|
||||
startInfo.EnvironmentVariables["LMD_VERSION"] =
|
||||
Path.GetFileName(installation).Replace("app-", "");
|
||||
|
||||
try
|
||||
{
|
||||
Process.Start(startInfo);
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowError($"启动失败: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureExecutable(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "chmod",
|
||||
Arguments = $"+x \"{path}\"",
|
||||
CreateNoWindow = true
|
||||
})?.WaitForExit();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private static void ShowError(string message)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
// Win32 MessageBox
|
||||
try
|
||||
{
|
||||
MessageBox(IntPtr.Zero, message, "LanMountainDesktop", 0x10);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.Error.WriteLine(message);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine(message);
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 修改 Launcher 项目文件
|
||||
|
||||
**文件**: `LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj`
|
||||
|
||||
**完整内容**:
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Version>1.0.0</Version>
|
||||
<ApplicationIcon>Assets\logo_nightly.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- 图标资源 -->
|
||||
<ItemGroup>
|
||||
<Content Include="..\LanMountainDesktop\Assets\logo_nightly.ico" Link="Assets\logo_nightly.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
#### 3. 移除主程序对 Launcher 的引用
|
||||
|
||||
**文件**: `LanMountainDesktop/LanMountainDesktop.csproj`
|
||||
|
||||
**修改**: 删除以下行
|
||||
```xml
|
||||
<!-- 删除这一行 -->
|
||||
<ProjectReference Include="..\LanMountainDesktop.Launcher\LanMountainDesktop.Launcher.csproj" ReferenceOutputAssembly="false" />
|
||||
```
|
||||
|
||||
#### 4. 修改主程序支持新架构
|
||||
|
||||
**文件**: `LanMountainDesktop/Program.cs`
|
||||
|
||||
**修改**: 添加环境变量读取
|
||||
|
||||
```csharp
|
||||
// 在 Program.cs 中添加
|
||||
internal static class LaunchContext
|
||||
{
|
||||
public static string? PackageRoot =>
|
||||
Environment.GetEnvironmentVariable("LMD_PACKAGE_ROOT");
|
||||
public static string? Version =>
|
||||
Environment.GetEnvironmentVariable("LMD_VERSION");
|
||||
public static bool IsLaunchedByLauncher =>
|
||||
!string.IsNullOrEmpty(PackageRoot);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### P1: 功能迁移
|
||||
|
||||
#### 5. 将 OOBE 迁移到主程序
|
||||
|
||||
**新建文件**: `LanMountainDesktop/Services/Oobe/OobeService.cs`
|
||||
|
||||
```csharp
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
|
||||
namespace LanMountainDesktop.Services.Oobe;
|
||||
|
||||
public class OobeService
|
||||
{
|
||||
private readonly string _oobeStatePath;
|
||||
|
||||
public OobeService()
|
||||
{
|
||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
_oobeStatePath = Path.Combine(appData, "LanMountainDesktop", ".oobe_completed");
|
||||
}
|
||||
|
||||
public bool IsFirstRun()
|
||||
{
|
||||
return !File.Exists(_oobeStatePath);
|
||||
}
|
||||
|
||||
public void MarkCompleted()
|
||||
{
|
||||
var dir = Path.GetDirectoryName(_oobeStatePath);
|
||||
if (!Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
File.WriteAllText(_oobeStatePath, DateTime.UtcNow.ToString("O"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**新建文件**: `LanMountainDesktop/Views/Oobe/OobeWindow.axaml`
|
||||
|
||||
```xml
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="LanMountainDesktop.Views.Oobe.OobeWindow"
|
||||
Title="欢迎使用阑山桌面"
|
||||
Width="800"
|
||||
Height="600"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<Grid>
|
||||
<!-- OOBE 界面内容 -->
|
||||
<TextBlock Text="欢迎使用阑山桌面" FontSize="24" HorizontalAlignment="Center" Margin="0,50,0,0"/>
|
||||
<Button Content="开始使用" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,50" Click="OnStartClick"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
```
|
||||
|
||||
**修改文件**: `LanMountainDesktop/App.axaml.cs`
|
||||
|
||||
```csharp
|
||||
// 在 OnFrameworkInitializationCompleted 中添加
|
||||
private async Task InitializeOobeAsync()
|
||||
{
|
||||
var oobeService = new OobeService();
|
||||
if (oobeService.IsFirstRun())
|
||||
{
|
||||
var oobeWindow = new Views.Oobe.OobeWindow();
|
||||
await oobeWindow.ShowDialog();
|
||||
oobeService.MarkCompleted();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. 将 Splash 迁移到主程序
|
||||
|
||||
**新建文件**: `LanMountainDesktop/Views/Splash/SplashWindow.axaml`
|
||||
|
||||
```xml
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="LanMountainDesktop.Views.Splash.SplashWindow"
|
||||
Title="阑山桌面"
|
||||
Width="400"
|
||||
Height="300"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ShowInTaskbar="False"
|
||||
SystemDecorations="None">
|
||||
<Grid Background="{DynamicResource SystemAccentColor}">
|
||||
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<Image Source="/Assets/logo_nightly.png" Width="100" Height="100"/>
|
||||
<TextBlock Text="阑山桌面" FontSize="20" Margin="0,20,0,0" HorizontalAlignment="Center"/>
|
||||
<TextBlock x:Name="StatusText" Text="正在启动..." Margin="0,10,0,0" HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
```
|
||||
|
||||
**修改文件**: `LanMountainDesktop/App.axaml.cs`
|
||||
|
||||
```csharp
|
||||
// 在初始化时显示 Splash
|
||||
private SplashWindow? _splashWindow;
|
||||
|
||||
private void ShowSplash()
|
||||
{
|
||||
_splashWindow = new SplashWindow();
|
||||
_splashWindow.Show();
|
||||
}
|
||||
|
||||
private void CloseSplash()
|
||||
{
|
||||
_splashWindow?.Close();
|
||||
_splashWindow = null;
|
||||
}
|
||||
```
|
||||
|
||||
#### 7. 将更新逻辑迁移到主程序
|
||||
|
||||
**新建目录**: `LanMountainDesktop/Services/Update/`
|
||||
|
||||
**新建文件**: `LanMountainDesktop/Services/Update/UpdateService.cs`
|
||||
|
||||
```csharp
|
||||
using System.Net.Http.Json;
|
||||
using LanMountainDesktop.Models;
|
||||
|
||||
namespace LanMountainDesktop.Services.Update;
|
||||
|
||||
public class UpdateService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _currentVersion;
|
||||
|
||||
public UpdateService()
|
||||
{
|
||||
_httpClient = new HttpClient();
|
||||
_httpClient.DefaultRequestHeaders.Add("User-Agent", "LanMountainDesktop");
|
||||
_currentVersion = GetType().Assembly.GetName().Version?.ToString() ?? "1.0.0";
|
||||
}
|
||||
|
||||
public async Task<UpdateCheckResult> CheckForUpdateAsync(UpdateChannel channel)
|
||||
{
|
||||
// 调用 GitHub Release API
|
||||
var releases = await _httpClient.GetFromJsonAsync<List<GitHubRelease>>(
|
||||
"https://api.github.com/repos/ClassIsland/LanMountainDesktop/releases");
|
||||
|
||||
var latest = channel == UpdateChannel.Stable
|
||||
? releases?.FirstOrDefault(r => !r.Prerelease)
|
||||
: releases?.FirstOrDefault();
|
||||
|
||||
if (latest == null)
|
||||
return new UpdateCheckResult { HasUpdate = false };
|
||||
|
||||
var latestVersion = latest.TagName.TrimStart('v');
|
||||
var hasUpdate = new Version(latestVersion) > new Version(_currentVersion);
|
||||
|
||||
return new UpdateCheckResult
|
||||
{
|
||||
HasUpdate = hasUpdate,
|
||||
Version = latestVersion,
|
||||
DownloadUrl = latest.Assets.FirstOrDefault()?.BrowserDownloadUrl
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class UpdateCheckResult
|
||||
{
|
||||
public bool HasUpdate { get; set; }
|
||||
public string? Version { get; set; }
|
||||
public string? DownloadUrl { get; set; }
|
||||
}
|
||||
|
||||
public enum UpdateChannel { Stable, Preview }
|
||||
|
||||
public class GitHubRelease
|
||||
{
|
||||
public string TagName { get; set; } = "";
|
||||
public bool Prerelease { get; set; }
|
||||
public List<GitHubAsset> Assets { get; set; } = new();
|
||||
}
|
||||
|
||||
public class GitHubAsset
|
||||
{
|
||||
public string BrowserDownloadUrl { get; set; } = "";
|
||||
}
|
||||
```
|
||||
|
||||
#### 8. 将插件管理迁移到主程序
|
||||
|
||||
**新建目录**: `LanMountainDesktop/Services/Plugins/`
|
||||
|
||||
**新建文件**: `LanMountainDesktop/Services/Plugins/PluginUpdateService.cs`
|
||||
|
||||
```csharp
|
||||
namespace LanMountainDesktop.Services.Plugins;
|
||||
|
||||
public class PluginUpdateService
|
||||
{
|
||||
private readonly string _pluginsDirectory;
|
||||
|
||||
public PluginUpdateService()
|
||||
{
|
||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
_pluginsDirectory = Path.Combine(appData, "LanMountainDesktop", "plugins");
|
||||
}
|
||||
|
||||
public async Task CheckAndUpdatePluginsAsync()
|
||||
{
|
||||
// 检查插件更新
|
||||
// 下载并安装更新
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### P2: 自更新机制
|
||||
|
||||
#### 9. 实现 Launcher 自更新
|
||||
|
||||
**修改文件**: `LanMountainDesktop.Launcher/Program.cs`
|
||||
|
||||
```csharp
|
||||
// 在 Main 方法开头添加自更新检查
|
||||
private static void CheckForLauncherUpdate()
|
||||
{
|
||||
var rootDir = GetRootDirectory();
|
||||
var updatePath = Path.Combine(rootDir, "LanMountainDesktop.Launcher.Update.exe");
|
||||
|
||||
if (File.Exists(updatePath))
|
||||
{
|
||||
// 有新版本 Launcher,替换自身
|
||||
try
|
||||
{
|
||||
var currentPath = Environment.ProcessPath;
|
||||
var backupPath = currentPath + ".old";
|
||||
|
||||
// 重命名当前版本
|
||||
if (File.Exists(backupPath))
|
||||
File.Delete(backupPath);
|
||||
File.Move(currentPath!, backupPath);
|
||||
|
||||
// 移动新版本
|
||||
File.Move(updatePath, currentPath!);
|
||||
|
||||
// 删除备份
|
||||
File.Delete(backupPath);
|
||||
|
||||
// 重启自己
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = currentPath,
|
||||
UseShellExecute = true
|
||||
});
|
||||
Environment.Exit(0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 回滚
|
||||
Console.Error.WriteLine($"Launcher 更新失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 10. 主程序支持更新 Launcher
|
||||
|
||||
**新建文件**: `LanMountainDesktop/Services/Update/LauncherUpdateService.cs`
|
||||
|
||||
```csharp
|
||||
namespace LanMountainDesktop.Services.Update;
|
||||
|
||||
public class LauncherUpdateService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public LauncherUpdateService()
|
||||
{
|
||||
_httpClient = new HttpClient();
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateLauncherAsync(string downloadUrl)
|
||||
{
|
||||
var rootDir = LaunchContext.PackageRoot
|
||||
?? Path.GetDirectoryName(Environment.ProcessPath)!;
|
||||
var updatePath = Path.Combine(rootDir, "LanMountainDesktop.Launcher.Update.exe");
|
||||
|
||||
// 下载新版本
|
||||
var response = await _httpClient.GetAsync(downloadUrl);
|
||||
await using var fs = File.Create(updatePath);
|
||||
await response.Content.CopyToAsync(fs);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RestartWithNewLauncher()
|
||||
{
|
||||
var launcherPath = Path.Combine(
|
||||
LaunchContext.PackageRoot ?? "",
|
||||
"LanMountainDesktop.exe");
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = launcherPath,
|
||||
UseShellExecute = true
|
||||
});
|
||||
|
||||
// 退出主程序,让 Launcher 接管
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### P3: 清理旧代码
|
||||
|
||||
#### 11. 删除文件清单
|
||||
|
||||
**删除以下文件/目录**:
|
||||
|
||||
```
|
||||
LanMountainDesktop.Launcher/
|
||||
├── App.axaml ← 删除
|
||||
├── App.axaml.cs ← 删除
|
||||
├── Views/ ← 删除整个目录
|
||||
│ ├── OobeWindow.axaml
|
||||
│ ├── OobeWindow.axaml.cs
|
||||
│ ├── SplashWindow.axaml
|
||||
│ └── SplashWindow.axaml.cs
|
||||
├── Services/ ← 删除大部分
|
||||
│ ├── LauncherFlowCoordinator.cs ← 删除
|
||||
│ ├── OobeStateService.cs ← 删除
|
||||
│ ├── UpdateCheckService.cs ← 删除
|
||||
│ ├── UpdateEngineService.cs ← 删除
|
||||
│ ├── PluginInstallerService.cs ← 删除
|
||||
│ └── PluginUpgradeQueueService.cs ← 删除
|
||||
└── Models/ ← 删除(如不再需要)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### P4: GitHub Actions 工作流修改
|
||||
|
||||
#### 12. 修改 release.yml
|
||||
|
||||
**关键修改点**:
|
||||
|
||||
1. **Launcher 单独编译**:
|
||||
```yaml
|
||||
- name: Publish Launcher
|
||||
run: |
|
||||
dotnet publish LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj `
|
||||
-c Release `
|
||||
-o ./publish/launcher-win-x64 `
|
||||
--self-contained `
|
||||
-r win-x64 `
|
||||
-p:PublishSingleFile=false `
|
||||
-p:PublishTrimmed=false `
|
||||
-p:DebugType=none
|
||||
```
|
||||
|
||||
2. **目录结构调整**:
|
||||
```yaml
|
||||
- name: Restructure for Launcher
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$publishDir = "publish/windows-x64"
|
||||
$launcherDir = "publish/launcher-win-x64"
|
||||
$appDir = "app-$version"
|
||||
|
||||
# 创建新结构
|
||||
$newStructure = "publish-launcher/windows-x64"
|
||||
New-Item -ItemType Directory -Path $newStructure -Force
|
||||
|
||||
# 移动主程序到 app-{version}/
|
||||
$appPath = Join-Path $newStructure $appDir
|
||||
Move-Item -Path $publishDir -Destination $appPath -Force
|
||||
|
||||
# 复制 Launcher 到根目录
|
||||
Copy-Item -Path "$launcherDir\*" -Destination $newStructure -Recurse -Force
|
||||
|
||||
# 创建 .current 标记
|
||||
New-Item -ItemType File -Path (Join-Path $appPath ".current") -Force
|
||||
```
|
||||
|
||||
3. **Linux/macOS 同样调整**:
|
||||
- Linux: 修改 DEB 打包流程
|
||||
- macOS: 修改 DMG 打包流程
|
||||
|
||||
#### 13. 修改 build.yml
|
||||
|
||||
**修改**: 移除 Launcher 相关构建步骤,因为 Launcher 现在完全独立
|
||||
|
||||
---
|
||||
|
||||
### P5: 图标资源处理
|
||||
|
||||
#### 14. Launcher 图标配置
|
||||
|
||||
**方案**: 使用链接方式引用主程序图标
|
||||
|
||||
**文件**: `LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj`
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<!-- 链接主程序的图标 -->
|
||||
<Content Include="..\LanMountainDesktop\Assets\logo_nightly.ico" Link="Assets\logo_nightly.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
#### 15. 安装程序配置
|
||||
|
||||
**文件**: `LanMountainDesktop/installer/LanMountainDesktop.iss` (Inno Setup)
|
||||
|
||||
**关键配置**:
|
||||
|
||||
```ini
|
||||
[Setup]
|
||||
AppName=阑山桌面
|
||||
AppVersion={#MyAppVersion}
|
||||
DefaultDirName={autopf}\LanMountainDesktop
|
||||
OutputBaseFilename=LanMountainDesktop-Setup-{#MyAppVersion}-x64
|
||||
SetupIconFile=..\Assets\logo_nightly.ico
|
||||
UninstallDisplayIcon={app}\LanMountainDesktop.exe
|
||||
|
||||
[Files]
|
||||
; Launcher
|
||||
Source: "..\..\publish\windows-x64\LanMountainDesktop.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
; 主程序版本目录
|
||||
Source: "..\..\publish\windows-x64\app-{#MyAppVersion}\*"; DestDir: "{app}\app-{#MyAppVersion}"; Flags: ignoreversion recursesubdirs
|
||||
|
||||
[Icons]
|
||||
; 桌面快捷方式
|
||||
Name: "{autodesktop}\阑山桌面"; Filename: "{app}\LanMountainDesktop.exe"; IconFilename: "{app}\LanMountainDesktop.exe"
|
||||
; 开始菜单
|
||||
Name: "{group}\阑山桌面"; Filename: "{app}\LanMountainDesktop.exe"; IconFilename: "{app}\LanMountainDesktop.exe"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文件变更清单
|
||||
|
||||
### 修改文件
|
||||
|
||||
1. `LanMountainDesktop.Launcher/Program.cs` - 完全重写
|
||||
2. `LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj` - 简化依赖
|
||||
3. `LanMountainDesktop/LanMountainDesktop.csproj` - 移除 Launcher 引用
|
||||
4. `LanMountainDesktop/Program.cs` - 添加 LaunchContext
|
||||
5. `LanMountainDesktop/App.axaml.cs` - 添加 OOBE/Splash/更新入口
|
||||
6. `.github/workflows/release.yml` - 调整打包流程
|
||||
7. `.github/workflows/build.yml` - 适配新构建流程
|
||||
|
||||
### 新增文件
|
||||
|
||||
1. `LanMountainDesktop/Services/Oobe/OobeService.cs`
|
||||
2. `LanMountainDesktop/Views/Oobe/OobeWindow.axaml`
|
||||
3. `LanMountainDesktop/Views/Oobe/OobeWindow.axaml.cs`
|
||||
4. `LanMountainDesktop/Views/Splash/SplashWindow.axaml`
|
||||
5. `LanMountainDesktop/Views/Splash/SplashWindow.axaml.cs`
|
||||
6. `LanMountainDesktop/Services/Update/UpdateService.cs`
|
||||
7. `LanMountainDesktop/Services/Update/LauncherUpdateService.cs`
|
||||
8. `LanMountainDesktop/Services/Plugins/PluginUpdateService.cs`
|
||||
|
||||
### 删除文件
|
||||
|
||||
1. `LanMountainDesktop.Launcher/App.axaml`
|
||||
2. `LanMountainDesktop.Launcher/App.axaml.cs`
|
||||
3. `LanMountainDesktop.Launcher/Views/` 目录
|
||||
4. `LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs`
|
||||
5. `LanMountainDesktop.Launcher/Services/OobeStateService.cs`
|
||||
6. `LanMountainDesktop.Launcher/Services/UpdateCheckService.cs`
|
||||
7. `LanMountainDesktop.Launcher/Services/UpdateEngineService.cs`
|
||||
8. `LanMountainDesktop.Launcher/Services/PluginInstallerService.cs`
|
||||
9. `LanMountainDesktop.Launcher/Services/PluginUpgradeQueueService.cs`
|
||||
|
||||
---
|
||||
|
||||
## 风险与回滚方案
|
||||
|
||||
### 风险
|
||||
|
||||
1. **启动失败**: 新 Launcher 可能有 bug 导致无法启动
|
||||
2. **更新中断**: 更新逻辑迁移可能导致更新失败
|
||||
3. **图标丢失**: 图标配置错误导致快捷方式无图标
|
||||
|
||||
### 回滚方案
|
||||
|
||||
1. 保留原 Launcher 代码分支
|
||||
2. 准备紧急修复版本
|
||||
3. 用户可手动下载完整安装包恢复
|
||||
|
||||
---
|
||||
|
||||
## 验证清单
|
||||
|
||||
- [ ] Launcher 能正常启动主程序
|
||||
- [ ] 版本选择逻辑正确
|
||||
- [ ] 旧版本清理正常
|
||||
- [ ] OOBE 流程正常
|
||||
- [ ] Splash 显示正常
|
||||
- [ ] 更新检查正常
|
||||
- [ ] 插件安装正常
|
||||
- [ ] GitHub Actions 打包成功
|
||||
- [ ] 安装程序图标正常
|
||||
- [ ] 快捷方式图标正常
|
||||
|
||||
---
|
||||
|
||||
## 实施顺序建议
|
||||
|
||||
### 第一阶段(立即实施)
|
||||
1. 重写 Launcher Program.cs
|
||||
2. 修改 Launcher.csproj
|
||||
3. 移除主程序对 Launcher 的引用
|
||||
4. 测试基本启动功能
|
||||
|
||||
### 第二阶段(功能迁移)
|
||||
1. 迁移 OOBE 到主程序
|
||||
2. 迁移 Splash 到主程序
|
||||
3. 迁移更新逻辑到主程序
|
||||
4. 迁移插件管理到主程序
|
||||
|
||||
### 第三阶段(CI/CD)
|
||||
1. 修改 release.yml
|
||||
2. 修改 build.yml
|
||||
3. 测试打包流程
|
||||
4. 验证安装程序
|
||||
|
||||
### 第四阶段(优化)
|
||||
1. 实现 Launcher 自更新
|
||||
2. 性能优化
|
||||
3. 清理旧代码
|
||||
717
.trae/documents/launcher_improved_plan_v2.md
Normal file
717
.trae/documents/launcher_improved_plan_v2.md
Normal file
@@ -0,0 +1,717 @@
|
||||
# LanMountainDesktop Launcher 改进计划 V2
|
||||
|
||||
## 核心设计理念
|
||||
|
||||
**Launcher 是核心协调器,不是极简启动器**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Launcher 职责定位 │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Launcher 负责(启动前 & 退出后): │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ • OOBE 首次引导 │ │
|
||||
│ │ • 启动动画 (Splash) │ │
|
||||
│ │ • 插件安装 │ │
|
||||
│ │ • 插件更新 │ │
|
||||
│ │ • 应用增量更新安装(不是下载!) │ │
|
||||
│ │ • 应用静默更新安装 │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 主程序负责(运行时): │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ • 多线程下载(有完整 Downloader) │ │
|
||||
│ │ • 更新渠道切换 │ │
|
||||
│ │ • 下载管理 │ │
|
||||
│ │ • 与 Launcher 通讯(启动进度) │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 关键优势: │
|
||||
│ • Launcher 在应用启动前运行 → 可以安装更新而不担心文件占用 │
|
||||
│ • Launcher 在应用退出后运行 → 可以完成待处理的安装任务 │
|
||||
│ • 主程序专注下载 → 利用完整的多线程下载器提高效率 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 为什么保留 Avalonia?
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 保留 Avalonia 的理由 │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. 启动画面 (Splash) │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ • 需要显示启动进度 │ │
|
||||
│ │ • 需要显示品牌 Logo │ │
|
||||
│ │ • 需要流畅的动画效果 │ │
|
||||
│ │ • 纯 Win32 实现复杂且不易维护 │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 2. OOBE 首次引导 │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ • 需要多步骤向导界面 │ │
|
||||
│ │ • 需要丰富的交互控件 │ │
|
||||
│ │ • 需要与主程序一致的视觉风格 │ │
|
||||
│ │ • Avalonia 提供完整的 UI 框架 │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 3. 与主程序的技术栈一致 │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ • 共享主题和资源 │ │
|
||||
│ │ • 共享控件和样式 │ │
|
||||
│ │ • 便于维护和迭代 │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 改进后的架构设计
|
||||
|
||||
### 目录结构(保持不变)
|
||||
|
||||
```
|
||||
安装根目录/
|
||||
├── LanMountainDesktop.exe ← Launcher(Avalonia 应用)
|
||||
├── app-1.0.0/ ← 版本目录
|
||||
│ ├── .current ← 当前版本标记
|
||||
│ ├── LanMountainDesktop.exe ← 主程序
|
||||
│ └── ... (所有依赖)
|
||||
└── .launcher/ ← Launcher 数据目录
|
||||
├── update/ ← 更新缓存
|
||||
│ └── incoming/ ← 下载的更新包(主程序下载到这里)
|
||||
└── snapshots/ ← 版本快照
|
||||
```
|
||||
|
||||
### 核心流程设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 启动流程(含通讯机制) │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. 用户启动 LanMountainDesktop.exe (Launcher) │
|
||||
│ ↓ │
|
||||
│ 2. Launcher 检查是否有待处理的更新安装 │
|
||||
│ ↓ │
|
||||
│ 3. 有更新?──Yes──▶ 显示 Splash "正在安装更新..." │
|
||||
│ ↓ ↓ │
|
||||
│ No 安装更新(增量/静默) │
|
||||
│ ↓ ↓ │
|
||||
│ 4. 检查是否首次运行 ──Yes──▶ 显示 OOBE 窗口 │
|
||||
│ ↓ No ↓ │
|
||||
│ 5. 显示 Splash "正在启动..." 完成 OOBE │
|
||||
│ ↓ │
|
||||
│ 6. 启动主程序进程(带通讯参数) │
|
||||
│ ↓ │
|
||||
│ 7. Launcher 保持运行,监听主程序进度 ─────── IPC 通讯 ───────▶ 主程序 │
|
||||
│ ↓ │
|
||||
│ 8. 主程序报告启动进度 ─────── IPC 通讯 ───────▶ Launcher 更新 Splash │
|
||||
│ ↓ │
|
||||
│ 9. 主程序完全启动 ──Yes──▶ Launcher 关闭 Splash,进入后台/退出 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 退出流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 退出流程(处理待安装任务) │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. 主程序准备退出 │
|
||||
│ ↓ │
|
||||
│ 2. 检查是否有待安装的更新/插件 ──Yes──▶ 重启 Launcher 并传递参数 │
|
||||
│ ↓ No ↓ │
|
||||
│ 3. 正常退出 Launcher 在应用退出后运行 │
|
||||
│ ↓ │
|
||||
│ 安装待处理的任务 │
|
||||
│ ↓ │
|
||||
│ 完成后再次启动主程序 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Launcher 与主程序的通讯机制
|
||||
|
||||
### IPC 方案选择
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ IPC 通讯方案 │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 方案 1: 命令行参数 + 退出码(推荐用于启动阶段) │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Launcher 启动主程序: │ │
|
||||
│ │ LanMountainDesktop.exe --launcher-pid 12345 --ipc-port 50000 │ │
|
||||
│ │ │ │
|
||||
│ │ 主程序通过命名管道/HTTP 与 Launcher 通讯 │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 方案 2: 命名管道(推荐用于进度报告) │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Launcher 创建命名管道: \\.\pipe\LanMountainDesktop_Launcher │ │
|
||||
│ │ 主程序连接并发送进度消息 │ │
|
||||
│ │ │ │
|
||||
│ │ 消息格式: JSON │ │
|
||||
│ │ {"stage": "initializing", "progress": 30, "message": "加载设置..."} │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 方案 3: 共享内存/文件(简单状态同步) │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Launcher 和主程序读写同一个状态文件 │ │
|
||||
│ │ .launcher/state/startup_status.json │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 通讯协议设计
|
||||
|
||||
```csharp
|
||||
// 共享契约(LanMountainDesktop.Shared.Contracts)
|
||||
namespace LanMountainDesktop.Shared.Contracts.Launcher;
|
||||
|
||||
public enum StartupStage
|
||||
{
|
||||
Initializing,
|
||||
LoadingSettings,
|
||||
LoadingPlugins,
|
||||
InitializingUI,
|
||||
Ready
|
||||
}
|
||||
|
||||
public record StartupProgressMessage
|
||||
{
|
||||
public StartupStage Stage { get; init; }
|
||||
public int ProgressPercent { get; init; } // 0-100
|
||||
public string? Message { get; init; }
|
||||
public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public static class LauncherIpc
|
||||
{
|
||||
public const string PipeName = "LanMountainDesktop_Launcher";
|
||||
public const string EnvironmentVariablePrefix = "LMD_";
|
||||
}
|
||||
```
|
||||
|
||||
## 详细实施步骤
|
||||
|
||||
### P0: 架构调整(核心)
|
||||
|
||||
#### 1. 调整 Launcher 项目引用
|
||||
|
||||
**文件**: `LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj`
|
||||
|
||||
**修改**:
|
||||
- 保留 Avalonia 依赖
|
||||
- 移除 PluginSdk 引用(Launcher 不需要)
|
||||
- 添加 Shared.Contracts 引用(用于 IPC)
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ApplicationIcon>Assetsogo_nightly.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- 保留 Avalonia -->
|
||||
<PackageReference Include="Avalonia" Version="11.3.12" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.12" />
|
||||
|
||||
<!-- 只引用 Shared.Contracts(IPC 协议) -->
|
||||
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- 图标资源 -->
|
||||
<ItemGroup>
|
||||
<Content Include="..\LanMountainDesktop\Assets\logo_nightly.ico" Link="Assets\logo_nightly.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
#### 2. 移除主程序对 Launcher 的引用
|
||||
|
||||
**文件**: `LanMountainDesktop/LanMountainDesktop.csproj`
|
||||
|
||||
**修改**: 删除 Launcher 引用
|
||||
```xml
|
||||
<!-- 删除 -->
|
||||
<!-- <ProjectReference Include="..\LanMountainDesktop.Launcher\LanMountainDesktop.Launcher.csproj" ReferenceOutputAssembly="false" /> -->
|
||||
```
|
||||
|
||||
#### 3. 创建 IPC 通讯契约
|
||||
|
||||
**新建文件**: `LanMountainDesktop.Shared.Contracts/Launcher/LauncherIpc.cs`
|
||||
|
||||
```csharp
|
||||
namespace LanMountainDesktop.Shared.Contracts.Launcher;
|
||||
|
||||
public enum StartupStage
|
||||
{
|
||||
Initializing,
|
||||
LoadingSettings,
|
||||
LoadingPlugins,
|
||||
InitializingUI,
|
||||
Ready
|
||||
}
|
||||
|
||||
public record StartupProgressMessage
|
||||
{
|
||||
public StartupStage Stage { get; init; }
|
||||
public int ProgressPercent { get; init; }
|
||||
public string? Message { get; init; }
|
||||
public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public static class LauncherIpcConstants
|
||||
{
|
||||
public const string PipeName = "LanMountainDesktop_Launcher";
|
||||
public const string LauncherPidEnvVar = "LMD_LAUNCHER_PID";
|
||||
public const string PackageRootEnvVar = "LMD_PACKAGE_ROOT";
|
||||
public const string VersionEnvVar = "LMD_VERSION";
|
||||
}
|
||||
```
|
||||
|
||||
### P1: Launcher 端实现
|
||||
|
||||
#### 4. 实现 IPC 服务端
|
||||
|
||||
**新建文件**: `LanMountainDesktop.Launcher/Services/Ipc/LauncherIpcServer.cs`
|
||||
|
||||
```csharp
|
||||
using System.IO.Pipes;
|
||||
using System.Text.Json;
|
||||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Services.Ipc;
|
||||
|
||||
public class LauncherIpcServer : IDisposable
|
||||
{
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private NamedPipeServerStream? _pipeServer;
|
||||
private readonly Action<StartupProgressMessage> _onProgress;
|
||||
|
||||
public LauncherIpcServer(Action<StartupProgressMessage> onProgress)
|
||||
{
|
||||
_onProgress = onProgress;
|
||||
}
|
||||
|
||||
public async Task StartAsync()
|
||||
{
|
||||
while (!_cts.Token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_pipeServer = new NamedPipeServerStream(
|
||||
LauncherIpcConstants.PipeName,
|
||||
PipeDirection.In,
|
||||
1,
|
||||
PipeTransmissionMode.Message);
|
||||
|
||||
await _pipeServer.WaitForConnectionAsync(_cts.Token);
|
||||
|
||||
using var reader = new StreamReader(_pipeServer);
|
||||
var json = await reader.ReadToEndAsync(_cts.Token);
|
||||
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
var message = JsonSerializer.Deserialize<StartupProgressMessage>(json);
|
||||
if (message != null)
|
||||
{
|
||||
_onProgress(message);
|
||||
}
|
||||
}
|
||||
|
||||
_pipeServer.Disconnect();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"IPC error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_pipeServer?.Dispose();
|
||||
_cts.Dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. 修改 Launcher 启动流程
|
||||
|
||||
**修改文件**: `LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs`
|
||||
|
||||
```csharp
|
||||
public async Task<LauncherResult> RunAsync()
|
||||
{
|
||||
// 1. 清理旧版本
|
||||
_deploymentLocator.CleanupDestroyedDeployments();
|
||||
|
||||
// 2. 检查并安装待处理的更新(主程序下载的)
|
||||
var pendingUpdate = _updateEngine.CheckPendingUpdate();
|
||||
if (pendingUpdate.HasUpdate)
|
||||
{
|
||||
_splashWindow?.UpdateStatus("正在安装更新...");
|
||||
var updateResult = await _updateEngine.ApplyPendingUpdateAsync();
|
||||
if (!updateResult.Success)
|
||||
{
|
||||
return updateResult;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查并安装待处理的插件更新
|
||||
var pendingPlugins = _pluginUpgradeQueueService.CheckPendingUpgrades();
|
||||
if (pendingPlugins.HasUpgrades)
|
||||
{
|
||||
_splashWindow?.UpdateStatus("正在更新插件...");
|
||||
var pluginResult = _pluginUpgradeQueueService.ApplyPendingUpgrades();
|
||||
if (!pluginResult.Success)
|
||||
{
|
||||
return pluginResult;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. OOBE
|
||||
if (_oobeStateService.IsFirstRun())
|
||||
{
|
||||
_splashWindow?.Hide();
|
||||
foreach (var step in _oobeSteps)
|
||||
{
|
||||
await step.RunAsync(CancellationToken.None);
|
||||
}
|
||||
_splashWindow?.Show();
|
||||
}
|
||||
|
||||
// 5. 启动 IPC 服务端监听主程序进度
|
||||
using var ipcServer = new LauncherIpcServer(msg =>
|
||||
{
|
||||
_splashWindow?.UpdateProgress(msg.ProgressPercent, msg.Message);
|
||||
});
|
||||
_ = ipcServer.StartAsync();
|
||||
|
||||
// 6. 启动主程序
|
||||
_splashWindow?.UpdateStatus("正在启动...");
|
||||
var hostResult = LaunchHostWithIpc();
|
||||
if (!hostResult.Success)
|
||||
{
|
||||
return hostResult;
|
||||
}
|
||||
|
||||
// 7. 等待主程序报告就绪或超时
|
||||
await WaitForHostReadyOrTimeoutAsync(TimeSpan.FromSeconds(30));
|
||||
|
||||
return new LauncherResult { Success = true };
|
||||
}
|
||||
```
|
||||
|
||||
### P2: 主程序端实现
|
||||
|
||||
#### 6. 实现 IPC 客户端
|
||||
|
||||
**新建文件**: `LanMountainDesktop/Services/Launcher/LauncherIpcClient.cs`
|
||||
|
||||
```csharp
|
||||
using System.IO.Pipes;
|
||||
using System.Text.Json;
|
||||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||||
|
||||
namespace LanMountainDesktop.Services.Launcher;
|
||||
|
||||
public class LauncherIpcClient : IDisposable
|
||||
{
|
||||
private NamedPipeClientStream? _pipeClient;
|
||||
|
||||
public async Task ConnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_pipeClient = new NamedPipeClientStream(
|
||||
".",
|
||||
LauncherIpcConstants.PipeName,
|
||||
PipeDirection.Out);
|
||||
|
||||
await _pipeClient.ConnectAsync(5000, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ReportProgressAsync(StartupProgressMessage message)
|
||||
{
|
||||
if (_pipeClient?.IsConnected != true)
|
||||
return;
|
||||
|
||||
var json = JsonSerializer.Serialize(message);
|
||||
using var writer = new StreamWriter(_pipeClient, leaveOpen: true);
|
||||
await writer.WriteAsync(json);
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_pipeClient?.Dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 7. 主程序启动时报告进度
|
||||
|
||||
**修改文件**: `LanMountainDesktop/App.axaml.cs`
|
||||
|
||||
```csharp
|
||||
public override async void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
// 检查是否从 Launcher 启动
|
||||
var launcherPid = Environment.GetEnvironmentVariable(LauncherIpcConstants.LauncherPidEnvVar);
|
||||
if (!string.IsNullOrEmpty(launcherPid))
|
||||
{
|
||||
// 连接到 Launcher 的 IPC 服务端
|
||||
_launcherIpc = new LauncherIpcClient();
|
||||
await _launcherIpc.ConnectAsync();
|
||||
|
||||
// 报告启动进度
|
||||
await _launcherIpc.ReportProgressAsync(new StartupProgressMessage
|
||||
{
|
||||
Stage = StartupStage.Initializing,
|
||||
ProgressPercent = 10,
|
||||
Message = "正在初始化..."
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化设置
|
||||
await _launcherIpc?.ReportProgressAsync(new StartupProgressMessage
|
||||
{
|
||||
Stage = StartupStage.LoadingSettings,
|
||||
ProgressPercent = 30,
|
||||
Message = "正在加载设置..."
|
||||
});
|
||||
InitializeSettings();
|
||||
|
||||
// 加载插件
|
||||
await _launcherIpc?.ReportProgressAsync(new StartupProgressMessage
|
||||
{
|
||||
Stage = StartupStage.LoadingPlugins,
|
||||
ProgressPercent = 50,
|
||||
Message = "正在加载插件..."
|
||||
});
|
||||
await InitializePluginsAsync();
|
||||
|
||||
// 初始化 UI
|
||||
await _launcherIpc?.ReportProgressAsync(new StartupProgressMessage
|
||||
{
|
||||
Stage = StartupStage.InitializingUI,
|
||||
ProgressPercent = 80,
|
||||
Message = "正在初始化界面..."
|
||||
});
|
||||
InitializeUI();
|
||||
|
||||
// 就绪
|
||||
await _launcherIpc?.ReportProgressAsync(new StartupProgressMessage
|
||||
{
|
||||
Stage = StartupStage.Ready,
|
||||
ProgressPercent = 100,
|
||||
Message = "就绪"
|
||||
});
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
```
|
||||
|
||||
### P3: 更新流程整合
|
||||
|
||||
#### 8. 主程序下载更新
|
||||
|
||||
**主程序职责**:
|
||||
```csharp
|
||||
// 主程序中的更新服务
|
||||
public class AppUpdateService
|
||||
{
|
||||
public async Task DownloadUpdateAsync(string version, string downloadUrl)
|
||||
{
|
||||
// 使用多线程下载器下载更新包
|
||||
var downloader = new MultiThreadedDownloader();
|
||||
var targetPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"LanMountainDesktop",
|
||||
".launcher",
|
||||
"update",
|
||||
"incoming",
|
||||
$"update-{version}.zip");
|
||||
|
||||
await downloader.DownloadAsync(downloadUrl, targetPath);
|
||||
|
||||
// 标记为待安装
|
||||
File.WriteAllText(
|
||||
Path.Combine(Path.GetDirectoryName(targetPath)!, ".pending"),
|
||||
version);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 9. Launcher 安装更新
|
||||
|
||||
**Launcher 职责**:
|
||||
```csharp
|
||||
// Launcher 中的更新安装服务
|
||||
public class UpdateInstallationService
|
||||
{
|
||||
public async Task<InstallResult> InstallPendingUpdateAsync()
|
||||
{
|
||||
var pendingPath = Path.Combine(
|
||||
_appRoot,
|
||||
".launcher",
|
||||
"update",
|
||||
"incoming",
|
||||
".pending");
|
||||
|
||||
if (!File.Exists(pendingPath))
|
||||
return InstallResult.NoUpdate;
|
||||
|
||||
var version = File.ReadAllText(pendingPath);
|
||||
var updatePackagePath = Path.Combine(
|
||||
Path.GetDirectoryName(pendingPath)!,
|
||||
$"update-{version}.zip");
|
||||
|
||||
// 创建新版本目录
|
||||
var newVersionDir = Path.Combine(_appRoot, $"app-{version}");
|
||||
Directory.CreateDirectory(newVersionDir);
|
||||
File.WriteAllText(Path.Combine(newVersionDir, ".partial"), "");
|
||||
|
||||
// 解压更新包
|
||||
ZipFile.ExtractToDirectory(updatePackagePath, newVersionDir);
|
||||
|
||||
// 验证文件完整性
|
||||
// ...
|
||||
|
||||
// 切换版本标记
|
||||
var currentDir = _deploymentLocator.FindCurrentDeploymentDirectory();
|
||||
if (currentDir != null)
|
||||
{
|
||||
File.Delete(Path.Combine(currentDir, ".current"));
|
||||
File.WriteAllText(Path.Combine(currentDir, ".destroy"), "");
|
||||
}
|
||||
|
||||
File.WriteAllText(Path.Combine(newVersionDir, ".current"), "");
|
||||
File.Delete(Path.Combine(newVersionDir, ".partial"));
|
||||
|
||||
// 清理待安装标记
|
||||
File.Delete(pendingPath);
|
||||
File.Delete(updatePackagePath);
|
||||
|
||||
return InstallResult.Success;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### P4: GitHub Actions 工作流
|
||||
|
||||
#### 10. 修改 release.yml
|
||||
|
||||
**关键修改点**:
|
||||
|
||||
```yaml
|
||||
# 1. Launcher 单独编译(保留 Avalonia)
|
||||
- name: Publish Launcher
|
||||
run: |
|
||||
dotnet publish LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj `
|
||||
-c Release `
|
||||
-o ./publish/launcher-win-x64 `
|
||||
--self-contained `
|
||||
-r win-x64 `
|
||||
-p:PublishSingleFile=false `
|
||||
-p:DebugType=none
|
||||
|
||||
# 2. 目录结构调整
|
||||
- name: Restructure for Launcher
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$publishDir = "publish/windows-x64"
|
||||
$launcherDir = "publish/launcher-win-x64"
|
||||
$appDir = "app-$version"
|
||||
|
||||
# 创建新结构
|
||||
$newStructure = "publish-launcher/windows-x64"
|
||||
New-Item -ItemType Directory -Path $newStructure -Force
|
||||
|
||||
# 移动主程序到 app-{version}/
|
||||
$appPath = Join-Path $newStructure $appDir
|
||||
Move-Item -Path $publishDir -Destination $appPath -Force
|
||||
|
||||
# 复制 Launcher 到根目录
|
||||
Copy-Item -Path "$launcherDir\*" -Destination $newStructure -Recurse -Force
|
||||
|
||||
# 创建 .current 标记
|
||||
New-Item -ItemType File -Path (Join-Path $appPath ".current") -Force
|
||||
```
|
||||
|
||||
## 文件变更清单
|
||||
|
||||
### 修改文件
|
||||
|
||||
1. `LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj` - 调整引用
|
||||
2. `LanMountainDesktop/LanMountainDesktop.csproj` - 移除 Launcher 引用
|
||||
3. `LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs` - 添加 IPC 和更新安装
|
||||
4. `LanMountainDesktop/App.axaml.cs` - 添加 IPC 客户端和进度报告
|
||||
5. `.github/workflows/release.yml` - 调整打包流程
|
||||
|
||||
### 新增文件
|
||||
|
||||
1. `LanMountainDesktop.Shared.Contracts/Launcher/LauncherIpc.cs` - IPC 契约
|
||||
2. `LanMountainDesktop.Launcher/Services/Ipc/LauncherIpcServer.cs` - IPC 服务端
|
||||
3. `LanMountainDesktop/Services/Launcher/LauncherIpcClient.cs` - IPC 客户端
|
||||
4. `LanMountainDesktop.Launcher/Services/Update/UpdateInstallationService.cs` - 更新安装
|
||||
|
||||
### 删除文件
|
||||
|
||||
1. 主程序对 Launcher 的项目引用(已存在)
|
||||
|
||||
## 实施顺序
|
||||
|
||||
### 第一阶段:基础架构
|
||||
1. 创建 IPC 契约(Shared.Contracts)
|
||||
2. 调整 Launcher 项目引用
|
||||
3. 移除主程序对 Launcher 的引用
|
||||
4. 测试基本启动
|
||||
|
||||
### 第二阶段:IPC 实现
|
||||
1. 实现 Launcher IPC 服务端
|
||||
2. 实现主程序 IPC 客户端
|
||||
3. 测试进度报告
|
||||
|
||||
### 第三阶段:更新流程
|
||||
1. 主程序实现下载功能
|
||||
2. Launcher 实现安装功能
|
||||
3. 测试完整更新流程
|
||||
|
||||
### 第四阶段:CI/CD
|
||||
1. 修改 GitHub Actions
|
||||
2. 测试打包流程
|
||||
3. 验证安装程序
|
||||
|
||||
## 验证清单
|
||||
|
||||
- [ ] Launcher 能正常启动主程序
|
||||
- [ ] Launcher 显示 Splash 并接收进度更新
|
||||
- [ ] 主程序能向 Launcher 报告启动进度
|
||||
- [ ] 主程序能下载更新
|
||||
- [ ] Launcher 能安装待处理的更新
|
||||
- [ ] OOBE 流程正常
|
||||
- [ ] 插件更新流程正常
|
||||
- [ ] GitHub Actions 打包成功
|
||||
- [ ] 安装程序图标正常
|
||||
- [ ] 快捷方式图标正常
|
||||
8
.trae/specs/launcher-upgrade/checklist.md
Normal file
8
.trae/specs/launcher-upgrade/checklist.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Launcher Upgrade Checklist
|
||||
|
||||
- [x] Build passes for `LanMountainDesktop.Launcher`.
|
||||
- [x] `update check` command returns structured JSON result.
|
||||
- [x] `plugin update` command returns structured JSON result.
|
||||
- [x] Legacy plugin install arguments still execute.
|
||||
- [x] OOBE and splash are implemented as separate windows.
|
||||
- [x] Update and rollback logic use version directory markers.
|
||||
54
.trae/specs/launcher-upgrade/spec.md
Normal file
54
.trae/specs/launcher-upgrade/spec.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Launcher Upgrade Spec
|
||||
|
||||
## Goal
|
||||
|
||||
Upgrade `LanMountainDesktop.Launcher` into the unified Launcher for:
|
||||
|
||||
- OOBE first-run entry
|
||||
- startup splash window
|
||||
- silent/incremental/rollback update
|
||||
- plugin install/update
|
||||
|
||||
## Scope (Phase 1)
|
||||
|
||||
- Avalonia GUI launcher with two windows:
|
||||
- `OOBEWindow` (first run only)
|
||||
- `SplashWindow` (every launch)
|
||||
- Default command `launch`
|
||||
- CLI commands:
|
||||
- `update check|download|apply|rollback`
|
||||
- `plugin install|update`
|
||||
- Legacy compatibility:
|
||||
- `--source --plugins-dir --result` still works for plugin install
|
||||
|
||||
## Update Behavior
|
||||
|
||||
- ClassIsland-style deployment folders:
|
||||
- `app-<version>-<number>/`
|
||||
- marker files `.current`, `.partial`, `.destroy`
|
||||
- Signed file map:
|
||||
- `files.json`
|
||||
- `files.json.sig`
|
||||
- `public-key.pem`
|
||||
- Incremental update:
|
||||
- `replace` from archive
|
||||
- `reuse` from current deployment
|
||||
- `delete` skip file in target deployment
|
||||
- Rollback:
|
||||
- snapshot metadata is written before apply
|
||||
- automatic rollback on apply failure
|
||||
- manual rollback via command
|
||||
|
||||
## OOBE and Splash
|
||||
|
||||
- OOBE is independent from splash.
|
||||
- OOBE shows only:
|
||||
- welcome text: `欢迎使用阑山桌面`
|
||||
- arrow button for continue
|
||||
- Splash shows only:
|
||||
- app name: `阑山桌面`
|
||||
|
||||
## Extensibility
|
||||
|
||||
- `IOobeStep` for future multi-step OOBE
|
||||
- `ISplashStageReporter` for future startup progress visualization
|
||||
12
.trae/specs/launcher-upgrade/tasks.md
Normal file
12
.trae/specs/launcher-upgrade/tasks.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Launcher Upgrade Tasks
|
||||
|
||||
- [x] Convert `LanMountainDesktop.Launcher` to Avalonia launcher entry.
|
||||
- [x] Add OOBE window with first-run marker handling.
|
||||
- [x] Add splash window for every startup.
|
||||
- [x] Implement unified command parsing with default `launch`.
|
||||
- [x] Keep legacy plugin install args compatibility.
|
||||
- [x] Add plugin pending upgrade queue processing.
|
||||
- [x] Implement incremental update apply with signed file map.
|
||||
- [x] Implement snapshot-based rollback and manual rollback command.
|
||||
- [x] Add update check/download/apply/rollback CLI commands.
|
||||
- [x] Add launcher spec files under `.trae/specs/launcher-upgrade/`.
|
||||
13
.trae/specs/pdc-incremental-migration/checklist.md
Normal file
13
.trae/specs/pdc-incremental-migration/checklist.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Checklist
|
||||
|
||||
- [ ] `release.yml` includes PDCC publish flow and does not invoke Velopack.
|
||||
- [ ] `release.yml` uploads app payload artifacts for PDCC.
|
||||
- [ ] S3 output path is rooted at `lanmountain/update/` (no system version prefix).
|
||||
- [ ] S3 has `repo/`, `meta/`, and `installers/` outputs after a release run.
|
||||
- [ ] Host update source default is `stcn` and old `pdc` values are auto-normalized.
|
||||
- [ ] Host can persist PDC payload into launcher incoming directory.
|
||||
- [ ] Launcher can apply PDC FileMap payload with signature/hash verification.
|
||||
- [ ] Legacy signed `files.json + update.zip` path still works as compatibility fallback.
|
||||
- [ ] CI run attached proving all release matrix jobs pass.
|
||||
- [ ] N-1 -> N incremental update verified on Windows x64/x86 and Linux x64.
|
||||
- [ ] Rollback verification report attached.
|
||||
44
.trae/specs/pdc-incremental-migration/spec.md
Normal file
44
.trae/specs/pdc-incremental-migration/spec.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# PDC Incremental Update Migration
|
||||
|
||||
## Goal
|
||||
|
||||
Replace VeloPack-based incremental packaging with a unified PDC FileMap + object-repo pipeline, while keeping Launcher installation, rollback, and update orchestration ownership unchanged.
|
||||
|
||||
## Stage 1 (Completed)
|
||||
|
||||
- Release workflow removed VeloPack-based release packaging.
|
||||
- Signed FileMap path was restored as an interim release mechanism.
|
||||
- Host/Launcher fallback behavior stayed compatible with `files.json + files.json.sig + update.zip`.
|
||||
|
||||
## Stage 2 (Current Implementation Target)
|
||||
|
||||
- Move release publishing to PDCC + `phainon.yml` (ClassIsland-style).
|
||||
- Promote PDC-distributed FileMap/object-repo as the primary incremental path.
|
||||
- Keep GitHub Release installers and metadata as parallel distribution.
|
||||
- Keep Launcher state machine ownership (`.current/.partial/.destroy` + snapshots).
|
||||
- Update source defaults to `stcn` (S3/PDC), with GitHub fallback.
|
||||
- S3 object root is fixed to `lanmountain/update/` with no update-system version prefix.
|
||||
|
||||
Expected S3 layout:
|
||||
- `lanmountain/update/repo/<hash-prefix>/<hash-object>`
|
||||
- `lanmountain/update/meta/channels/<channel>/<subchannel>/latest.json`
|
||||
- `lanmountain/update/meta/distributions/<distributionId>/*.json`
|
||||
- `lanmountain/update/installers/<platform>/<arch>/*`
|
||||
|
||||
## Acceptance
|
||||
|
||||
- `release.yml` includes PDCC publish steps and no Velopack steps.
|
||||
- Release jobs keep building installers for Windows x64/x86, Linux x64, and macOS.
|
||||
- PDC metadata + FileMap + object repo are published under `lanmountain/update/`.
|
||||
- Host can consume PDC payload (`stcn` source) and fallback to GitHub when unavailable.
|
||||
- Launcher can apply both:
|
||||
- legacy signed `files.json + update.zip`
|
||||
- PDC FileMap object-repo payload.
|
||||
- Rollback semantics remain unchanged.
|
||||
|
||||
## Deprecated Notes
|
||||
|
||||
- The following interim outputs are compatibility-only (not the long-term primary path):
|
||||
- `files-windows-x64.json` / `.sig` / `update-windows-x64.zip`
|
||||
- `files-windows-x86.json` / `.sig` / `update-windows-x86.zip`
|
||||
- `files-linux-x64.json` / `.sig` / `update-linux-x64.zip`
|
||||
15
.trae/specs/pdc-incremental-migration/tasks.md
Normal file
15
.trae/specs/pdc-incremental-migration/tasks.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Tasks
|
||||
|
||||
- [x] Remove VeloPack packaging from release workflow.
|
||||
- [x] Keep signed FileMap path as interim compatibility fallback.
|
||||
- [x] Remove launcher/runtime Velopack branching.
|
||||
- [ ] Add `phainon.yml` for PDCC publish configuration.
|
||||
- [ ] Add PDCC installation + publish steps in `release.yml`.
|
||||
- [ ] Upload app payload artifacts for PDCC consumption in release build jobs.
|
||||
- [ ] Publish PDC metadata + object repo to S3 path root `lanmountain/update/`.
|
||||
- [ ] Mirror installers to `lanmountain/update/installers/<platform>/<arch>/`.
|
||||
- [ ] Replace update source canonical value with `stcn` (keep legacy `pdc` compatibility).
|
||||
- [ ] Add PDC payload model into host update check result.
|
||||
- [ ] Add host download path for PDC payload (`pdc-filemap.json` + signature + metadata).
|
||||
- [ ] Add launcher PDC FileMap apply path with rollback-compatible semantics.
|
||||
- [ ] Keep old `files.json + update.zip` path behind compatibility fallback.
|
||||
5
.trae/specs/velopack-update-integration/checklist.md
Normal file
5
.trae/specs/velopack-update-integration/checklist.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Checklist (Deprecated)
|
||||
|
||||
- [x] Spec marked as deprecated.
|
||||
- [x] Active implementation ownership moved to `pdc-incremental-migration`.
|
||||
- [x] No release workflow dependency remains on VeloPack.
|
||||
15
.trae/specs/velopack-update-integration/spec.md
Normal file
15
.trae/specs/velopack-update-integration/spec.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# VeloPack Update Integration (Deprecated)
|
||||
|
||||
## Status
|
||||
|
||||
This spec is deprecated and superseded by `.trae/specs/pdc-incremental-migration/`.
|
||||
|
||||
## Deprecation Reason
|
||||
|
||||
- VeloPack native package generation introduced unstable release blocking (version format coupling and platform divergence).
|
||||
- The project has switched back to signed FileMap incremental assets as the primary update path.
|
||||
- Launcher remains the update installer/rollback authority; packaging and distribution are being migrated to PDC/S3-compatible flows.
|
||||
|
||||
## Migration Note
|
||||
|
||||
Use `.trae/specs/pdc-incremental-migration/spec.md` as the active authority for incremental update implementation and acceptance.
|
||||
6
.trae/specs/velopack-update-integration/tasks.md
Normal file
6
.trae/specs/velopack-update-integration/tasks.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Tasks (Deprecated)
|
||||
|
||||
- [x] Mark VeloPack integration spec as deprecated.
|
||||
- [x] Remove VeloPack runtime branches from launcher/host update path.
|
||||
- [x] Remove VeloPack release workflow packaging steps.
|
||||
- [ ] Keep archive for historical context only (no new implementation tasks here).
|
||||
Reference in New Issue
Block a user