Browse Source

docs: 添加开机自启动实施计划

DESKTOP-74CLTRG\Leol 1 tháng trước cách đây
mục cha
commit
f5fe976c24
1 tập tin đã thay đổi với 558 bổ sung và 0 xóa
  1. 558 0
      docs/superpowers/plans/2026-09-01-autostart.md

+ 558 - 0
docs/superpowers/plans/2026-09-01-autostart.md

@@ -0,0 +1,558 @@
+# 开机自启动功能实施计划
+
+> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 为 YZWater3 添加跨平台开机自启动功能,用户可在参数设置页面切换开关。
+
+**Architecture:** 创建 `StartupService` 静态类,通过 `RuntimeInformation.IsOSPlatform()` 判断平台,分别操作 Windows 注册表、Linux `.desktop` 文件、macOS LaunchAgent。在 `SystemConfig` 中新增 `RunAtStartup` 属性持久化到 JSON,在 `ViewBViewModel` 中添加属性和切换逻辑,在 `ViewBView.axaml` 中添加 ToggleSwitch。
+
+**Tech Stack:** .NET 8, Avalonia 12, CommunityToolkit.Mvvm, Microsoft.Win32.Registry
+
+**Spec:** `docs/superpowers/specs/2026-09-01-autostart-design.md`
+
+---
+
+## 文件结构
+
+| 操作 | 文件路径 | 职责 |
+|------|----------|------|
+| 新建 | `src/YZWater.Core/Services/StartupService.cs` | 跨平台开机自启动管理 |
+| 修改 | `src/YZWater.Core/Models/SystemConfig.cs` | 新增 `RunAtStartup` 属性 |
+| 修改 | `src/YZWater.Core/ViewModels/ViewBViewModel.cs` | 新增 `RunAtStartup` 属性 + 保存逻辑 + 修复 SaveConfig bug |
+| 修改 | `src/YZWater.Avalonia/Views/ViewBView.axaml` | 新增系统设置区块 + ToggleSwitch |
+| 修改 | `src/YZWater.Core/Services/LanguageService.cs` | 新增中英文 i18n 键 |
+| 修改 | `src/YZWater.Core/YZWater.Core.csproj` | 添加 `Microsoft.Win32.Registry` 包引用 |
+
+---
+
+## Task 1: 添加 NuGet 包引用
+
+**Files:**
+- Modify: `src/YZWater.Core/YZWater.Core.csproj:10-23`
+
+- [ ] **Step 1: 添加 Microsoft.Win32.Registry 包**
+
+在 `<ItemGroup>` 的 PackageReference 列表中添加:
+
+```xml
+<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
+```
+
+添加到 `Serilog.Sinks.File` 之后(第 20 行后)。
+
+- [ ] **Step 2: 还原依赖**
+
+Run: `dotnet restore src/YZWater.Core`
+Expected: 成功还原,无错误
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add src/YZWater.Core/YZWater.Core.csproj
+git commit -m "chore: 添加 Microsoft.Win32.Registry 包引用"
+```
+
+---
+
+## Task 2: SystemConfig 模型新增属性
+
+**Files:**
+- Modify: `src/YZWater.Core/Models/SystemConfig.cs:130-141`
+
+- [ ] **Step 1: 在 `_plcPollingIntervalMs` 字段后添加新属性**
+
+在第 141 行(`private int _plcPollingIntervalMs = 1000;`)之后,添加:
+
+```csharp
+/// <summary>
+/// 是否开机自启动
+/// </summary>
+[ObservableProperty]
+private bool _runAtStartup;
+```
+
+- [ ] **Step 2: 构建验证**
+
+Run: `dotnet build src/YZWater.Core`
+Expected: 编译成功
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add src/YZWater.Core/Models/SystemConfig.cs
+git commit -m "feat: SystemConfig 新增 RunAtStartup 属性"
+```
+
+---
+
+## Task 3: 创建 StartupService
+
+**Files:**
+- Create: `src/YZWater.Core/Services/StartupService.cs`
+
+- [ ] **Step 1: 创建 StartupService.cs**
+
+创建文件 `src/YZWater.Core/Services/StartupService.cs`,内容如下:
+
+```csharp
+using System.Runtime.InteropServices;
+using Microsoft.Win32;
+using Serilog;
+
+namespace YZWater.Core.Services;
+
+/// <summary>
+/// 跨平台开机自启动管理服务
+/// </summary>
+public static class StartupService
+{
+    private const string AppName = "YZWater";
+    private const string WindowsRegistryPath = @"Software\Microsoft\Windows\CurrentVersion\Run";
+    private const string LinuxDesktopFileName = "yzwater.desktop";
+    private const string MacPlistFileName = "com.yxwater.yzwater.plist";
+
+    /// <summary>
+    /// 设置开机自启动
+    /// </summary>
+    public static bool SetAutoStart(bool enabled)
+    {
+        try
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                return SetWindowsAutoStart(enabled);
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+                return SetLinuxAutoStart(enabled);
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+                return SetMacAutoStart(enabled);
+
+            Log.Warning("StartupService: 不支持的操作系统");
+            return false;
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "StartupService: 设置开机自启动失败 (enabled={Enabled})", enabled);
+            return false;
+        }
+    }
+
+    /// <summary>
+    /// 检查是否已启用开机自启动
+    /// </summary>
+    public static bool IsAutoStartEnabled()
+    {
+        try
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                return IsWindowsAutoStartEnabled();
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+                return IsLinuxAutoStartEnabled();
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+                return IsMacAutoStartEnabled();
+
+            return false;
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "StartupService: 检查开机自启动状态失败");
+            return false;
+        }
+    }
+
+    /// <summary>
+    /// 获取当前可执行文件路径
+    /// </summary>
+    private static string GetExecutablePath()
+    {
+        return Environment.ProcessPath ?? Environment.GetCommandLineArgs()[0];
+    }
+
+    #region Windows
+
+    private static bool SetWindowsAutoStart(bool enabled)
+    {
+        using var key = Registry.CurrentUser.OpenSubKey(WindowsRegistryPath, true);
+        if (key == null)
+        {
+            Log.Error("StartupService: 无法打开注册表路径 {Path}", WindowsRegistryPath);
+            return false;
+        }
+
+        if (enabled)
+        {
+            key.SetValue(AppName, GetExecutablePath());
+            Log.Information("StartupService: Windows 开机自启动已启用");
+        }
+        else
+        {
+            key.DeleteValue(AppName, false);
+            Log.Information("StartupService: Windows 开机自启动已禁用");
+        }
+        return true;
+    }
+
+    private static bool IsWindowsAutoStartEnabled()
+    {
+        using var key = Registry.CurrentUser.OpenSubKey(WindowsRegistryPath);
+        return key?.GetValue(AppName) != null;
+    }
+
+    #endregion
+
+    #region Linux
+
+    private static string GetLinuxDesktopFilePath()
+    {
+        var configDir = Path.Combine(
+            Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+            ".config", "autostart");
+        return Path.Combine(configDir, LinuxDesktopFileName);
+    }
+
+    private static bool SetLinuxAutoStart(bool enabled)
+    {
+        var filePath = GetLinuxDesktopFilePath();
+
+        if (enabled)
+        {
+            var dir = Path.GetDirectoryName(filePath)!;
+            Directory.CreateDirectory(dir);
+
+            var content = $"""
+                [Desktop Entry]
+                Type=Application
+                Name={AppName}
+                Exec={GetExecutablePath()}
+                X-GNOME-Autostart-enabled=true
+                """;
+            File.WriteAllText(filePath, content);
+            Log.Information("StartupService: Linux 开机自启动已启用 ({Path})", filePath);
+        }
+        else
+        {
+            if (File.Exists(filePath))
+                File.Delete(filePath);
+            Log.Information("StartupService: Linux 开机自启动已禁用");
+        }
+        return true;
+    }
+
+    private static bool IsLinuxAutoStartEnabled()
+    {
+        return File.Exists(GetLinuxDesktopFilePath());
+    }
+
+    #endregion
+
+    #region macOS
+
+    private static string GetMacPlistPath()
+    {
+        var libraryDir = Path.Combine(
+            Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+            "Library", "LaunchAgents");
+        return Path.Combine(libraryDir, MacPlistFileName);
+    }
+
+    private static bool SetMacAutoStart(bool enabled)
+    {
+        var filePath = GetMacPlistPath();
+
+        if (enabled)
+        {
+            var dir = Path.GetDirectoryName(filePath)!;
+            Directory.CreateDirectory(dir);
+
+            var content = $"""
+                <?xml version="1.0" encoding="UTF-8"?>
+                <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+                <plist version="1.0">
+                <dict>
+                    <key>Label</key>
+                    <string>com.yxwater.yzwater</string>
+                    <key>ProgramArguments</key>
+                    <array>
+                        <string>{GetExecutablePath()}</string>
+                    </array>
+                    <key>RunAtLoad</key>
+                    <true/>
+                </dict>
+                </plist>
+                """;
+            File.WriteAllText(filePath, content);
+            Log.Information("StartupService: macOS 开机自启动已启用 ({Path})", filePath);
+        }
+        else
+        {
+            if (File.Exists(filePath))
+                File.Delete(filePath);
+            Log.Information("StartupService: macOS 开机自启动已禁用");
+        }
+        return true;
+    }
+
+    private static bool IsMacAutoStartEnabled()
+    {
+        return File.Exists(GetMacPlistPath());
+    }
+
+    #endregion
+}
+```
+
+- [ ] **Step 2: 构建验证**
+
+Run: `dotnet build src/YZWater.Core`
+Expected: 编译成功
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add src/YZWater.Core/Services/StartupService.cs
+git commit -m "feat: 添加 StartupService 跨平台开机自启动服务"
+```
+
+---
+
+## Task 4: ViewBViewModel 添加属性和逻辑
+
+**Files:**
+- Modify: `src/YZWater.Core/ViewModels/ViewBViewModel.cs`
+
+- [ ] **Step 1: 添加 RunAtStartup 属性**
+
+在 `_isConnecting` 字段(第 40 行)之后添加:
+
+```csharp
+[ObservableProperty]
+private bool _runAtStartup;
+```
+
+- [ ] **Step 2: 添加翻译文本属性**
+
+在 `_connectingText` 字段(第 62 行)之后添加:
+
+```csharp
+[ObservableProperty] private string _runAtStartupText;
+[ObservableProperty] private string _systemSettingsText;
+```
+
+- [ ] **Step 3: 在 UpdateTexts() 方法中添加赋值**
+
+在 `UpdateTexts()` 方法中 `ConnectingText = _lang.Get("Connecting");`(第 92 行)之后添加:
+
+```csharp
+RunAtStartupText = _lang.Get("RunAtStartup");
+SystemSettingsText = _lang.Get("SystemSettings");
+```
+
+- [ ] **Step 4: 添加 OnRunAtStartupChanged 部分方法**
+
+在 `UpdateTexts()` 方法之后(第 93 行后),添加:
+
+```csharp
+partial void OnRunAtStartupChanged(bool value)
+{
+    var success = StartupService.SetAutoStart(value);
+    if (!success)
+    {
+        _runAtStartup = !value;
+        OnPropertyChanged(nameof(RunAtStartup));
+        Log.Warning("开机自启动设置失败,已回滚 UI 状态");
+    }
+    else
+    {
+        var config = ConfigService.GetConfig();
+        if (config != null)
+        {
+            config.RunAtStartup = value;
+            ConfigService.SaveConfig();
+            Log.Information("开机自启动已{Status}", value ? "启用" : "禁用");
+        }
+    }
+}
+```
+
+- [ ] **Step 5: 修改 LoadConfig() 加载 RunAtStartup**
+
+在 `LoadConfig()` 方法中,`PumpFrequency = config.PumpFrequency;`(第 110 行)之后添加:
+
+```csharp
+RunAtStartup = config.RunAtStartup;
+```
+
+- [ ] **Step 6: 修复 SaveConfig() bug**
+
+将 `SaveConfig()` 方法(第 118-134 行)替换为:
+
+```csharp
+/// <summary>
+/// 保存配置
+/// </summary>
+[RelayCommand]
+private void SaveConfig()
+{
+    var config = ConfigService.GetConfig();
+    if (config == null)
+    {
+        Log.Warning("配置为空,无法保存");
+        return;
+    }
+
+    config.PlcIp = PlcIp;
+    config.PlcPort = PlcPort;
+    config.AutoConnect = AutoConnect;
+    config.LevelHighAlarm = LevelHighAlarm;
+    config.LevelLowAlarm = LevelLowAlarm;
+    config.FlowHighAlarm = FlowHighAlarm;
+    config.PumpFrequency = PumpFrequency;
+    // RunAtStartup 在 OnRunAtStartupChanged 中已单独保存
+
+    ConfigService.SaveConfig();
+    Log.Information("配置已保存");
+}
+```
+
+- [ ] **Step 7: 构建验证**
+
+Run: `dotnet build src/YZWater.Core`
+Expected: 编译成功
+
+- [ ] **Step 8: 提交**
+
+```bash
+git add src/YZWater.Core/ViewModels/ViewBViewModel.cs
+git commit -m "feat: ViewBViewModel 添加开机自启动属性和逻辑"
+```
+
+---
+
+## Task 5: LanguageService 添加 i18n 键
+
+**Files:**
+- Modify: `src/YZWater.Core/Services/LanguageService.cs`
+
+- [ ] **Step 1: 在中文词典中添加键**
+
+在 `["Connecting"] = "连接中...",`(第 145 行)之后添加:
+
+```csharp
+["RunAtStartup"] = "开机自启动",
+["SystemSettings"] = "系统设置",
+```
+
+- [ ] **Step 2: 在英文词典中添加键**
+
+在 `["Connecting"] = "Connecting...";`(约第 392 行)之后添加:
+
+```csharp
+["RunAtStartup"] = "START WITH WINDOWS",
+["SystemSettings"] = "SYSTEM SETTINGS",
+```
+
+- [ ] **Step 3: 构建验证**
+
+Run: `dotnet build src/YZWater.Core`
+Expected: 编译成功
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add src/YZWater.Core/Services/LanguageService.cs
+git commit -m "feat: 添加开机自启动中英文翻译"
+```
+
+---
+
+## Task 6: ViewBView 添加 UI
+
+**Files:**
+- Modify: `src/YZWater.Avalonia/Views/ViewBView.axaml`
+
+- [ ] **Step 1: 调整 Grid RowDefinitions**
+
+将第 24 行的 `RowDefinitions="Auto,Auto"` 改为 `RowDefinitions="Auto,Auto,Auto"`,为新区块增加一行。
+
+- [ ] **Step 2: 将"泵频率设置"移到 Row 1**
+
+将"泵频率设置"的 `Border`(第 80-95 行)中的 `Grid.Row="1"` 改为 `Grid.Row="1"`(保持不变),但将 `Margin` 从 `"0,4,4,0"` 改为 `"0,4,4,4"`。
+
+- [ ] **Step 3: 将"操作按钮"移到 Row 2**
+
+将"操作按钮"的 `Border`(第 97-108 行)中的 `Grid.Row="1"` 改为 `Grid.Row="2"`,`Margin` 从 `"4,4,0,0"` 改为 `"4,4,0,4"`。
+
+- [ ] **Step 4: 在 Row 1 Column 1 添加系统设置区块**
+
+在"泵频率设置" `Border`(第 95 行 `</Border>`)之后,"操作按钮" `Border` 之前,插入:
+
+```xml
+<!-- 系统设置 -->
+<Border Grid.Row="1" Grid.Column="1" Background="{DynamicResource SurfaceBgBrush}" CornerRadius="2"
+        BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" Padding="12" Margin="4,4,0,4">
+    <StackPanel>
+        <StackPanel Orientation="Horizontal" Spacing="6" Margin="0,0,0,12">
+            <Border Background="{DynamicResource PrimaryBrush}" Width="3" Height="14" CornerRadius="1"/>
+            <TextBlock Text="{Binding SystemSettingsText}" FontFamily="{DynamicResource MonoFont}" FontSize="12" FontWeight="Bold"
+                       Foreground="{DynamicResource TextSecondaryBrush}"/>
+        </StackPanel>
+        <Grid ColumnDefinitions="120,*" Margin="4,0">
+            <TextBlock Grid.Column="0" Text="{Binding RunAtStartupText}" FontFamily="{DynamicResource MonoFont}" FontSize="11" Foreground="{DynamicResource HeaderSubtextBrush}" VerticalAlignment="Center"/>
+            <ToggleSwitch Grid.Column="1" IsChecked="{Binding RunAtStartup}" Margin="0,2"/>
+        </Grid>
+    </StackPanel>
+</Border>
+```
+
+- [ ] **Step 5: 构建验证**
+
+Run: `dotnet build src/YZWater.Avalonia`
+Expected: 编译成功
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add src/YZWater.Avalonia/Views/ViewBView.axaml
+git commit -m "feat: ViewBView 添加开机自启动 ToggleSwitch"
+```
+
+---
+
+## Task 7: 集成测试
+
+- [ ] **Step 1: 全量构建**
+
+Run: `dotnet build`
+Expected: 编译成功,无错误
+
+- [ ] **Step 2: 运行应用验证**
+
+Run: `dotnet run --project src/YZWater.Avalonia`
+
+手动验证:
+1. 登录应用
+2. 导航到参数设置页面(ViewB)
+3. 确认"系统设置"区块可见,包含"开机自启动"标签和 ToggleSwitch
+4. 切换 ToggleSwitch 为开启状态
+5. (Windows)打开注册表编辑器,检查 `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` 下是否有 `YZWater` 键
+6. 关闭应用,重新打开,确认 ToggleSwitch 状态已恢复
+7. 切换 ToggleSwitch 为关闭状态,确认注册表键已删除
+8. 切换语言为英文,确认"开机自启动"变为"START WITH WINDOWS"
+
+- [ ] **Step 3: 提交(如果有修改)**
+
+```bash
+git add -A
+git commit -m "fix: 开机自启动集成测试修复"
+```
+
+---
+
+## 完成标准
+
+- [ ] `SystemConfig` 包含 `RunAtStartup` 属性
+- [ ] `StartupService` 支持 Windows/Linux/macOS 三平台
+- [ ] ViewB 页面显示"系统设置"区块,包含开机自启动开关
+- [ ] 开关切换即时生效(写入注册表/文件 + 持久化到 JSON)
+- [ ] 重启应用后状态正确恢复
+- [ ] 中英文翻译完整
+- [ ] `SaveConfig()` bug 已修复
+- [ ] 全量构建无错误