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 包引用 |
Files:
Modify: src/YZWater.Core/YZWater.Core.csproj:10-23
[ ] Step 1: 添加 Microsoft.Win32.Registry 包
在 <ItemGroup> 的 PackageReference 列表中添加:
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
添加到 Serilog.Sinks.File 之后(第 20 行后)。
Run: dotnet restore src/YZWater.Core
Expected: 成功还原,无错误
[ ] Step 3: 提交
git add src/YZWater.Core/YZWater.Core.csproj
git commit -m "chore: 添加 Microsoft.Win32.Registry 包引用"
Files:
Modify: src/YZWater.Core/Models/SystemConfig.cs:130-141
[ ] Step 1: 在 _plcPollingIntervalMs 字段后添加新属性
在第 141 行(private int _plcPollingIntervalMs = 1000;)之后,添加:
/// <summary>
/// 是否开机自启动
/// </summary>
[ObservableProperty]
private bool _runAtStartup;
Run: dotnet build src/YZWater.Core
Expected: 编译成功
[ ] Step 3: 提交
git add src/YZWater.Core/Models/SystemConfig.cs
git commit -m "feat: SystemConfig 新增 RunAtStartup 属性"
Files:
Create: src/YZWater.Core/Services/StartupService.cs
[ ] Step 1: 创建 StartupService.cs
创建文件 src/YZWater.Core/Services/StartupService.cs,内容如下:
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
}
Run: dotnet build src/YZWater.Core
Expected: 编译成功
[ ] Step 3: 提交
git add src/YZWater.Core/Services/StartupService.cs
git commit -m "feat: 添加 StartupService 跨平台开机自启动服务"
Files:
Modify: src/YZWater.Core/ViewModels/ViewBViewModel.cs
[ ] Step 1: 添加 RunAtStartup 属性
在 _isConnecting 字段(第 40 行)之后添加:
[ObservableProperty]
private bool _runAtStartup;
在 _connectingText 字段(第 62 行)之后添加:
[ObservableProperty] private string _runAtStartupText;
[ObservableProperty] private string _systemSettingsText;
在 UpdateTexts() 方法中 ConnectingText = _lang.Get("Connecting");(第 92 行)之后添加:
RunAtStartupText = _lang.Get("RunAtStartup");
SystemSettingsText = _lang.Get("SystemSettings");
在 UpdateTexts() 方法之后(第 93 行后),添加:
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 ? "启用" : "禁用");
}
}
}
在 LoadConfig() 方法中,PumpFrequency = config.PumpFrequency;(第 110 行)之后添加:
RunAtStartup = config.RunAtStartup;
将 SaveConfig() 方法(第 118-134 行)替换为:
/// <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("配置已保存");
}
Run: dotnet build src/YZWater.Core
Expected: 编译成功
[ ] Step 8: 提交
git add src/YZWater.Core/ViewModels/ViewBViewModel.cs
git commit -m "feat: ViewBViewModel 添加开机自启动属性和逻辑"
Files:
Modify: src/YZWater.Core/Services/LanguageService.cs
[ ] Step 1: 在中文词典中添加键
在 ["Connecting"] = "连接中...",(第 145 行)之后添加:
["RunAtStartup"] = "开机自启动",
["SystemSettings"] = "系统设置",
在 ["Connecting"] = "Connecting...";(约第 392 行)之后添加:
["RunAtStartup"] = "START WITH WINDOWS",
["SystemSettings"] = "SYSTEM SETTINGS",
Run: dotnet build src/YZWater.Core
Expected: 编译成功
[ ] Step 4: 提交
git add src/YZWater.Core/Services/LanguageService.cs
git commit -m "feat: 添加开机自启动中英文翻译"
Files:
Modify: src/YZWater.Avalonia/Views/ViewBView.axaml
[ ] Step 1: 调整 Grid RowDefinitions
将第 24 行的 RowDefinitions="Auto,Auto" 改为 RowDefinitions="Auto,Auto,Auto",为新区块增加一行。
将"泵频率设置"的 Border(第 80-95 行)中的 Grid.Row="1" 改为 Grid.Row="1"(保持不变),但将 Margin 从 "0,4,4,0" 改为 "0,4,4,4"。
将"操作按钮"的 Border(第 97-108 行)中的 Grid.Row="1" 改为 Grid.Row="2",Margin 从 "4,4,0,0" 改为 "4,4,0,4"。
在"泵频率设置" Border(第 95 行 </Border>)之后,"操作按钮" Border 之前,插入:
<!-- 系统设置 -->
<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>
Run: dotnet build src/YZWater.Avalonia
Expected: 编译成功
[ ] Step 6: 提交
git add src/YZWater.Avalonia/Views/ViewBView.axaml
git commit -m "feat: ViewBView 添加开机自启动 ToggleSwitch"
Run: dotnet build
Expected: 编译成功,无错误
Run: dotnet run --project src/YZWater.Avalonia
手动验证:
HKCU\Software\Microsoft\Windows\CurrentVersion\Run 下是否有 YZWater 键[ ] Step 3: 提交(如果有修改)
git add -A
git commit -m "fix: 开机自启动集成测试修复"
SystemConfig 包含 RunAtStartup 属性StartupService 支持 Windows/Linux/macOS 三平台SaveConfig() bug 已修复