diff --git a/.trae/documents/launcher_comprehensive_improvement_plan.md b/.trae/documents/launcher_comprehensive_improvement_plan.md new file mode 100644 index 0000000..6b67c90 --- /dev/null +++ b/.trae/documents/launcher_comprehensive_improvement_plan.md @@ -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 + + + WinExe + net10.0 + enable + enable + 1.0.0 + Assets\logo_nightly.ico + + + + + + PreserveNewest + + + +``` + +#### 3. 移除主程序对 Launcher 的引用 + +**文件**: `LanMountainDesktop/LanMountainDesktop.csproj` + +**修改**: 删除以下行 +```xml + + +``` + +#### 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 + + + + +