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
+
+
+
+
+
+
+
+```
+
+**修改文件**: `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
+
+
+
+
+
+
+
+
+
+```
+
+**修改文件**: `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 CheckForUpdateAsync(UpdateChannel channel)
+ {
+ // 调用 GitHub Release API
+ var releases = await _httpClient.GetFromJsonAsync>(
+ "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 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 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
+
+
+
+ PreserveNewest
+
+
+```
+
+#### 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. 清理旧代码
diff --git a/.trae/documents/launcher_improved_plan_v2.md b/.trae/documents/launcher_improved_plan_v2.md
new file mode 100644
index 0000000..b9874c5
--- /dev/null
+++ b/.trae/documents/launcher_improved_plan_v2.md
@@ -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
+
+
+ WinExe
+ net10.0
+ enable
+ enable
+ Assetsogo_nightly.ico
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+```
+
+#### 2. 移除主程序对 Launcher 的引用
+
+**文件**: `LanMountainDesktop/LanMountainDesktop.csproj`
+
+**修改**: 删除 Launcher 引用
+```xml
+
+
+```
+
+#### 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 _onProgress;
+
+ public LauncherIpcServer(Action 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(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 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 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 打包成功
+- [ ] 安装程序图标正常
+- [ ] 快捷方式图标正常
diff --git a/LanMountainDesktop.Launcher/App.axaml b/LanMountainDesktop.Launcher/App.axaml
index cbb832e..3fc30e9 100644
--- a/LanMountainDesktop.Launcher/App.axaml
+++ b/LanMountainDesktop.Launcher/App.axaml
@@ -1,8 +1,9 @@
-
+
diff --git a/LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj b/LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj
index 32742ce..efe7aa3 100644
--- a/LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj
+++ b/LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj
@@ -10,13 +10,14 @@
-
+
+
-
+
diff --git a/LanMountainDesktop.Launcher/Services/DeploymentLocator.cs b/LanMountainDesktop.Launcher/Services/DeploymentLocator.cs
index df3e489..c94be13 100644
--- a/LanMountainDesktop.Launcher/Services/DeploymentLocator.cs
+++ b/LanMountainDesktop.Launcher/Services/DeploymentLocator.cs
@@ -58,6 +58,8 @@ internal sealed class DeploymentLocator
public string? ResolveHostExecutablePath()
{
var executable = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop";
+
+ // 1. 首先查找 app-{version} 目录(生产环境)
var currentDeployment = FindCurrentDeploymentDirectory();
if (!string.IsNullOrWhiteSpace(currentDeployment))
{
@@ -68,15 +70,98 @@ internal sealed class DeploymentLocator
}
}
+ // 2. 查找 Launcher 所在目录(开发环境 - 直接运行)
var inRoot = Path.Combine(_appRoot, executable);
if (File.Exists(inRoot))
{
return inRoot;
}
+ // 3. 查找父目录(开发环境 - 从 Launcher 项目运行)
var parent = Path.GetFullPath(Path.Combine(_appRoot, ".."));
var inParent = Path.Combine(parent, executable);
- return File.Exists(inParent) ? inParent : null;
+ if (File.Exists(inParent))
+ {
+ return inParent;
+ }
+
+ // 4. 开发模式:如果启用了开发模式,优先扫描开发路径
+ if (Views.ErrorWindow.CheckDevModeEnabled())
+ {
+ var devPath = ScanDevelopmentPaths(executable);
+ if (!string.IsNullOrWhiteSpace(devPath))
+ {
+ return devPath;
+ }
+ }
+
+ // 5. 开发模式:查找主程序项目的输出目录
+ var devPaths = GetDevelopmentPaths(executable);
+ foreach (var devPath in devPaths)
+ {
+ if (File.Exists(devPath))
+ {
+ return devPath;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 扫描开发路径(开发模式)
+ ///
+ private static string? ScanDevelopmentPaths(string executable)
+ {
+ var possiblePaths = new[]
+ {
+ // 从 Launcher 项目运行
+ Path.Combine(AppContext.BaseDirectory, "..", "..", "LanMountainDesktop", "bin", "Debug", "net10.0", executable),
+ Path.Combine(AppContext.BaseDirectory, "..", "..", "LanMountainDesktop", "bin", "Release", "net10.0", executable),
+
+ // 从解决方案根目录运行
+ Path.Combine(AppContext.BaseDirectory, "..", "LanMountainDesktop", "bin", "Debug", "net10.0", executable),
+ Path.Combine(AppContext.BaseDirectory, "..", "LanMountainDesktop", "bin", "Release", "net10.0", executable),
+
+ // dev-test 目录
+ Path.Combine(AppContext.BaseDirectory, "..", "dev-test", "app-1.0.0-dev", executable),
+ };
+
+ foreach (var path in possiblePaths.Select(Path.GetFullPath).Distinct())
+ {
+ if (File.Exists(path))
+ {
+ return path;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 获取开发环境可能的主程序路径
+ ///
+ private static IEnumerable GetDevelopmentPaths(string executable)
+ {
+ // 获取 Launcher 所在目录
+ var launcherDir = AppContext.BaseDirectory;
+
+ // 可能的开发目录结构
+ var possiblePaths = new[]
+ {
+ // 从 Launcher 项目运行:..\LanMountainDesktop\bin\Debug\net10.0\LanMountainDesktop.exe
+ Path.Combine(launcherDir, "..", "..", "LanMountainDesktop", "bin", "Debug", "net10.0", executable),
+ Path.Combine(launcherDir, "..", "..", "LanMountainDesktop", "bin", "Release", "net10.0", executable),
+
+ // 从解决方案根目录运行:LanMountainDesktop\bin\Debug\net10.0\LanMountainDesktop.exe
+ Path.Combine(launcherDir, "..", "LanMountainDesktop", "bin", "Debug", "net10.0", executable),
+ Path.Combine(launcherDir, "..", "LanMountainDesktop", "bin", "Release", "net10.0", executable),
+
+ // 从 dev-test 目录运行
+ Path.Combine(launcherDir, "..", "dev-test", "app-1.0.0-dev", executable),
+ };
+
+ return possiblePaths.Select(Path.GetFullPath).Distinct();
}
public string GetCurrentVersion()
diff --git a/LanMountainDesktop.Launcher/Services/Ipc/LauncherIpcServer.cs b/LanMountainDesktop.Launcher/Services/Ipc/LauncherIpcServer.cs
new file mode 100644
index 0000000..61156a4
--- /dev/null
+++ b/LanMountainDesktop.Launcher/Services/Ipc/LauncherIpcServer.cs
@@ -0,0 +1,109 @@
+using System.IO.Pipes;
+using System.Text.Json;
+using LanMountainDesktop.Shared.Contracts.Launcher;
+
+namespace LanMountainDesktop.Launcher.Services.Ipc;
+
+///
+/// Launcher IPC 服务端 - 接收主程序的启动进度报告
+///
+public class LauncherIpcServer : IDisposable
+{
+ private readonly CancellationTokenSource _cts = new();
+ private NamedPipeServerStream? _pipeServer;
+ private readonly Action _onProgress;
+ private Task? _listenTask;
+
+ public LauncherIpcServer(Action onProgress)
+ {
+ _onProgress = onProgress;
+ }
+
+ ///
+ /// 启动 IPC 服务端监听
+ ///
+ public void Start()
+ {
+ _listenTask = Task.Run(async () =>
+ {
+ 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))
+ {
+ try
+ {
+ var message = JsonSerializer.Deserialize(json);
+ if (message != null)
+ {
+ _onProgress(message);
+ }
+ }
+ catch (JsonException)
+ {
+ // 忽略解析错误
+ }
+ }
+
+ try
+ {
+ _pipeServer.Disconnect();
+ }
+ catch { }
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (IOException)
+ {
+ // 管道断开,继续监听
+ continue;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"IPC error: {ex.Message}");
+ await Task.Delay(100, _cts.Token);
+ }
+ }
+ }, _cts.Token);
+ }
+
+ ///
+ /// 停止 IPC 服务端
+ ///
+ public void Stop()
+ {
+ _cts.Cancel();
+ try
+ {
+ _pipeServer?.Disconnect();
+ }
+ catch { }
+ }
+
+ public void Dispose()
+ {
+ Stop();
+ _pipeServer?.Dispose();
+ _cts.Dispose();
+
+ try
+ {
+ _listenTask?.Wait(TimeSpan.FromSeconds(2));
+ }
+ catch { }
+ }
+}
diff --git a/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs b/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs
index bf6a059..cb710f0 100644
--- a/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs
+++ b/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs
@@ -1,7 +1,9 @@
using System.Diagnostics;
using Avalonia.Threading;
using LanMountainDesktop.Launcher.Models;
+using LanMountainDesktop.Launcher.Services.Ipc;
using LanMountainDesktop.Launcher.Views;
+using LanMountainDesktop.Shared.Contracts.Launcher;
namespace LanMountainDesktop.Launcher.Services;
@@ -39,14 +41,7 @@ internal sealed class LauncherFlowCoordinator
// 清理待删除的旧版本
_deploymentLocator.CleanupDestroyedDeployments();
- if (_oobeStateService.IsFirstRun())
- {
- foreach (var step in _oobeSteps)
- {
- await step.RunAsync(CancellationToken.None).ConfigureAwait(false);
- }
- }
-
+ // 显示 Splash 窗口
var splashWindow = await Dispatcher.UIThread.InvokeAsync(() =>
{
var window = new SplashWindow();
@@ -55,17 +50,29 @@ internal sealed class LauncherFlowCoordinator
});
var reporter = (ISplashStageReporter)splashWindow;
+
+ // 启动 IPC 服务端监听主程序进度
+ using var ipcServer = new LauncherIpcServer(msg =>
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ reporter.Report(msg.Stage.ToString().ToLower(), msg.Message ?? "");
+ });
+ });
+ ipcServer.Start();
try
{
- reporter.Report("silentUpdate", "update");
+ // 检查并安装待处理的更新(主程序下载的)
+ reporter.Report("update", "检查更新...");
var updateResult = await _updateEngine.ApplyPendingUpdateAsync().ConfigureAwait(false);
if (!updateResult.Success)
{
return updateResult;
}
- reporter.Report("pluginTasks", "plugins");
+ // 检查并安装待处理的插件更新
+ reporter.Report("plugins", "检查插件更新...");
var pluginsDir = _context.GetOption("plugins-dir")
?? Path.Combine(_deploymentLocator.GetAppRoot(), "plugins");
var queueResult = new PluginUpgradeQueueService(_pluginInstallerService).ApplyPendingUpgrades(pluginsDir);
@@ -74,13 +81,28 @@ internal sealed class LauncherFlowCoordinator
return queueResult;
}
- reporter.Report("launchHost", "launch");
- var hostResult = LaunchHost();
+ // OOBE(首次运行引导)
+ if (_oobeStateService.IsFirstRun())
+ {
+ await Dispatcher.UIThread.InvokeAsync(() => splashWindow.Hide());
+ foreach (var step in _oobeSteps)
+ {
+ await step.RunAsync(CancellationToken.None).ConfigureAwait(false);
+ }
+ await Dispatcher.UIThread.InvokeAsync(() => splashWindow.Show());
+ }
+
+ // 启动主程序
+ reporter.Report("launch", "正在启动...");
+ var hostResult = await LaunchHostWithIpcAsync();
if (!hostResult.Success)
{
return hostResult;
}
+ // 等待主程序就绪或超时
+ await Task.Delay(TimeSpan.FromSeconds(30));
+
return new LauncherResult
{
Success = true,
@@ -107,11 +129,28 @@ internal sealed class LauncherFlowCoordinator
}
}
- private LauncherResult LaunchHost()
+ private async Task LaunchHostWithIpcAsync(string? customHostPath = null)
{
- var hostPath = _deploymentLocator.ResolveHostExecutablePath();
+ // 优先使用自定义路径(调试模式选择的路径)
+ var hostPath = customHostPath ?? _deploymentLocator.ResolveHostExecutablePath();
+
if (string.IsNullOrWhiteSpace(hostPath))
{
+ // 关闭 Splash 窗口
+ // 显示错误窗口而不是直接退出
+ var (errorResult, selectedPath) = await ShowHostNotFoundErrorAsync();
+
+ if (errorResult == ErrorWindowResult.Retry)
+ {
+ // 用户选择重试,如果有选择路径则使用,否则重新尝试
+ if (!string.IsNullOrWhiteSpace(selectedPath))
+ {
+ return await LaunchHostWithIpcAsync(selectedPath);
+ }
+ return await LaunchHostWithIpcAsync();
+ }
+
+ // 用户选择退出
return new LauncherResult
{
Success = false,
@@ -133,6 +172,12 @@ internal sealed class LauncherFlowCoordinator
WorkingDirectory = Path.GetDirectoryName(hostPath) ?? _deploymentLocator.GetAppRoot()
};
+ // 传递环境变量供 IPC 使用
+ processStartInfo.EnvironmentVariables[LauncherIpcConstants.LauncherPidEnvVar] =
+ Environment.ProcessId.ToString();
+ processStartInfo.EnvironmentVariables[LauncherIpcConstants.PackageRootEnvVar] =
+ _deploymentLocator.GetAppRoot();
+
Process.Start(processStartInfo);
return new LauncherResult
{
@@ -143,6 +188,26 @@ internal sealed class LauncherFlowCoordinator
};
}
+ ///
+ /// 显示找不到主程序的错误窗口
+ ///
+ private async Task<(ErrorWindowResult Result, string? CustomPath)> ShowHostNotFoundErrorAsync()
+ {
+ return await Dispatcher.UIThread.InvokeAsync(async () =>
+ {
+ var errorWindow = new ErrorWindow();
+ errorWindow.SetErrorMessage("找不到阑山桌面应用程序。");
+ errorWindow.Show();
+
+ var result = await errorWindow.WaitForChoiceAsync();
+ var customPath = errorWindow.GetCustomHostPath();
+
+ await Dispatcher.UIThread.InvokeAsync(() => errorWindow.Close());
+
+ return (result, customPath);
+ });
+ }
+
private static void EnsureExecutable(string path)
{
if (OperatingSystem.IsWindows())
diff --git a/LanMountainDesktop.Launcher/Services/PluginInstallerService.cs b/LanMountainDesktop.Launcher/Services/PluginInstallerService.cs
index 2e4eea2..30f4650 100644
--- a/LanMountainDesktop.Launcher/Services/PluginInstallerService.cs
+++ b/LanMountainDesktop.Launcher/Services/PluginInstallerService.cs
@@ -1,11 +1,18 @@
using System.IO.Compression;
-using LanMountainDesktop.PluginSdk;
+using System.Text.Json;
using LanMountainDesktop.Launcher.Models;
namespace LanMountainDesktop.Launcher.Services;
+///
+/// 插件安装服务 - 简化版,不依赖 PluginSdk
+///
internal sealed class PluginInstallerService
{
+ private const string ManifestFileName = "manifest.json";
+ private const string PackageFileExtension = ".lmdp";
+ private const string RuntimeDirectoryName = "runtime";
+
private static readonly TimeSpan[] RetryDelays =
[
TimeSpan.FromMilliseconds(120),
@@ -48,33 +55,40 @@ internal sealed class PluginInstallerService
{
using var archive = ZipFile.OpenRead(packagePath);
var entries = archive.Entries
- .Where(entry => string.Equals(entry.Name, PluginSdkInfo.ManifestFileName, StringComparison.OrdinalIgnoreCase))
+ .Where(entry => string.Equals(entry.Name, ManifestFileName, StringComparison.OrdinalIgnoreCase))
.ToArray();
if (entries.Length == 0)
{
throw new InvalidOperationException(
- $"Plugin package '{packagePath}' does not contain '{PluginSdkInfo.ManifestFileName}'.");
+ $"Plugin package '{packagePath}' does not contain '{ManifestFileName}'.");
}
if (entries.Length > 1)
{
throw new InvalidOperationException(
- $"Plugin package '{packagePath}' contains multiple '{PluginSdkInfo.ManifestFileName}' files.");
+ $"Plugin package '{packagePath}' contains multiple '{ManifestFileName}' files.");
}
using var stream = entries[0].Open();
- return PluginManifest.Load(stream, $"{packagePath}!/{entries[0].FullName}");
+ using var reader = new StreamReader(stream);
+ var json = reader.ReadToEnd();
+ var manifest = JsonSerializer.Deserialize(json);
+ if (manifest == null)
+ {
+ throw new InvalidOperationException($"Failed to deserialize manifest from '{packagePath}'.");
+ }
+ return manifest;
}
private void RemoveExistingPluginPackages(string pluginsDirectory, string pluginId, string destinationPath, string stagingPath)
{
- var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(pluginsDirectory), PluginSdkInfo.RuntimeDirectoryName));
+ var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(pluginsDirectory), RuntimeDirectoryName));
var pendingDeletionDir = Path.Combine(pluginsDirectory, ".pending-deletions");
Directory.CreateDirectory(pendingDeletionDir);
foreach (var existingPackagePath in Directory
- .EnumerateFiles(pluginsDirectory, "*" + PluginSdkInfo.PackageFileExtension, SearchOption.AllDirectories)
+ .EnumerateFiles(pluginsDirectory, "*" + PackageFileExtension, SearchOption.AllDirectories)
.Select(Path.GetFullPath)
.Where(path => !path.StartsWith(runtimeRootDirectory, StringComparison.OrdinalIgnoreCase)))
{
@@ -188,7 +202,7 @@ internal sealed class PluginInstallerService
{
var invalidChars = Path.GetInvalidFileNameChars();
var fileName = new string(pluginId.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
- return fileName + PluginSdkInfo.PackageFileExtension;
+ return fileName + PackageFileExtension;
}
private static string EnsureTrailingSeparator(string path)
@@ -198,3 +212,15 @@ internal sealed class PluginInstallerService
: path + Path.DirectorySeparatorChar;
}
}
+
+///
+/// 简化的插件清单模型
+///
+public class PluginManifest
+{
+ public string Id { get; set; } = "";
+ public string Name { get; set; } = "";
+ public string Version { get; set; } = "";
+ public string? Description { get; set; }
+ public string? Author { get; set; }
+}
diff --git a/LanMountainDesktop.Launcher/Services/UpdateEngineService.cs b/LanMountainDesktop.Launcher/Services/UpdateEngineService.cs
index 802927b..667fb98 100644
--- a/LanMountainDesktop.Launcher/Services/UpdateEngineService.cs
+++ b/LanMountainDesktop.Launcher/Services/UpdateEngineService.cs
@@ -146,7 +146,9 @@ internal sealed class UpdateEngineService
var currentDeployment = _deploymentLocator.FindCurrentDeploymentDirectory();
if (string.IsNullOrWhiteSpace(currentDeployment))
{
- return Failed("update.apply", "no_current_deployment", "Current deployment directory not found.");
+ // 全新安装场景:没有当前部署目录,但有更新包
+ // 这种情况下应该直接应用更新作为首次安装
+ return await ApplyInitialDeploymentAsync(fileMap, archivePath, fileMapPath, signaturePath);
}
var currentVersion = _deploymentLocator.GetCurrentVersion();
@@ -258,6 +260,167 @@ internal sealed class UpdateEngineService
}
}
+ ///
+ /// 全新安装场景:直接应用更新包作为首次部署
+ ///
+ private async Task ApplyInitialDeploymentAsync(
+ SignedFileMap fileMap,
+ string archivePath,
+ string fileMapPath,
+ string signaturePath)
+ {
+ var targetVersion = string.IsNullOrWhiteSpace(fileMap.ToVersion) ? "1.0.0" : fileMap.ToVersion!;
+ var targetDeployment = _deploymentLocator.BuildNextDeploymentDirectory(targetVersion);
+ var partialMarker = Path.Combine(targetDeployment, ".partial");
+ var snapshotPath = Path.Combine(_snapshotsRoot, $"initial-{Guid.NewGuid():N}.json");
+
+ var extractRoot = Path.Combine(_incomingRoot, "extracted");
+ try
+ {
+ // 保存快照(用于回滚,虽然首次安装回滚意义不大)
+ var snapshot = new SnapshotMetadata
+ {
+ SnapshotId = Guid.NewGuid().ToString("N"),
+ SourceVersion = "0.0.0",
+ TargetVersion = targetVersion,
+ CreatedAt = DateTimeOffset.UtcNow,
+ SourceDirectory = "",
+ TargetDirectory = targetDeployment,
+ Status = "pending"
+ };
+ SaveSnapshot(snapshotPath, snapshot);
+
+ // 清理并解压更新包
+ if (Directory.Exists(extractRoot))
+ {
+ Directory.Delete(extractRoot, true);
+ }
+ Directory.CreateDirectory(extractRoot);
+ ZipFile.ExtractToDirectory(archivePath, extractRoot, overwriteFiles: true);
+
+ // 创建目标部署目录
+ Directory.CreateDirectory(targetDeployment);
+ File.WriteAllText(partialMarker, string.Empty);
+
+ // 应用所有文件(全新安装时,所有文件都是新增或替换)
+ foreach (var file in fileMap.Files)
+ {
+ ApplyInitialFileEntry(file, targetDeployment, extractRoot);
+ }
+
+ // 验证文件哈希
+ foreach (var file in fileMap.Files)
+ {
+ if (!NeedsVerification(file))
+ {
+ continue;
+ }
+
+ var fullPath = Path.Combine(targetDeployment, file.Path);
+ var actualHash = ComputeSha256Hex(fullPath);
+ if (!string.Equals(actualHash, file.Sha256, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException($"File hash mismatch for '{file.Path}'.");
+ }
+ }
+
+ // 激活部署(创建 .current 标记,删除 .partial 标记)
+ var currentMarker = Path.Combine(targetDeployment, ".current");
+ File.WriteAllText(currentMarker, string.Empty);
+ if (File.Exists(partialMarker))
+ {
+ File.Delete(partialMarker);
+ }
+
+ // 清理更新包
+ snapshot.Status = "applied";
+ SaveSnapshot(snapshotPath, snapshot);
+ CleanupIncomingArtifacts();
+
+ return new LauncherResult
+ {
+ Success = true,
+ Stage = "update.apply",
+ Code = "ok",
+ Message = $"Initial deployment to {targetVersion}.",
+ CurrentVersion = "0.0.0",
+ TargetVersion = targetVersion
+ };
+ }
+ catch (Exception ex)
+ {
+ // 清理失败的目标目录
+ try
+ {
+ if (Directory.Exists(targetDeployment))
+ {
+ Directory.Delete(targetDeployment, true);
+ }
+ }
+ catch
+ {
+ }
+
+ return new LauncherResult
+ {
+ Success = false,
+ Stage = "update.apply",
+ Code = "initial_deploy_failed",
+ Message = "Failed to apply initial deployment.",
+ ErrorMessage = ex.Message,
+ CurrentVersion = "0.0.0",
+ TargetVersion = targetVersion
+ };
+ }
+ finally
+ {
+ try
+ {
+ if (Directory.Exists(extractRoot))
+ {
+ Directory.Delete(extractRoot, true);
+ }
+ }
+ catch
+ {
+ }
+ }
+ }
+
+ ///
+ /// 应用初始部署文件(全新安装场景,不需要源目录)
+ ///
+ private void ApplyInitialFileEntry(UpdateFileEntry file, string targetDeployment, string extractRoot)
+ {
+ var normalizedPath = NormalizeRelativePath(file.Path);
+
+ // 删除操作在全新安装时忽略
+ if (string.Equals(file.Action, "delete", StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ var targetPath = Path.Combine(targetDeployment, normalizedPath);
+ EnsurePathWithinRoot(targetPath, targetDeployment);
+ var targetDir = Path.GetDirectoryName(targetPath);
+ if (!string.IsNullOrWhiteSpace(targetDir))
+ {
+ Directory.CreateDirectory(targetDir);
+ }
+
+ // 无论是 add 还是 replace,都从压缩包复制
+ var archiveRelative = string.IsNullOrWhiteSpace(file.ArchivePath) ? normalizedPath : NormalizeRelativePath(file.ArchivePath);
+ var extractedPath = Path.Combine(extractRoot, archiveRelative);
+ EnsurePathWithinRoot(extractedPath, extractRoot);
+
+ if (!File.Exists(extractedPath))
+ {
+ throw new FileNotFoundException($"Archive file '{archiveRelative}' not found for '{file.Path}'.");
+ }
+
+ File.Copy(extractedPath, targetPath, overwrite: true);
+ }
+
public LauncherResult RollbackLatest()
{
if (!Directory.Exists(_snapshotsRoot))
diff --git a/LanMountainDesktop.Launcher/Views/ErrorDebugWindow.axaml b/LanMountainDesktop.Launcher/Views/ErrorDebugWindow.axaml
new file mode 100644
index 0000000..b09d71e
--- /dev/null
+++ b/LanMountainDesktop.Launcher/Views/ErrorDebugWindow.axaml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop.Launcher/Views/ErrorDebugWindow.axaml.cs b/LanMountainDesktop.Launcher/Views/ErrorDebugWindow.axaml.cs
new file mode 100644
index 0000000..6f4c47c
--- /dev/null
+++ b/LanMountainDesktop.Launcher/Views/ErrorDebugWindow.axaml.cs
@@ -0,0 +1,128 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Avalonia.Platform.Storage;
+
+namespace LanMountainDesktop.Launcher.Views;
+
+///
+/// 错误调试窗口 - 开发人员专用调试设置
+///
+public partial class ErrorDebugWindow : Window
+{
+ private string? _selectedHostPath;
+
+ ///
+ /// 是否启用了开发模式
+ ///
+ public bool IsDevModeEnabled { get; private set; }
+
+ ///
+ /// 选择的主程序路径
+ ///
+ public string? SelectedHostPath => _selectedHostPath;
+
+ public ErrorDebugWindow()
+ {
+ AvaloniaXamlLoader.Load(this);
+ InitializeComponents();
+ }
+
+ public ErrorDebugWindow(bool devModeEnabled, string? initialPath) : this()
+ {
+ IsDevModeEnabled = devModeEnabled;
+ _selectedHostPath = initialPath;
+
+ // 设置初始值
+ var devModeToggle = this.FindControl("DevModeToggle");
+ if (devModeToggle is not null)
+ {
+ devModeToggle.IsChecked = devModeEnabled;
+ }
+
+ UpdatePathDisplay(initialPath);
+ }
+
+ private void InitializeComponents()
+ {
+ // 开发模式开关
+ var devModeToggle = this.FindControl("DevModeToggle");
+ if (devModeToggle is not null)
+ {
+ devModeToggle.IsCheckedChanged += (s, e) =>
+ {
+ IsDevModeEnabled = devModeToggle.IsChecked ?? false;
+ };
+ }
+
+ // 浏览按钮
+ var browseButton = this.FindControl
@@ -80,33 +80,5 @@
-
-
- <_LauncherOutputPath>..\LanMountainDesktop.Launcher\bin\$(Configuration)\net10.0\
-
-
-
-
-
-
-
-
-
- <_LauncherPublishPath>..\LanMountainDesktop.Launcher\bin\$(Configuration)\net10.0\$(RuntimeIdentifier)\publish\
-
-
-
-
-
-
- <_LauncherPublishSource Condition="'$(RuntimeIdentifier)' != '' and Exists('..\LanMountainDesktop.Launcher\bin\$(Configuration)\net10.0\$(RuntimeIdentifier)\publish\')">..\LanMountainDesktop.Launcher\bin\$(Configuration)\net10.0\$(RuntimeIdentifier)\publish\
- <_LauncherPublishSource Condition="'$(_LauncherPublishSource)' == ''">..\LanMountainDesktop.Launcher\bin\$(Configuration)\net10.0\
-
-
-
-
-
-
+
diff --git a/LanMountainDesktop/Services/Launcher/LauncherIpcClient.cs b/LanMountainDesktop/Services/Launcher/LauncherIpcClient.cs
new file mode 100644
index 0000000..d00dddf
--- /dev/null
+++ b/LanMountainDesktop/Services/Launcher/LauncherIpcClient.cs
@@ -0,0 +1,82 @@
+using System.IO.Pipes;
+using System.Text.Json;
+using LanMountainDesktop.Shared.Contracts.Launcher;
+
+namespace LanMountainDesktop.Services.Launcher;
+
+///
+/// Launcher IPC 客户端 - 向 Launcher 报告启动进度
+///
+public class LauncherIpcClient : IDisposable
+{
+ private NamedPipeClientStream? _pipeClient;
+ private bool _isConnected;
+
+ ///
+ /// 连接到 Launcher 的 IPC 服务端
+ ///
+ public async Task ConnectAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ _pipeClient = new NamedPipeClientStream(
+ ".",
+ LauncherIpcConstants.PipeName,
+ PipeDirection.Out);
+
+ await _pipeClient.ConnectAsync(5000, cancellationToken);
+ _isConnected = true;
+ return true;
+ }
+ catch (TimeoutException)
+ {
+ // Launcher 可能没有启动 IPC 服务端,这是正常的
+ return false;
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Warn("LauncherIpc", $"Failed to connect to Launcher IPC: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// 报告启动进度
+ ///
+ public async Task ReportProgressAsync(StartupProgressMessage message)
+ {
+ if (!_isConnected || _pipeClient?.IsConnected != true)
+ return;
+
+ try
+ {
+ var json = JsonSerializer.Serialize(message);
+ using var writer = new StreamWriter(_pipeClient, leaveOpen: true);
+ await writer.WriteAsync(json);
+ await writer.FlushAsync();
+ }
+ catch (IOException)
+ {
+ // 管道断开
+ _isConnected = false;
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Warn("LauncherIpc", $"Failed to report progress: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 检查是否从 Launcher 启动
+ ///
+ public static bool IsLaunchedByLauncher()
+ {
+ return !string.IsNullOrEmpty(
+ Environment.GetEnvironmentVariable(LauncherIpcConstants.LauncherPidEnvVar));
+ }
+
+ public void Dispose()
+ {
+ _pipeClient?.Dispose();
+ }
+}
diff --git a/scripts/Setup-DevEnvironment.ps1 b/scripts/Setup-DevEnvironment.ps1
new file mode 100644
index 0000000..5a494e4
--- /dev/null
+++ b/scripts/Setup-DevEnvironment.ps1
@@ -0,0 +1,72 @@
+# 开发环境设置脚本
+# 创建模拟的生产目录结构,方便测试 Launcher
+
+param(
+ [string]$Configuration = "Debug",
+ [string]$Version = "1.0.0-dev"
+)
+
+$ErrorActionPreference = "Stop"
+
+# 获取项目根目录
+$ProjectRoot = Split-Path -Parent $PSScriptRoot
+$LauncherOutput = Join-Path $ProjectRoot "LanMountainDesktop.Launcher\bin\$Configuration\net10.0"
+$MainAppOutput = Join-Path $ProjectRoot "LanMountainDesktop\bin\$Configuration\net10.0"
+$DevRoot = Join-Path $ProjectRoot "dev-test"
+
+Write-Host "Setting up development environment..." -ForegroundColor Cyan
+Write-Host "Project Root: $ProjectRoot"
+Write-Host "Launcher Output: $LauncherOutput"
+Write-Host "Main App Output: $MainAppOutput"
+Write-Host "Dev Root: $DevRoot"
+Write-Host ""
+
+# 检查主程序是否已构建
+if (-not (Test-Path (Join-Path $MainAppOutput "LanMountainDesktop.exe"))) {
+ Write-Host "Main application not found. Building..." -ForegroundColor Yellow
+ dotnet build "$ProjectRoot\LanMountainDesktop.slnx" -c $Configuration
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error "Build failed!"
+ exit 1
+ }
+}
+
+# 清理旧的开发环境
+if (Test-Path $DevRoot) {
+ Write-Host "Cleaning old dev environment..." -ForegroundColor Yellow
+ Remove-Item -Path $DevRoot -Recurse -Force
+}
+
+# 创建目录结构
+$AppDir = Join-Path $DevRoot "app-$Version"
+New-Item -ItemType Directory -Path $AppDir -Force | Out-Null
+
+# 复制主程序到 app-{version} 目录
+Write-Host "Copying main application to app-$Version..." -ForegroundColor Green
+Copy-Item -Path "$MainAppOutput\*" -Destination $AppDir -Recurse -Force
+
+# 创建 .current 标记文件
+New-Item -ItemType File -Path (Join-Path $AppDir ".current") -Force | Out-Null
+
+# 复制 Launcher
+Write-Host "Copying Launcher..." -ForegroundColor Green
+Copy-Item -Path "$LauncherOutput\LanMountainDesktop.Launcher.exe" -Destination (Join-Path $DevRoot "LanMountainDesktop.exe") -Force
+
+# 复制 Launcher 依赖
+$LauncherDeps = Get-ChildItem -Path $LauncherOutput -Filter "*.dll" -File
+foreach ($dep in $LauncherDeps) {
+ Copy-Item -Path $dep.FullName -Destination $DevRoot -Force
+}
+
+# 复制 Avalonia 主题文件
+$ThemeFiles = Get-ChildItem -Path $LauncherOutput -Filter "*.xaml" -File
+foreach ($theme in $ThemeFiles) {
+ Copy-Item -Path $theme.FullName -Destination $DevRoot -Force
+}
+
+Write-Host ""
+Write-Host "Development environment setup complete!" -ForegroundColor Green
+Write-Host "Run the Launcher from: $DevRoot\LanMountainDesktop.exe" -ForegroundColor Cyan
+Write-Host ""
+Write-Host "Directory structure:" -ForegroundColor Gray
+Get-ChildItem -Path $DevRoot | Format-Table Name, @{Label="Type"; Expression={if($_.PSIsContainer){"Directory"}else{"File"}}}