diff --git a/.claude/skills/generalupdate-advanced/templates/BowlIntegration.cs b/.claude/skills/generalupdate-advanced/templates/BowlIntegration.cs index a93a64a..3d12ddb 100644 --- a/.claude/skills/generalupdate-advanced/templates/BowlIntegration.cs +++ b/.claude/skills/generalupdate-advanced/templates/BowlIntegration.cs @@ -2,65 +2,44 @@ using GeneralUpdate.Core.Models; /// -/// 【Skill 自动生成】Bowl 崩溃守护集成 -/// -/// Bowl 是一个跨平台的崩溃监控助手,在升级完成后监控主应用的启动情况。 -/// 如果主应用崩溃,Bowl 会: -/// 1. 生成 MiniDump 文件 -/// 2. 写入 CrashReport.json(崩溃诊断报告) -/// 3. 可选:从备份自动回滚 -/// 4. 触发 OnCrash 回调 -/// -/// 使用方式: -/// 在 Upgrade 完成后启动 Bowl(由 StartAppAsync 自动处理) -/// 或者在主应用启动后手动调用 Bowl.LaunchAsync() +/// [Skill Generated] Bowl crash daemon integration. +/// Bowl monitors the main app after update. On crash it generates: +/// - MiniDump (.dmp) +/// - Crash report (.json) +/// - System diagnostics (event log / drivers / system info) +/// - Auto-restore from backup (optional) /// /// NuGet: dotnet add package GeneralUpdate.Bowl /// -/// ⚠️ 注意事项: -/// 1. Bowl 目前仅在 Windows 上充分测试 -/// 2. 回滚依赖于更新前的备份(BackupEnabled = true) -/// 3. 备份保留最多 3 个版本 -/// 4. Bowl 需要使用 procdump 工具(Windows) +/// Notes: +/// - Bowl is fully tested on Windows only +/// - Rollback depends on BackupEnabled = true +/// - Keeps only the 3 most recent backups +/// - Requires procdump tool (Windows) /// public static class BowlIntegration { - /// - /// 启动 Bowl 崩溃守护 - /// public static async Task StartBowlAsync(string appPath, string installPath) { - Console.WriteLine("[Bowl] 启动崩溃守护进程..."); + Console.WriteLine("[Bowl] Starting crash daemon..."); var bowl = new Bowl(); - - // 注册崩溃回调 bowl.OnCrash += (crashReport) => { - Console.WriteLine($"[Bowl] 检测到崩溃!"); - Console.WriteLine($"[Bowl] 原因: {crashReport.CrashReason}"); - Console.WriteLine($"[Bowl] Dump 文件: {crashReport.DumpFilePath}"); + Console.WriteLine($"[Bowl] Crash detected!"); + Console.WriteLine($"[Bowl] Reason: {crashReport.CrashReason}"); + Console.WriteLine($"[Bowl] Dump file: {crashReport.DumpFilePath}"); - // 自动回滚(前提是有备份) if (crashReport.AutoRestore) - { - Console.WriteLine("[Bowl] 正在回滚到备份版本..."); - // Bowl 会自动从备份目录恢复 - } + Console.WriteLine("[Bowl] Restoring from backup..."); }; - // 启动监控 await bowl.LaunchAsync(new BowlOptions { - // 要监控的主应用路径 TargetAppPath = appPath, - // 安装目录(用于定位备份) InstallPath = installPath, - // 启用自动回滚 AutoRestore = true, - // 崩溃报告输出目录 - ReportOutputPath = Path.Combine( - installPath, "CrashReports") + ReportOutputPath = Path.Combine(installPath, "CrashReports") }); } } diff --git a/.claude/skills/generalupdate-advanced/templates/CustomHooks.cs b/.claude/skills/generalupdate-advanced/templates/CustomHooks.cs index 248ded2..1f03590 100644 --- a/.claude/skills/generalupdate-advanced/templates/CustomHooks.cs +++ b/.claude/skills/generalupdate-advanced/templates/CustomHooks.cs @@ -2,98 +2,70 @@ using GeneralUpdate.Core.Models; /// -/// 【Skill 自动生成】自定义生命周期 Hooks +/// [Skill Generated] Custom lifecycle hooks. +/// Implements IUpdateHooks for full lifecycle control. +/// All methods have default implementations (return null/true) — override only what you need. /// -/// 实现 IUpdateHooks 接口,在更新的各个生命周期阶段插入自定义逻辑。 -/// 所有方法都有默认实现(返回 null/true),只需重写需要的方法。 -/// -/// 使用方式: +/// Usage: /// .Hooks() /// public class MyCustomHooks : IUpdateHooks { - /// - /// 更新开始前调用。返回 false 中止更新。 - /// 可用于:检查磁盘空间、检查是否在营业时间、用户确认等。 - /// + /// Called before update starts. Return false to abort. public async Task OnBeforeUpdateAsync(UpdateContext context) { - Console.WriteLine($"[Hooks] 开始更新: {context.CurrentVersion} → {context.LastVersion}"); + Console.WriteLine($"[Hooks] Update starting: {context.CurrentVersion} -> {context.LastVersion}"); - // 检查磁盘空间 + // Check disk space var drive = new DriveInfo(Path.GetPathRoot(context.InstallPath)!); - if (drive.AvailableFreeSpace < 100 * 1024 * 1024) // 100MB 最低要求 + if (drive.AvailableFreeSpace < 100 * 1024 * 1024) { - Console.WriteLine("[Hooks] 磁盘空间不足,中止更新"); + Console.WriteLine("[Hooks] Insufficient disk space, aborting"); return false; } - return true; // true = 继续更新 + return true; } - /// - /// 下载完成后调用(在 Client 进程) - /// 可用于:下载后扫描、日志记录、通知 UI - /// + /// Called after download completes (Client process). public async Task OnDownloadCompletedAsync(UpdateContext context) { - Console.WriteLine($"[Hooks] 下载完成: {context.LastVersion}"); - // 可以在这里触发 UI 通知 + Console.WriteLine($"[Hooks] Download complete: {context.LastVersion}"); await Task.CompletedTask; } - /// - /// 更新完成后调用(在 Upgrade 进程,替换文件后) - /// 可用于:清理临时文件、更新数据库 schema、迁移用户配置 - /// + /// Called after update applies (Upgrade process). Clean up temp files, migrate configs. public async Task OnAfterUpdateAsync(UpdateContext context) { - Console.WriteLine($"[Hooks] 更新完成: {context.LastVersion}"); - - // 清理临时文件 + Console.WriteLine($"[Hooks] Update complete: {context.LastVersion}"); var tempDir = context.UpdatePath; if (Directory.Exists(tempDir)) - { - try { Directory.Delete(tempDir, true); } - catch { /* 忽略清理中的错误 */ } - } - + try { Directory.Delete(tempDir, true); } catch { } await Task.CompletedTask; } - /// - /// 更新过程出错时调用 - /// 可用于:错误日志、通知管理员、触发回滚 - /// + /// Called on update error. Log the error, notify admin, trigger rollback. public async Task OnUpdateErrorAsync(UpdateContext context, Exception exception) { - Console.WriteLine($"[Hooks] 更新失败: {exception.Message}"); - // 记录错误日志 - File.WriteAllText( - Path.Combine(context.InstallPath, "update_error.log"), + Console.WriteLine($"[Hooks] Update failed: {exception.Message}"); + File.WriteAllText(Path.Combine(context.InstallPath, "update_error.log"), $"[{DateTime.UtcNow}] {exception}"); await Task.CompletedTask; } - /// - /// 启动主应用前调用(在 Upgrade 进程) - /// 可用于:修改配置文件、设置环境变量、检查版本兼容性 - /// 返回 false 阻止主应用启动 - /// + /// Called before starting the main app. Return false to prevent launch. public async Task OnBeforeStartAppAsync(UpdateContext context) { - Console.WriteLine($"[Hooks] 准备启动主应用: {context.MainAppName}"); + Console.WriteLine($"[Hooks] Preparing to launch: {context.MainAppName}"); - // 在 Linux/MacOS 上设置可执行权限 + // Set executable permissions on Linux/macOS if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { var appPath = Path.Combine(context.InstallPath, context.MainAppName ?? ""); if (File.Exists(appPath)) - { await UnixPermissionHooks.SetExecutablePermissionAsync(appPath); - } } - return true; // true = 启动主应用 + return true; } } diff --git a/.claude/skills/generalupdate-advanced/templates/CustomStrategy.cs b/.claude/skills/generalupdate-advanced/templates/CustomStrategy.cs index 5e767c0..49f91d1 100644 --- a/.claude/skills/generalupdate-advanced/templates/CustomStrategy.cs +++ b/.claude/skills/generalupdate-advanced/templates/CustomStrategy.cs @@ -2,74 +2,52 @@ using GeneralUpdate.Core.Pipeline; /// -/// 【Skill 自动生成】自定义平台更新策略 +/// [Skill Generated] Custom platform update strategy. +/// Completely replaces default strategies (WindowsStrategy / LinuxStrategy / MacStrategy). /// -/// 完全替换 GeneralUpdate 的默认策略(WindowsStrategy / LinuxStrategy / MacStrategy)。 -/// 适用于需要完全控制更新行为的场景。 -/// -/// 使用方式: +/// Usage: /// .Strategy() -/// -/// IStrategy 接口主要方法: -/// - ExecuteAsync(UpdateContext):执行策略主体 -/// - StartAppAsync(UpdateContext):启动主应用 /// public class MyCustomStrategy : AbstractStrategy { - /// - /// 自定义策略入口 - /// public override async Task ExecuteAsync(UpdateContext context) { - Console.WriteLine("[CustomStrategy] 执行自定义更新策略"); + Console.WriteLine("[CustomStrategy] Executing custom update strategy"); - // 1. 前置检查 + // 1. Pre-update check if (await Hooks.SafeOnBeforeUpdateAsync(context) == false) { - Console.WriteLine("[CustomStrategy] 前置检查未通过,中止更新"); + Console.WriteLine("[CustomStrategy] Pre-check failed, aborting"); return; } - // 2. 对每个版本执行管道 + // 2. Process each version through the pipeline foreach (var version in context.UpdateVersions) { - Console.WriteLine($"[CustomStrategy] 处理版本: {version.Version}"); + Console.WriteLine($"[CustomStrategy] Processing version: {version.Version}"); - // 2a. 构建管道(可自定义) var pipeline = new PipelineBuilder(context) - .UseMiddleware() // SHA256 校验 - .UseMiddleware() // 解压 ZIP + .UseMiddleware() + .UseMiddleware() .Build(); - // 2b. 执行管道 await pipeline.ExecuteAsync(context, version); - - Console.WriteLine($"[CustomStrategy] 版本 {version.Version} 处理完成"); + Console.WriteLine($"[CustomStrategy] Version {version.Version} done"); } - // 3. 后置处理 + // 3. Post-update await Hooks.SafeOnAfterUpdateAsync(context); - // 4. 启动主应用 + // 4. Start main app await StartAppAsync(context); } - /// - /// 启动主应用 - /// public override async Task StartAppAsync(UpdateContext context) { - Console.WriteLine("[CustomStrategy] 启动主应用"); - - var appPath = Path.Combine( - context.InstallPath, - context.MainAppName ?? "MyApp.exe"); - + Console.WriteLine("[CustomStrategy] Starting main app"); + var appPath = Path.Combine(context.InstallPath, context.MainAppName ?? "MyApp.exe"); if (!File.Exists(appPath)) - { - throw new FileNotFoundException( - $"主应用不存在: {appPath}"); - } + throw new FileNotFoundException($"App not found: {appPath}"); var process = Process.Start(new ProcessStartInfo { @@ -79,17 +57,11 @@ public override async Task StartAppAsync(UpdateContext context) }); if (process == null) - { - throw new InvalidOperationException( - $"无法启动主应用: {appPath}"); - } + throw new InvalidOperationException($"Failed to start: {appPath}"); - Console.WriteLine($"[CustomStrategy] 主应用已启动 (PID: {process.Id})"); + Console.WriteLine($"[CustomStrategy] App started (PID: {process.Id})"); } - /// - /// 实现原样退出 - /// public override async Task ExecuteAsync(UpdateContext context, string pipeHandle) { await ExecuteAsync(context); diff --git a/.claude/skills/generalupdate-advanced/templates/NamedPipeIPC.cs b/.claude/skills/generalupdate-advanced/templates/NamedPipeIPC.cs index c681d60..a769e9b 100644 --- a/.claude/skills/generalupdate-advanced/templates/NamedPipeIPC.cs +++ b/.claude/skills/generalupdate-advanced/templates/NamedPipeIPC.cs @@ -1,106 +1,84 @@ using System.IO.Pipes; -using System.Security.Cryptography; using System.Text; using System.Text.Json; /// -/// 【Skill 自动生成】命名管道 IPC(替代加密文件 IPC) +/// [Skill Generated] NamedPipe IPC — replaces encrypted file IPC. /// -/// GeneralUpdate 默认使用 AES 加密文件进行 Client → Upgrade 的 IPC 通信。 -/// 在某些场景下(如安全要求高、反病毒软件干扰文件访问), -/// 可以使用命名管道(NamedPipe)替代文件 IPC。 +/// Advantages over default encrypted file IPC: +/// - No disk writes (more secure) +/// - No TOCTOU attack risk +/// - No antivirus interference +/// - Bidirectional communication /// -/// 优势: -/// - 无磁盘文件写入(安全性更高) -/// - 无 TOCTOU 攻击风险 -/// - 无防病毒软件干扰 -/// - 支持双向通信 +/// The default GeneralUpdate IPC uses AES-encrypted files at +/// %TEMP%/GeneralUpdate/ipc/process_info.enc. For environments +/// with high security requirements or antivirus interference, +/// NamedPipe provides a safer alternative. /// -/// 注意:此实现需要自行集成到 GeneralUpdateBootstrap 的流程中。 -/// 当前 GeneralUpdate 默认使用 EncryptedFileProcessContractProvider, -/// 可通过 IProcessInfoProvider 接口替换。 -/// -/// NuGet: 无需额外包(System.IO.Pipes 在 .NET 内置) +/// Integration: Wire into IProcessInfoProvider to replace the default. /// public class NamedPipeIpcProvider : IAsyncDisposable { - private const string PipeName = "GeneralUpdate_IPC_" + /* ProcessId */ ""; + // Unique pipe name per client process — prevents collisions between parallel instances + private readonly string _pipeName = $"GeneralUpdate_IPC_{Environment.ProcessId}"; private NamedPipeServerStream? _server; private NamedPipeClientStream? _client; private readonly CancellationTokenSource _cts = new(); - /// - /// 由 Client 进程调用:创建服务端管道,等待 Upgrade 进程连接 - /// + /// Called by Client process: create server pipe, wait for Upgrade. public async Task ServerWaitAsync(int timeoutMs = 30000) { - _server = new NamedPipeServerStream( - PipeName, - PipeDirection.InOut, - maxNumberOfServerInstances: 1, - TransmissionMode.Byte, - PipeOptions.Asynchronous); + _server = new NamedPipeServerStream(_pipeName, PipeDirection.InOut, + maxNumberOfServerInstances: 1, TransmissionMode.Byte, PipeOptions.Asynchronous); - // 等待 Upgrade 进程连接 - await _server.WaitForConnectionAsync(_cts.Token); - return PipeName; // 返回管道名,通过环境变量传给 Upgrade 进程 + // Enforce caller-supplied timeout via CancellationTokenSource + using var timeoutCts = new CancellationTokenSource(timeoutMs); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, _cts.Token); + try + { + await _server.WaitForConnectionAsync(linkedCts.Token); + } + catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested) + { + throw new TimeoutException($"Upgrade process did not connect within {timeoutMs}ms"); + } + return _pipeName; } - /// - /// 由 Upgrade 进程调用:连接到 Client 创建的管道 - /// + /// Called by Upgrade process: connect to Client pipe. public async Task ClientConnectAsync(string pipeName, int timeoutMs = 30000) { - _client = new NamedPipeClientStream( - ".", - pipeName, - PipeDirection.InOut, - PipeOptions.Asynchronous); - + _client = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); await _client.ConnectAsync(timeoutMs, _cts.Token); } - /// - /// 发送数据 - /// + /// Send serialized data through the pipe. public async Task SendAsync(T data) { - var stream = _server ?? _client as Stream - ?? throw new InvalidOperationException("未连接"); - + var stream = _server ?? _client as Stream ?? throw new InvalidOperationException("Not connected"); var json = JsonSerializer.Serialize(data); var bytes = Encoding.UTF8.GetBytes(json); var lengthBytes = BitConverter.GetBytes(bytes.Length); - - // 先发长度,再发内容 await stream.WriteAsync(lengthBytes, _cts.Token); await stream.WriteAsync(bytes, _cts.Token); await stream.FlushAsync(_cts.Token); } - /// - /// 接收数据 - /// + /// Receive deserialized data from the pipe. public async Task ReceiveAsync() { - var stream = _server ?? _client as Stream - ?? throw new InvalidOperationException("未连接"); - - // 先读长度 + var stream = _server ?? _client as Stream ?? throw new InvalidOperationException("Not connected"); var lengthBuffer = new byte[4]; await stream.ReadAsync(lengthBuffer, _cts.Token); var length = BitConverter.ToInt32(lengthBuffer); - - // 再读内容 var buffer = new byte[length]; var offset = 0; while (offset < length) { - var read = await stream.ReadAsync( - buffer, offset, length - offset, _cts.Token); + var read = await stream.ReadAsync(buffer, offset, length - offset, _cts.Token); offset += read; } - var json = Encoding.UTF8.GetString(buffer); return JsonSerializer.Deserialize(json); } diff --git a/.claude/skills/generalupdate-init/project-scaffold/ClientProgram.cs b/.claude/skills/generalupdate-init/project-scaffold/ClientProgram.cs index b9560ca..3161069 100644 --- a/.claude/skills/generalupdate-init/project-scaffold/ClientProgram.cs +++ b/.claude/skills/generalupdate-init/project-scaffold/ClientProgram.cs @@ -5,38 +5,35 @@ string updateUrl = args.Length > 0 ? args[0] : "https://your-server.com/api"; string secretKey = args.Length > 1 ? args[1] : "your-secret-key"; -Console.WriteLine($"[Client] 启动版本检查: {updateUrl}"); -Console.WriteLine($"[Client] 当前版本: {GetCurrentVersion()}"); +Console.WriteLine($"[Client] Starting version check: {updateUrl}"); +Console.WriteLine($"[Client] Current version: {GetCurrentVersion()}"); var result = await new GeneralUpdateBootstrap() .SetSource(updateUrl, secretKey) .SetOption(Option.AppType, AppType.Client) .AddListenerUpdateInfo((_, e) => - Console.WriteLine($"[Client] 发现新版本: {e.Version} ({e.Size} bytes)")) + Console.WriteLine($"[Client] Found version: {e.Version} ({e.Size} bytes)")) .AddListenerMultiDownloadStatistics((_, e) => - Console.WriteLine($"[Client] 下载进度: {e.ProgressValue}% | {e.Speed}/s")) + Console.WriteLine($"[Client] Download: {e.ProgressValue}% | {e.Speed}/s")) .AddListenerMultiDownloadCompleted((_, e) => - Console.WriteLine($"[Client] 版本 {e.Versions?.LastOrDefault()?.Version} 下载完成")) + Console.WriteLine($"[Client] Version {e.Versions?.LastOrDefault()?.Version} downloaded")) .AddListenerMultiAllDownloadCompleted((_, e) => - Console.WriteLine("[Client] 全部下载完成,即将启动升级程序")) + Console.WriteLine("[Client] All downloads complete, starting Upgrade process")) .AddListenerException((_, e) => - Console.WriteLine($"[Client] 错误: {e.Message}")) + Console.WriteLine($"[Client] Error: {e.Message}")) .LaunchAsync(); if (result) -{ - Console.WriteLine("[Client] 更新完成,应用重启中..."); -} + Console.WriteLine("[Client] Update complete, restarting..."); else { - Console.WriteLine("[Client] 已是最新版本"); - Console.WriteLine("按任意键退出..."); + Console.WriteLine("[Client] Already latest version"); + Console.WriteLine("Press any key to exit..."); Console.ReadKey(); } static string GetCurrentVersion() { - var version = System.Reflection.Assembly.GetEntryAssembly() - ?.GetName()?.Version; - return version?.ToString() ?? "1.0.0.0"; + return System.Reflection.Assembly.GetEntryAssembly() + ?.GetName()?.Version?.ToString(4) ?? "1.0.0.0"; } diff --git a/.claude/skills/generalupdate-init/project-scaffold/UpgradeApp.csproj b/.claude/skills/generalupdate-init/project-scaffold/UpgradeApp.csproj index e6aef39..0842860 100644 --- a/.claude/skills/generalupdate-init/project-scaffold/UpgradeApp.csproj +++ b/.claude/skills/generalupdate-init/project-scaffold/UpgradeApp.csproj @@ -1,20 +1,17 @@ - Exe net8.0 enable enable - - - - diff --git a/.claude/skills/generalupdate-init/project-scaffold/UpgradeProgram.cs b/.claude/skills/generalupdate-init/project-scaffold/UpgradeProgram.cs index 93ac2d5..0006bd0 100644 --- a/.claude/skills/generalupdate-init/project-scaffold/UpgradeProgram.cs +++ b/.claude/skills/generalupdate-init/project-scaffold/UpgradeProgram.cs @@ -3,36 +3,30 @@ using GeneralUpdate.Core.Enum; /// -/// Upgrade 升级程序 — 由 Client 进程通过 IPC 启动 +/// Upgrade process — launched by Client via IPC. /// -/// 职责:读取 IPC 数据 → 应用更新 → 启动主程序 → 退出 -/// 注意:Upgrade 程序不访问网络(所有数据由 Client 预下载) -/// Upgrade 程序和 Client 必须使用相同的 AppSecretKey +/// Responsibilities: Read IPC -> Apply updates -> Start main app -> Exit +/// Note: Upgrade process does NOT access network (all data pre-downloaded by Client). +/// Upgrade and Client MUST use the same AppSecretKey. /// -Console.WriteLine("[Upgrade] 升级程序启动"); +Console.WriteLine("[Upgrade] Upgrade process started"); try { var result = await new GeneralUpdateBootstrap() - // Upgrade 模式会从 IPC 文件读取配置,无需 SetSource .SetOption(Option.AppType, AppType.Upgrade) - - // 事件监听 .AddListenerProgress((_, e) => - Console.WriteLine($"[Upgrade] 处理: {e.FileName} — {e.ProgressValue}% ({e.Type})")) + Console.WriteLine($"[Upgrade] Processing: {e.FileName} - {e.ProgressValue}% ({e.Type})")) .AddListenerException((_, e) => - Console.WriteLine($"[Upgrade] 错误: {e.Message}")) - + Console.WriteLine($"[Upgrade] Error: {e.Message}")) .LaunchAsync(); Console.WriteLine(result - ? "[Upgrade] 更新完成,主程序已启动" - : "[Upgrade] 无需更新,主程序已启动"); + ? "[Upgrade] Update complete, main app started" + : "[Upgrade] No update needed, main app started"); } catch (Exception ex) { - Console.WriteLine($"[Upgrade] 严重错误: {ex}"); - // Upgrade 失败不应完全静默,记录日志后退出 - // Client 下次启动时会重试 + Console.WriteLine($"[Upgrade] Fatal error: {ex}"); Environment.ExitCode = 1; } diff --git a/.claude/skills/generalupdate-init/reference.md b/.claude/skills/generalupdate-init/reference.md index bbe4afc..5b4c493 100644 --- a/.claude/skills/generalupdate-init/reference.md +++ b/.claude/skills/generalupdate-init/reference.md @@ -133,11 +133,10 @@ Content-Type: application/json ## 框架兼容性矩阵 -| 框架 | 最低版本 | AOT 兼容 | SignalR 支持 | -|------|---------|---------|------------| -| WPF (.NET Framework) | .NET 8 | ✅ | ✅ (JSON协议) | -| WPF (.NET Core) | .NET 8 | ✅ | ✅ | -| WinForms | .NET 8 | ✅ | ✅ | +| 框架 | 最低 SDK 版本 | AOT 兼容 | SignalR 支持 | +|------|:------------:|:---------:|:-----------:| +| WPF (Windows) | .NET 8 (`net8.0-windows`) | ✅ | ✅ (JSON协议) | +| WinForms (Windows) | .NET 8 (`net8.0-windows`) | ✅ | ✅ | | Avalonia | .NET 8 | ✅ | ✅ | | MAUI | .NET 10 | ✅ | ❌ (无 SignalR) | | 控制台 | .NET 8 | ✅ | ✅ | diff --git a/.claude/skills/generalupdate-init/templates/FullIntegration.cs b/.claude/skills/generalupdate-init/templates/FullIntegration.cs index ede0021..d0d974d 100644 --- a/.claude/skills/generalupdate-init/templates/FullIntegration.cs +++ b/.claude/skills/generalupdate-init/templates/FullIntegration.cs @@ -1,6 +1,9 @@ +using System.Text.Json; using GeneralUpdate.Core; using GeneralUpdate.Core.Configuration; using GeneralUpdate.Core.Enum; +using GeneralUpdate.Core.Event; +using Microsoft.Extensions.Configuration; /// /// GeneralUpdate 完整集成示例 — 包含所有配置选项、事件监听和 4 种场景处理 @@ -134,7 +137,7 @@ private static void OnUpdateInfo(object? sender, UpdateInfoEventArgs e) { var type = v.IsCrossVersion ? "跨版本" : (v.FromVersion == null ? "全量" : "增量"); Console.WriteLine($" ├─ {v.Version} [{type}] {v.Name}"); - Console.WriteLine($" │ Hash: {v.Hash?[..16]}..."); + Console.WriteLine($" │ Hash: {(v.Hash?.Length >= 16 ? v.Hash[..16] : v.Hash ?? "N/A")}..."); Console.WriteLine($" │ AppType: {v.AppType} (0=Client, 1=Upgrade)"); // ⚠️ AppType 决定场景判断(#465),Client 包 → HasMainUpdate,Upgrade 包 → HasUpgradeUpdate } @@ -145,10 +148,10 @@ private static void OnUpdateInfo(object? sender, UpdateInfoEventArgs e) private static void OnDownloadStats(object? sender, MultiDownloadStatisticsEventArgs e) { // e.ProgressValue: 0-100 - // e.Speed: 格式如 "2.5 MB/s" + // e.Speed: 格式如 "2.5 MB/s"(已包含单位) // e.Remaining: TimeSpan // e.Version: VersionEntry?(当前下载的版本) - Console.Write($"\r[下载] {e.ProgressPercentage:F0}% | {e.Speed}/s | " + + Console.Write($"\r[下载] {e.ProgressPercentage:F0}% | {e.Speed} | " + $"剩余 {e.Remaining:hh\\:mm\\:ss}"); } diff --git a/.claude/skills/generalupdate-init/templates/MinimalIntegration.cs b/.claude/skills/generalupdate-init/templates/MinimalIntegration.cs index 2b90a43..3a48bff 100644 --- a/.claude/skills/generalupdate-init/templates/MinimalIntegration.cs +++ b/.claude/skills/generalupdate-init/templates/MinimalIntegration.cs @@ -47,9 +47,12 @@ public static async Task RunAsync() * 3. 文件放错目录(必须和 .exe 同级) * 4. UpdatePath 写成了相对路径但当前目录不对 * - * ⚠️ 版本号规则:必须是 4 段式,如 "1.0.0.0" - * "1.0" 会被 System.Version 解析为 1.0.0.0(未指定段为 -1) - * 但服务器可能返回 1.0.0.0 → 版本比较可能出错 + * ⚠️ Version format: Must use 4 segments, e.g. "1.0.0.0" + * new Version("1.0") parses to 1.0.-1.-1 (Build=-1, Revision=-1), + * NOT 1.0.0.0. When the server returns 1.0.0.0 (Build=0, Revision=0), + * new Version("1.0") < new Version("1.0.0.0") is TRUE, + * causing a false "update available" detection → infinite upgrade loop. + * ALWAYS use 4-segment version strings everywhere. * * ⚠️ 路径规则: * InstallPath: 可写 "."(表示 exe 所在目录)或绝对路径 diff --git a/.claude/skills/generalupdate-init/templates/generalupdate.manifest.json b/.claude/skills/generalupdate-init/templates/generalupdate.manifest.json index 011b2bf..57b3b91 100644 --- a/.claude/skills/generalupdate-init/templates/generalupdate.manifest.json +++ b/.claude/skills/generalupdate-init/templates/generalupdate.manifest.json @@ -7,26 +7,3 @@ "ClientVersion": "1.0.0.0", "UpgradeClientVersion": "1.0.0.0" } - -/* ═══════════════════════════════════════════════════ - * generalupdate.manifest.json 字段说明 - * - * MainAppName — 主程序可执行文件名(含扩展名) - * UpdateAppName — 升级程序可执行文件名(含扩展名) - * ProductId — 产品标识(与服务端 ProductId 对应) - * InstallPath — 安装目录("."=当前目录,或绝对路径) - * UpdatePath — Upgrade.exe 所在子目录(相对 InstallPath) - * ClientVersion — 主程序当前版本号(必须 4 段式!不能为空!) - * UpgradeClientVersion — 升级程序当前版本号 - * - * ⚠️ 不得修改文件名! - * ⚠️ 必须放在应用根目录(和 .exe 同级) - * ⚠️ ClientVersion 和 UpgradeClientVersion 必须是 4 段式版本号 - * ⚠️ 空字段会导致 AppMetadataDiscoverer.Discover() 无法自动发现 - * - * 条件可选字段(写了会覆盖自动发现,不写则自动从程序集读取): - * InstallPath、ClientVersion、UpgradeClientVersion - * - * 必填字段(manifest 必须提供): - * MainAppName、UpdateAppName、ProductId - ═══════════════════════════════════════════════════ */ diff --git a/.claude/skills/generalupdate-strategy/examples/ClientServerStrategy.cs b/.claude/skills/generalupdate-strategy/examples/ClientServerStrategy.cs index 7f4587e..8562ede 100644 --- a/.claude/skills/generalupdate-strategy/examples/ClientServerStrategy.cs +++ b/.claude/skills/generalupdate-strategy/examples/ClientServerStrategy.cs @@ -3,16 +3,12 @@ using GeneralUpdate.Core.Enum; /// -/// 策略 1:标准客户端-服务端更新 +/// Strategy 1: Standard Client-Server update. +/// Suitable for most scenarios with a backend server. /// -/// 适用场景: -/// - 已部署 GeneralSpacestation 或兼容的后端服务 -/// - 需要精确控制更新包的发布 -/// - 需要升级状态跟踪和上报 -/// -/// 后端要求: -/// - POST /Upgrade/Verification — 版本验证 -/// - (可选) POST /Upgrade/Report — 状态上报 +/// Backend required: +/// - POST /Upgrade/Verification — version verification +/// - (Optional) POST /Upgrade/Report — status reporting /// /// NuGet: dotnet add package GeneralUpdate.Core /// @@ -21,27 +17,18 @@ public static class ClientServerStrategy public static async Task RunAsync() { var bootstrap = new GeneralUpdateBootstrap() - .SetSource( - "https://your-server.com/api", - "your-32-char-secret-key") + .SetSource("https://your-server.com/api", "your-secret-key") .SetOption(Option.AppType, AppType.Client) - - // 可选配置 .SetOption(Option.MaxConcurrency, 3) .SetOption(Option.BackupEnabled, true) - .SetOption(Option.PatchEnabled, false) - .SetOption(Option.DownloadTimeout, 60) - - // 事件 .AddListenerUpdateInfo((_, e) => - Console.WriteLine($"发现版本: {e.Version} | 大小: {e.Size}")) + Console.WriteLine($"[Version] Found: {e.Version} | Size: {e.Size}")) .AddListenerMultiDownloadStatistics((_, e) => - Console.WriteLine($"进度: {e.ProgressValue}% | {e.Speed}")) + Console.WriteLine($"[Download] {e.ProgressValue}% | {e.Speed}")) .AddListenerMultiDownloadCompleted((_, e) => - Console.WriteLine($"下载完成: {e.Versions?.LastOrDefault()?.Version}")) + Console.WriteLine($"[Download] Complete: {e.Versions?.LastOrDefault()?.Version}")) .AddListenerException((_, e) => - Console.WriteLine($"错误: {e.Message}")); - + Console.WriteLine($"[Error] {e.Message}")); await bootstrap.LaunchAsync(); } } diff --git a/.claude/skills/generalupdate-strategy/examples/CrossVersionStrategy.cs b/.claude/skills/generalupdate-strategy/examples/CrossVersionStrategy.cs index 4eca92f..7f83bc9 100644 --- a/.claude/skills/generalupdate-strategy/examples/CrossVersionStrategy.cs +++ b/.claude/skills/generalupdate-strategy/examples/CrossVersionStrategy.cs @@ -3,68 +3,41 @@ using GeneralUpdate.Core.Enum; /// -/// 策略 5:跨版本直跳更新(CVP — Cross-Version Package) +/// Strategy 5: Cross-Version Package (CVP) — jump directly to latest. +/// Skips all intermediate versions, applies one CVP delta package. /// -/// 适用场景: -/// - 用户长期没有更新(如 v1.0 → 最新 v3.0) -/// - 中间有大量版本,逐个下载太慢 -/// - 需要从指定版本直接跳到目标版本 +/// Server: uses two full-package archives (v1 and v3) -> CrossVersionPacketBuilder +/// -> DiffPipeline.CleanAsync() -> CVP delta package +/// Client: downloads one CVP package -> applies directly -> done /// -/// 工作原理: -/// 服务端自动构建: -/// 1. 取两个版本的全量包归档(如 v1.0 和 v3.0 的 ZIP) -/// 2. CrossVersionPacketBuilder 调用 DiffPipeline.CleanAsync() 生成差分包 -/// 3. 上传差分包到对象存储,写入 TbPacket(IsCrossVersion=true) -/// -/// 客户端流程: -/// Verify → 服务端返回 CVP 包(一个 ZIP) -/// → 下载 → 应用(一次 patch 操作)→ 完成 -/// -/// CVP + 链式兜底(从 v5.0 开始支持,Issue #499): -/// 服务器一次返回所有路径(CVP 包 + 链式包),客户端优先走 CVP, -/// 失败自动退化为链式重试,无需二次请求。 +/// CVP + chain fallback (v5.0+, #499): +/// Server returns both CVP and chain packages. Client tries CVP first, +/// auto-falls back to chain if it fails. No second server request needed. /// /// NuGet: dotnet add package GeneralUpdate.Core -/// -/// ⚠️ 注意事项: -/// - 需要 GeneralSpacestation 服务端支持 CrossVersion 构建 -/// - CVP 构建是异步的(Redis 队列 + BackgroundService 消费) -/// - 构建材料是全量包归档(TbVersionArchive) /// public static class CrossVersionStrategy { public static async Task RunAsync() { var bootstrap = new GeneralUpdateBootstrap() - .SetSource( - "https://your-server.com/api", - "your-secret-key") + .SetSource("https://your-server.com/api", "your-secret-key") .SetOption(Option.AppType, AppType.Client) - .SetOption(Option.BackupEnabled, true) // CVP 建议启用备份 - - // 跨版本更新配置 - // 服务端会自动判断返回 CVP 包还是链式包 - // 客户端优先尝试 CVP,失败自动退链式 - + .SetOption(Option.BackupEnabled, true) .AddListenerUpdateInfo((_, e) => { - Console.WriteLine($"[CVP] 版本: {e.Version} | " + - $"跨版本: {e.IsCrossVersion} | " + - $"文件数: {e.FileCount} | 大小: {e.Size}"); + Console.WriteLine($"[CVP] Version: {e.Version} | Cross-version: {e.IsCrossVersion}"); + Console.WriteLine($"[CVP] Files: {e.FileCount} | Size: {e.Size}"); }) .AddListenerMultiDownloadStatistics((_, e) => - Console.WriteLine($"[CVP] 下载: {e.ProgressValue}%")) + Console.WriteLine($"[CVP] Download: {e.ProgressValue}%")) .AddListenerMultiDownloadCompleted((_, e) => { - var lastVer = e.Versions?.LastOrDefault(); - Console.WriteLine($"[CVP] 包下载完成: {lastVer?.Version} " + - $"(跨版本: {lastVer?.IsCrossVersion})"); + var last = e.Versions?.LastOrDefault(); + Console.WriteLine($"[CVP] Package done: {last?.Version} (CVP: {last?.IsCrossVersion})"); }) .AddListenerException((_, e) => - { - Console.WriteLine($"[CVP] 错误: {e.Message}"); - // 如果 CVP 失败,GeneralUpdate 会自动退化为链式重试 - }); + Console.WriteLine($"[CVP] Error: {e.Message}")); await bootstrap.LaunchAsync(); } diff --git a/.claude/skills/generalupdate-strategy/examples/OssStrategy.cs b/.claude/skills/generalupdate-strategy/examples/OssStrategy.cs index aad13eb..17a5873 100644 --- a/.claude/skills/generalupdate-strategy/examples/OssStrategy.cs +++ b/.claude/skills/generalupdate-strategy/examples/OssStrategy.cs @@ -3,62 +3,45 @@ using GeneralUpdate.Core.Enum; /// -/// 策略 2:OSS 对象存储更新 +/// Strategy 2: OSS (Object Storage Service) update. +/// No backend server required — uses S3/MinIO/Aliyun OSS directly. /// -/// 适用场景: -/// - 没有后端服务,只有对象存储(AWS S3 / MinIO / 阿里云OSS / 华为OBS) -/// - 想以最低成本实现自动更新 -/// - 不需要复杂的版本管理和升级追踪 +/// How it works: +/// 1. Client downloads versions.json from OSS +/// 2. Compares client version vs latest in versions.json +/// 3. Downloads update ZIP directly from OSS +/// 4. Starts Upgrade process /// -/// 工作原理: -/// 1. Client 下载 versions.json(从 OSS 的固定路径) -/// 2. 比较客户端版本 vs versions.json 中的最新版本 -/// 3. 有更新 → 直接从 OSS 下载 ZIP 包 -/// 4. 启动 Upgrade 进程应用更新 -/// -/// OSS 上需要的文件: -/// your-bucket/ -/// ├── versions.json ← 版本清单,由发布工具生成 -/// ├── v1.1.0.0/ -/// │ └── update.zip -/// └── v1.2.0.0/ -/// └── update.zip -/// -/// versions.json 格式: -/// { -/// "versions": [ -/// { "version": "1.1.0.0", "url": "https://bucket/v1.1.0.0/update.zip", "hash": "...", "size": 1048576 }, -/// { "version": "1.2.0.0", "url": "https://bucket/v1.2.0.0/update.zip", "hash": "...", "size": 2097152 } -/// ] -/// } +/// OSS structure: +/// bucket/ +/// +-- versions.json +/// +-- v1.1.0.0/update.zip +/// +-- v1.2.0.0/update.zip /// /// NuGet: dotnet add package GeneralUpdate.Core /// -/// ⚠️ 已知问题(来自 Issue #485、#487): -/// 1. OSS 模式不区分 MainApp 和 UpgradeApp 更新,两者的可用性总是同步的 -/// 2. SSL 验证策略默认不覆盖文件下载请求 -/// 3. UpgradeApp.exe 必须放在 update/ 子目录中 +/// Known issues (#485, #487): +/// 1. OSS does not distinguish Main/Upgrade updates +/// 2. SSL validation does NOT cover file downloads +/// 3. Upgrade.exe must be in update/ subdirectory /// public static class OssStrategy { public static async Task RunAsync() { var bootstrap = new GeneralUpdateBootstrap() - .SetSource( - "https://your-storage.com/versions.json", // OSS versions.json URL - "your-secret-key") // 用于 IPC 加密 - .SetOption(Option.AppType, AppType.OssClient) // 使用 OSS 客户端模式 + .SetSource("https://your-storage.com/versions.json", "your-secret-key") + .SetOption(Option.AppType, AppType.OssClient) .SetOption(Option.MaxConcurrency, 3) .SetOption(Option.BackupEnabled, false) - .AddListenerMultiDownloadStatistics((_, e) => - Console.WriteLine($"OSS 下载: {e.ProgressValue}%")) + Console.WriteLine($"[OSS] Download: {e.ProgressValue}%")) .AddListenerMultiDownloadCompleted((_, e) => - Console.WriteLine($"OSS 版本 {e.Versions?.LastOrDefault()?.Version} 下载完成")) + Console.WriteLine($"[OSS] Version {e.Versions?.LastOrDefault()?.Version} done")) .AddListenerException((_, e) => - Console.WriteLine($"OSS 错误: {e.Message}")); + Console.WriteLine($"[OSS] Error: {e.Message}")); var result = await bootstrap.LaunchAsync(); - Console.WriteLine(result ? "更新完成" : "已是最新"); + Console.WriteLine(result ? "Update complete" : "Already latest"); } } diff --git a/.claude/skills/generalupdate-strategy/examples/PushStrategy.cs b/.claude/skills/generalupdate-strategy/examples/PushStrategy.cs index 0e110f7..3be7297 100644 --- a/.claude/skills/generalupdate-strategy/examples/PushStrategy.cs +++ b/.claude/skills/generalupdate-strategy/examples/PushStrategy.cs @@ -4,27 +4,20 @@ using Microsoft.AspNetCore.SignalR.Client; /// -/// 策略 6:SignalR 推送更新 +/// Strategy 6: SignalR Push update. +/// Server actively pushes update notifications to clients. /// -/// 适用场景: -/// - 需要服务端主动控制更新时机 -/// - 管理员从后台管理界面点击"推送更新" -/// - 需要立即通知所有在线的客户端 -/// -/// 工作流程: -/// 1. 客户端连接 SignalR Hub -/// 2. 管理员在后台上传新版本包 -/// 3. 管理员点击"推送" → SignalR Hub 通知所有客户端 -/// 4. 客户端收到推送 → 触发 GeneralUpdateBootstrap 开始更新 +/// Flow: +/// Client connects to SignalR Hub -> Admin uploads new package +/// -> Clicks "Push" -> Hub notifies all clients -> Bootstrap starts update /// /// NuGet: /// dotnet add package GeneralUpdate.Core /// dotnet add package Microsoft.AspNetCore.SignalR.Client /// -/// ⚠️ 已知问题(来自 Issue #402): -/// 1. SignalR Client 支持 Native AOT(需 JSON 协议 + JsonSerializerContext) -/// 2. UpgradeHubService 在 Dispose 后调用 StartAsync 会崩溃 -/// 解决方案:使用下面的 SafeHubConnection 包装类 +/// Known issue (#402, Code Audit #5): +/// UpgradeHubService.DisposeAsync does not null the connection reference +/// -> ObjectDisposedException on reconnect. Use SafeHubConnection wrapper. /// public static class PushStrategy { @@ -34,39 +27,34 @@ public static async Task RunAsync() var secretKey = "your-secret-key"; var hubUrl = "https://your-server.com/hub/upgrade"; - // 1. 连接到 SignalR Hub var connection = new HubConnectionBuilder() .WithUrl(hubUrl) - .WithAutomaticReconnect() // 自动重连 + .WithAutomaticReconnect() .Build(); - // 2. 注册推送事件处理 connection.On("OnPushUpgrade", async (message) => { - Console.WriteLine($"[推送] 收到更新通知: {message}"); + Console.WriteLine($"[Push] Update notification: {message}"); await StartUpdateAsync(updateUrl, secretKey); }); connection.On("OnForceUpgrade", async (message) => { - Console.WriteLine($"[推送] 收到强制更新通知: {message}"); - // 强制更新 - 不询问用户直接开始 + Console.WriteLine($"[Push] Force update: {message}"); await StartUpdateAsync(updateUrl, secretKey); }); - // 3. 启动连接 try { await connection.StartAsync(); - Console.WriteLine("[推送] 已连接到更新推送服务"); + Console.WriteLine("[Push] Connected to update push service"); } catch (Exception ex) { - Console.WriteLine($"[推送] 连接失败: {ex.Message}"); + Console.WriteLine($"[Push] Connection failed: {ex.Message}"); } - // 4. 保持应用运行 - Console.WriteLine("[推送] 等待服务端推送更新..."); + Console.WriteLine("[Push] Waiting for server push..."); await Task.Delay(Timeout.Infinite); } @@ -79,64 +67,52 @@ private static async Task StartUpdateAsync(string updateUrl, string secretKey) .SetOption(Option.AppType, AppType.Client) .SetOption(Option.BackupEnabled, true) .AddListenerMultiDownloadStatistics((_, e) => - Console.WriteLine($"[推送更新] 下载: {e.ProgressValue}%")) + Console.WriteLine($"[Push/Update] Download: {e.ProgressValue}%")) .AddListenerMultiDownloadCompleted((_, e) => - Console.WriteLine($"[推送更新] 下载完成")) + Console.WriteLine($"[Push/Update] Download complete")) .AddListenerException((_, e) => - Console.WriteLine($"[推送更新] 错误: {e.Message}")); + Console.WriteLine($"[Push/Update] Error: {e.Message}")); await bootstrap.LaunchAsync(); } catch (Exception ex) { - Console.WriteLine($"[推送更新] 启动失败: {ex.Message}"); + Console.WriteLine($"[Push/Update] Failed: {ex.Message}"); } } } /// -/// 安全的 HubConnection 包装器(修复 Dispose 后重连崩溃问题) +/// Safe HubConnection wrapper — fixes ObjectDisposedException on reconnect. +/// UpgradeHubService.DisposeAsync does not null the connection reference. +/// This wrapper ensures null after Dispose, allowing clean reconnect. /// public class SafeHubConnection : IAsyncDisposable { private HubConnection? _connection; private readonly string _hubUrl; - public SafeHubConnection(string hubUrl) - { - _hubUrl = hubUrl; - } + public SafeHubConnection(string hubUrl) { _hubUrl = hubUrl; } public async Task StartAsync() { - // 如果已释放,重新创建 if (_connection == null) - { - _connection = new HubConnectionBuilder() - .WithUrl(_hubUrl) - .WithAutomaticReconnect() - .Build(); - } - + _connection = new HubConnectionBuilder().WithUrl(_hubUrl).WithAutomaticReconnect().Build(); if (_connection.State != HubConnectionState.Connected) - { await _connection.StartAsync(); - } } public async Task StopAsync() { if (_connection?.State == HubConnectionState.Connected) - { await _connection.StopAsync(); - } } public HubConnectionState? State => _connection?.State; public IDisposable On(string methodName, Action handler) { - return _connection?.On(methodName, handler) ?? throw new InvalidOperationException("Connection not initialized"); + return _connection?.On(methodName, handler) ?? throw new InvalidOperationException("Not connected"); } public async ValueTask DisposeAsync() @@ -144,7 +120,7 @@ public async ValueTask DisposeAsync() if (_connection != null) { await _connection.DisposeAsync(); - _connection = null; // 必须置 null,否则重连时崩溃 + _connection = null; // CRITICAL: must null to prevent ObjectDisposedException on reconnect } } } diff --git a/.claude/skills/generalupdate-strategy/examples/SilentStrategy.cs b/.claude/skills/generalupdate-strategy/examples/SilentStrategy.cs index 592c84e..4e136eb 100644 --- a/.claude/skills/generalupdate-strategy/examples/SilentStrategy.cs +++ b/.claude/skills/generalupdate-strategy/examples/SilentStrategy.cs @@ -3,30 +3,23 @@ using GeneralUpdate.Core.Enum; /// -/// 策略 3:静默后台更新 +/// Strategy 3: Silent background update. +/// For long-running apps that should update without disturbing the user. +/// Updates are downloaded in the background and applied when the app exits. /// -/// 适用场景: -/// - 用户长期不关闭应用(如桌面工具、监控面板) -/// - 希望后台下载更新,不打扰用户 -/// - 用户下次打开应用时自动切换到新版本 -/// -/// 工作流程: -/// 1. 应用启动 → SilentPollOrchestrator 开始后台轮询 -/// 2. 轮询到新版本 → 后台下载所有包(不启动 Upgrade 进程) -/// 3. 下载完成 → 写入 IPC 文件,标记准备就绪 -/// 4. 用户关闭应用 → ProcessExit → 触发 Upgrade 进程 → 更新 → 下次启动是新版本 +/// Flow: Background polling -> download -> IPC -> ProcessExit -> Upgrade /// /// NuGet: dotnet add package GeneralUpdate.Core /// -/// ⚠️ 已知问题(来自 Issue #484、#471、#443): -/// 1. ProcessExit 事件不保证触发(FailFast、TerminateProcess、Ctrl+C 时不会) -/// 解决方案:在应用关闭逻辑中显式调用 TryLaunchUpgrade() -/// 2. 静默模式 PatchMiddleware 可能抛异常(PatchEnabled 的默认行为) -/// 解决方案:显式设置 SetOption(Option.PatchEnabled, false) -/// 3. manifest.json 的默认非空字段会阻塞自动版本发现 -/// 解决方案:确保 manifest.json 中字段为空或正确 -/// 4. 静默模式默认更新完不启动应用(#443) -/// 解决方案:通过 SetOption(Option.SilentAutoRestart, true) 配置 +/// Known issues (Issues #484, #471, #443): +/// 1. ProcessExit not guaranteed (FailFast/TerminateProcess/Ctrl+C) +/// Fix: Call TryLaunchUpgrade() explicitly on app close +/// 2. manifest.json defaults can block auto-discovery +/// Fix: Fill version fields in manifest +/// 3. PatchMiddleware throws without DiffPipeline injection +/// Fix: Set PatchEnabled = false for silent mode +/// 4. Auto-restarts app after update (#443) +/// Fix: Configure SilentAutoRestart = false /// public class SilentStrategy : IDisposable { @@ -36,68 +29,43 @@ public class SilentStrategy : IDisposable public async Task RunAsync() { _bootstrap = new GeneralUpdateBootstrap() - .SetSource( - "https://your-server.com/api", - "your-secret-key") + .SetSource("https://your-server.com/api", "your-secret-key") .SetOption(Option.AppType, AppType.Client) - - // 静默模式配置 - .SetOption(Option.Silent, true) // 启用静默模式 - .SetOption(Option.SilentPollIntervalMinutes, 60) // 每60分钟检查一次 - .SetOption(Option.PatchEnabled, false) // 关闭差分(避免#471 Bug) - .SetOption(Option.BackupEnabled, true) // 静默模式建议启用备份 - + .SetOption(Option.Silent, true) + .SetOption(Option.SilentPollIntervalMinutes, 60) + .SetOption(Option.PatchEnabled, false) // Avoid issue #471 + .SetOption(Option.BackupEnabled, true) .AddListenerUpdateInfo((_, e) => - Console.WriteLine($"[静默] 发现新版本: {e.Version}")) + Console.WriteLine($"[Silent] New version: {e.Version}")) .AddListenerMultiDownloadStatistics((_, e) => - Console.WriteLine($"[静默] 后台下载: {e.ProgressValue}%")) + Console.WriteLine($"[Silent] Downloading: {e.ProgressValue}%")) .AddListenerMultiDownloadCompleted((_, e) => - Console.WriteLine($"[静默] 版本 {e.Versions?.LastOrDefault()?.Version} 就绪")) + Console.WriteLine($"[Silent] Version {e.Versions?.LastOrDefault()?.Version} ready")) .AddListenerException((_, e) => - Console.WriteLine($"[静默] 错误: {e.Message}")); + Console.WriteLine($"[Silent] Error: {e.Message}")); var result = await _bootstrap.LaunchAsync(); - - // 获取静默轮询器,以便手动触发更新 _orchestrator = _bootstrap.SilentOrchestrator; if (_orchestrator != null) - { _orchestrator.HasPreparedUpdate += () => - { - Console.WriteLine("[静默] 更新已就绪,将在进程退出时安装"); - }; - } + Console.WriteLine("[Silent] Update ready, will install on exit"); - // 启动应用主循环... await RunApplicationAsync(); } - /// - /// 应用关闭时主动触发更新,弥补 ProcessExit 不稳定的问题 - /// + /// Call this on app close instead of relying on ProcessExit. public void OnAppClosing() { - // 显式触发升级(比依赖 ProcessExit 更可靠) - try - { - _orchestrator?.TryLaunchUpgrade(); - } - catch (Exception ex) - { - Console.WriteLine($"[静默] 触发升级失败: {ex.Message}"); - } + try { _orchestrator?.TryLaunchUpgrade(); } + catch (Exception ex) { Console.WriteLine($"[Silent] Launch failed: {ex.Message}"); } } private async Task RunApplicationAsync() { - // 模拟应用主循环 - Console.WriteLine("[静默] 应用正在运行,更新将在后台静默下载..."); - await Task.Delay(TimeSpan.FromHours(8)); // 模拟长时间运行 + Console.WriteLine("[Silent] App running, updates downloading in background..."); + await Task.Delay(TimeSpan.FromHours(8)); } - public void Dispose() - { - OnAppClosing(); - } + public void Dispose() => OnAppClosing(); } diff --git a/.claude/skills/generalupdate-troubleshoot/reference.md b/.claude/skills/generalupdate-troubleshoot/reference.md index 445a110..9257e76 100644 --- a/.claude/skills/generalupdate-troubleshoot/reference.md +++ b/.claude/skills/generalupdate-troubleshoot/reference.md @@ -1,276 +1,276 @@ -# GeneralUpdate 故障排查参考手册(完整版) +# GeneralUpdate Troubleshooting Reference Manual (Complete Edition) -> 覆盖 50+ 症状,均来自 GitHub Issues(#308–#517)、Gitee Issues(30个)、 -> 及全面代码审计(17 CRITICAL/HIGH 项 + 14 MEDIUM 项 + 10 INFO 项) -> 排查日期:2026-06-16 +> Covers 50+ symptoms from GitHub Issues (#308-#517), Gitee Issues (30 items), +> and full code audit (17 CRITICAL/HIGH items + 14 MEDIUM items + 10 INFO items) +> Audit date: 2026-06-16 --- -## 使用方式 +## How to Use -按症状查找 → 确认根因 → 应用修复。如果症状不匹配,运行通用诊断流程(见底部)。 +Find symptom -> Confirm root cause -> Apply fix. If no symptom matches, run the general diagnostic workflow (see bottom). --- -## 🔴 一级:致命/阻断性故障 +## Level 1: Critical/Blocking Faults -### C1. 升级进程没启动 / "FileNotFoundException: upgrade application not found" +### C1. Upgrade process doesn't start / "FileNotFoundException: upgrade application not found" -| 来源 | 根因 | 诊断 | +| Source | Root Cause | Diagnosis | |------|------|------| -| #485, #ID3H5V | UpgradeApp.exe 未随主程序发布 | 检查 `UpdatePath` + `UpdateAppName` | +| #485, #ID3H5V | UpgradeApp.exe not shipped with main app | Check `UpdatePath` + `UpdateAppName` | -**修复**: -1. UpgradeApp.exe **必须**从首个版本就和主程序一起发布 -2. OSS 模式下 Upgrade.exe 必须放在 `update/` 子目录(#485) -3. `generalupdate.manifest.json` 中 `UpdateAppName` 必须包含 `.exe` +**Fix**: +1. UpgradeApp.exe **must** be shipped together with the main app starting from the first version +2. In OSS mode, Upgrade.exe must be placed in the `update/` subdirectory (#485) +3. `generalupdate.manifest.json` `UpdateAppName` must include `.exe` ``` -发布目录结构: +Deployment directory structure: /InstallPath ├── MyApp.exe ├── generalupdate.manifest.json └── update/ - └── UpgradeApp.exe ← 必须存在 + └── UpgradeApp.exe ← Must exist ``` --- -### C2. "Method not found" — NuGet 版本冲突 +### C2. "Method not found" — NuGet version conflict -| 来源 | 根因 | 诊断 | +| Source | Root Cause | Diagnosis | |------|------|------| -| #I7MCA5 | Client 和 Upgrade 使用不同版本 NuGet | 检查两个项目的 csproj | +| #I7MCA5 | Client and Upgrade use different NuGet versions | Check csproj of both projects | -**修复**:Client.csproj 和 Upgrade.csproj 使用完全相同版本: +**Fix**: Client.csproj and Upgrade.csproj use exactly the same version: ```xml ``` --- -### C3. BSOD / 内存溢出 / 进程崩溃 — BSDIFF 整数溢出 +### C3. BSOD / Memory overflow / Process crash — BSDIFF integer overflow -| 来源 | 代码审计 #3, #4 | +| Source | Code Audit #3, #4 | |------|----------------| -| **根因** | `BsdiffDiffer.WriteInt64` 对 `long.MinValue` 求反溢出;control 值 `> int.MaxValue` 转型截断产生负值 | +| **Root Cause** | `BsdiffDiffer.WriteInt64` negation overflow on `long.MinValue`; control value `> int.MaxValue` cast truncation produces negative values | -**影响**:恶意构造的 patch 文件或超过 2GB 的正常 patch 可导致进程崩溃或 OOM -**修复**:更新到 v5.0+(#514 已修复)。如无法更新,在差分引擎中添加 `MaxInputFileSize` 限制 +**Impact**: Maliciously crafted patch files or normal patches exceeding 2GB can cause process crash or OOM +**Fix**: Update to v5.0+ (fixed in #514). If unable to update, add `MaxInputFileSize` limit in the diff engine --- -### C4. 备份递归嵌套 → PathTooLongException(路径超长) +### C4. Backup recursive nesting → PathTooLongException (Path too long) -| 来源 | #501 | +| Source | #501 | |------|------| -| **根因** | `StorageManager.Backup()` 在 `InstallPath` **内部**创建备份目录,且空列表 `new List()` 不触发默认跳过目录逻辑 | +| **Root Cause** | `StorageManager.Backup()` creates backup directory **inside** `InstallPath`, and an empty list `new List()` does not trigger the default directory skip logic | -**修复**:更新到 v5.0+(#510 默认关闭备份)。手动启用时显式指定跳过目录: +**Fix**: Update to v5.0+ (#510 disables backup by default). When manually enabled, explicitly specify skip directories: ```csharp .SetOption(Option.BackupEnabled, true) -// 确保 DirectoryNames 非空,或使用默认跳过列表 +// Make sure DirectoryNames is not empty, or use the default skip list ``` --- -### C5. ZIP 解压路径穿越 — 恶意包可覆盖任意文件 +### C5. ZIP extraction path traversal — Malicious package can overwrite arbitrary files -| 来源 | 代码审计 #7 | +| Source | Code Audit #7 | |------|-----------| -| **根因** | `ZipCompressionStrategy.Decompress` 只做 `Regex.Replace` 清理,未验证 `Path.GetFullPath(combinedPath).StartsWith(Path.GetFullPath(unZipDir))` | +| **Root Cause** | `ZipCompressionStrategy.Decompress` only does `Regex.Replace` cleanup, does not verify `Path.GetFullPath(combinedPath).StartsWith(Path.GetFullPath(unZipDir))` | -**影响**:攻击者通过 `../../evil.exe` 条目逃逸到任意目录 -**修复**:更新到 v5.0+(已修复)。旧版本手动添加路径校验 +**Impact**: Attacker can escape to arbitrary directories via `../../evil.exe` entries +**Fix**: Update to v5.0+ (fixed). For older versions, manually add path validation --- -### C6. 硬编码 AES 密钥 — IPC 加密形同虚设 +### C6. Hardcoded AES key — IPC encryption is effectively useless -| 来源 | 代码审计 #1, #2, #14 | +| Source | Code Audit #1, #2, #14 | |------|---------------------| -| **根因** | AES 密钥由常量 `SHA256("GeneralUpdate.IPC.EnvironmentProvider.v1")` 派生,IV 16 字节中仅第 1 字节非零 | +| **Root Cause** | AES key derived from constant `SHA256("GeneralUpdate.IPC.EnvironmentProvider.v1")`, only the 1st byte of the 16-byte IV is non-zero | -**影响**:任何拿到反编译代码的人可解密 IPC 文件 -**修复**:使用 NamedPipe IPC(见 advanced/templates/NamedPipeIPC.cs);或部署 DPAPI 加密 +**Impact**: Anyone with decompiled code can decrypt IPC files +**Fix**: Use NamedPipe IPC (see advanced/templates/NamedPipeIPC.cs); or deploy DPAPI encryption --- -### C7. 跨租户数据泄漏(服务端) +### C7. Cross-tenant data leakage (Server-side) -| 来源 | 代码审计 #15 | +| Source | Code Audit #15 | |------|------------| -| **影响范围** | 11 处服务端漏洞:包/客户端/分组/升级记录/文件/租户隔离均缺失 | +| **Impact Scope** | 11 server-side vulnerabilities: package/client/group/upgrade record/file/tenant isolation all missing | -**修复**:升级到 GeneralSpacestation 最新版;紧急措施:为每个租户部署独立实例 +**Fix**: Upgrade to latest GeneralSpacestation; emergency measure: deploy separate instances per tenant -| 具体漏洞 | 所在文件 | 影响 | +| Specific Vulnerability | File Location | Impact | |---------|---------|------| -| GroupId 过滤条件取反 | `ClientService.cs:36-37` | 分组查询返回错误客户端 | -| UserService 可改 TenantId | `UserService.cs:338-356` | 租户间权限提升 | -| 升级记录无租户 ID | `UpgradeService.cs:242-256` | 租户隔离彻底失效 | -| 全局包可见于所有租户 | `UpgradeService.cs:49-57` | 跨租户数据暴露 | -| 文件删除无租户过滤 | `FileService.cs:98-108` | 任意文件可删 | +| GroupId filter condition inverted | `ClientService.cs:36-37` | Group query returns wrong clients | +| UserService can modify TenantId | `UserService.cs:338-356` | Cross-tenant privilege escalation | +| Upgrade records lack tenant ID | `UpgradeService.cs:242-256` | Tenant isolation completely ineffective | +| Global packages visible to all tenants | `UpgradeService.cs:49-57` | Cross-tenant data exposure | +| File deletion without tenant filtering | `FileService.cs:98-108` | Any file can be deleted | --- -### C8. PushJob 静默吞异常 — Quartz 不知作业失败 +### C8. PushJob silently swallows exceptions — Quartz unaware of job failure -| 来源 | 代码审计 #16 | +| Source | Code Audit #16 | |------|------------| -| **根因** | `PushJob.Execute` 被 `try-catch(Exception)` 包裹只 `LogError`,Quartz 不触发重试 | +| **Root Cause** | `PushJob.Execute` is wrapped in `try-catch(Exception)` which only `LogError`s, Quartz does not trigger retry | -**影响**:推送任务对运维完全不可见 -**修复**:在 catch 中 rethrow 或移除外层 catch +**Impact**: Push tasks are completely invisible to operations +**Fix**: Rethrow in the catch block or remove the outer catch --- -## 🟠 二级:高优先级 / 场景阻断 +## Level 2: High Priority / Scenario-Blocking -### H1. 静默模式不生效 +### H1. Silent mode not working -| 来源 | #484, #471, #443, #IJQ0Q5 | +| Source | #484, #471, #443, #IJQ0Q5 | |------|--------------------------| -| **根因**(多重): | | -| | ① `ProcessExit` 事件不保证触发(FailFast/TerminateProcess/Ctrl+C 下不触发) | -| | ② `manifest.json` 默认非空字段阻塞 `AppMetadataDiscoverer.Discover()` | -| | ③ 静默模式下 `PatchMiddleware` 抛出异常(未注入 DiffPipeline) | -| | ④ `manifest.json` 的 MainAppName 默认值 "GeneralUpdate.Core.exe" 在静默启动时阻塞身份发现 | +| **Root Cause** (multiple): | | +| | ① `ProcessExit` event is not guaranteed to fire (does not fire under FailFast/TerminateProcess/Ctrl+C) | +| | ② `manifest.json` default non-empty fields block `AppMetadataDiscoverer.Discover()` | +| | ③ `PatchMiddleware` throws exception in silent mode (DiffPipeline not injected) | +| | ④ `manifest.json` default MainAppName "GeneralUpdate.Core.exe" blocks identity discovery during silent startup | -**修复**: +**Fix**: ```csharp -// ① 应用关闭时显式触发(替代 ProcessExit 依赖) +// ① Trigger explicitly when app closes (replaces ProcessExit dependency) public void OnAppClosing() { _bootstrap.SilentOrchestrator?.TryLaunchUpgrade(); } -// ② manifest 字段确保填写正确的版本号 -// ③ 显式关闭差分 +// ② Ensure manifest fields have correct version numbers +// ③ Explicitly disable differential updates .SetOption(Option.PatchEnabled, false) -// ④ 如需更新后不自动启动应用(#443) -// 配置 SilentAutoRestart = false +// ④ If auto-start after update is not desired (#443) +// Configure SilentAutoRestart = false ``` --- -### H2. 无限升级循环(每次启动都检查到"新版本") +### H2. Infinite update loop ("new version" found on every launch) -| 来源 | #475, #467, 代码审计 #20 | +| Source | #475, #467, Code Audit #20 | |------|------------------------| -| **根因**(多重): | | -| | ① 场景判断与 DownloadPlan 不一致(服务端说有更新但无包可下载) | -| | ② manifest.json 未 WriteBack 版本号 | -| | ③ Version 为 null/空 → 被转为默认值 "1.0.0.0" → 永远比服务端旧 | +| **Root Cause** (multiple): | | +| | ① Scenario detection inconsistent with DownloadPlan (server says update exists but no package to download) | +| | ② manifest.json did not WriteBack version number | +| | ③ Version is null/empty → converted to default "1.0.0.0" → always older than server | -**修复**: -1. 更新到 v5.0+(已修复 WriteBack + 场景判断) -2. 旧版本在 `OnAfterUpdateAsync` hook 中手动回写版本号(见 H11) -3. 确保 `ClientVersion` 始终是有效的 4 段式版本号 +**Fix**: +1. Update to v5.0+ (WriteBack + scenario detection fixed) +2. Older versions: manually write back version number in `OnAfterUpdateAsync` hook (see H11) +3. Ensure `ClientVersion` is always a valid 4-segment version number --- -### H3. 循环:Process.Start 启动进程后未检查返回值 +### H3. Process.Start return value not checked after launch -| 来源 | 代码审计 H2 | +| Source | Code Audit H2 | |------|-----------| -| **根因** | 5 个 Strategy 文件中 `Process.Start()` 返回值未检查(null → 静默失败) | +| **Root Cause** | `Process.Start()` return value not checked in 5 Strategy files (null → silent failure) | -**修复**:更新到 v5.0+(已修复,失败时抛异常) +**Fix**: Update to v5.0+ (fixed, throws exception on failure) --- -### H4. UpdateReporter 注入不生效 / ReportUrl 未配置抛异常 +### H4. UpdateReporter injection not working / ReportUrl not configured throws exception -| 来源 | #470 | +| Source | #470 | |------|------| -| **根因** | `UpdateReporter()` 注册的实现未被消费;`ProcessInfo` 构造函数将 `ReportUrl` 作为必填 | +| **Root Cause** | Registered implementation of `UpdateReporter()` is never consumed; `ProcessInfo` constructor requires `ReportUrl` as mandatory field | -**修复**:更新到 v5.0+ 或显式设置 `ReportUrl`(即使不打算用) +**Fix**: Update to v5.0+ or explicitly set `ReportUrl` (even if not planning to use it) --- -### H5. Sync-over-async 死锁 — GetAwaiter().GetResult() +### H5. Sync-over-async deadlock — GetAwaiter().GetResult() -| 来源 | #451, 代码审计 #6 | +| Source | #451, Code Audit #6 | |------|-----------------| -| **根因** | `AppDomain.ProcessExit` 事件处理程序同步调用 `.GetAwaiter().GetResult()`,在 WPF/WinForms 的 SynchronizationContext 上死锁 | +| **Root Cause** | `AppDomain.ProcessExit` event handler synchronously calls `.GetAwaiter().GetResult()`, causing deadlock on WPF/WinForms SynchronizationContext | -**影响**:桌面应用使用静默模式时,进程退出可能挂起 -**修复**:更新到 v5.0+(已修复,改为 `ConfigureAwait(false)` + Task.Run) +**Impact**: Desktop apps using silent mode may hang on process exit +**Fix**: Update to v5.0+ (fixed, changed to `ConfigureAwait(false)` + Task.Run) --- -### H6. 前钩子 (SafeOnBeforeUpdateAsync) 异常时返回 true(应返回 false) +### H6. Before-hook (SafeOnBeforeUpdateAsync) returns true on exception (should return false) -| 来源 | 代码审计 H4 | +| Source | Code Audit H4 | |------|-----------| -| **根因** | `ClientStrategy.cs:1015-1026` 异常时返回 `true`(放行更新),应返回 `false`(中止) | +| **Root Cause** | `ClientStrategy.cs:1015-1026` returns `true` on exception (proceeds with update), should return `false` (abort) | -**影响**:Hooks 中的 `OnBeforeUpdateAsync` 即使抛异常也会继续更新 -**修复**:更新到 v5.0+ +**Impact**: `OnBeforeUpdateAsync` in Hooks continues even if it throws an exception +**Fix**: Update to v5.0+ --- -### H7. Scenario = Both 误判 — DownloadPlan 为空但判断为有更新 +### H7. Scenario = Both misjudgment — DownloadPlan is empty but judged as having update -| 来源 | #465, #475 | +| Source | #465, #475 | |------|-----------| -| **根因** | `HttpDownloadSource.ListAsync()` 只检查 `Body.Count > 0`,未验证 `AppType` 匹配;`DownloadPlanBuilder.Build()` 版本过滤后可能返回空列表 | +| **Root Cause** | `HttpDownloadSource.ListAsync()` only checks `Body.Count > 0`, does not verify `AppType` match; `DownloadPlanBuilder.Build()` may return empty list after version filtering | -**修复**:更新到 v5.0+(已修复场景判断逻辑) +**Fix**: Update to v5.0+ (scenario detection logic fixed) --- -### H8. OSS 模式:下载完成但没有更新 +### H8. OSS mode: Download completed but no update applied -| 来源 | #485, #487 | +| Source | #485, #487 | |------|-----------| -| **根因** | ① OSS 不区分 Main/Upgrade 更新(HasMainUpdate 和 HasUpgradeUpdate 总是相同)② SSL 验证不覆盖文件下载 | +| **Root Cause** | ① OSS does not distinguish Main/Upgrade updates (HasMainUpdate and HasUpgradeUpdate are always the same) ② SSL validation does not cover file downloads | -**修复**: +**Fix**: ```csharp -// OSS 模式推荐配置 +// Recommended OSS mode configuration .SetOption(Option.PatchEnabled, false) -// 自定义 SSL 策略确保覆盖下载请求 +// Custom SSL policy to ensure download requests are covered bootstrap.SslValidationPolicy(); ``` --- -### H9. PatchMiddleware 在静默模式必定抛异常 +### H9. PatchMiddleware always throws in silent mode -| 来源 | #471 | +| Source | #471 | |------|------| -| **根因** | `SilentPollOrchestrator.CreateStrategy()` 创建裸 `WindowsStrategy`,未注入 `DiffPipeline` | +| **Root Cause** | `SilentPollOrchestrator.CreateStrategy()` creates a bare `WindowsStrategy` without injecting `DiffPipeline` | -**修复**: +**Fix**: ```csharp -.SetOption(Option.PatchEnabled, false) // 静默模式下关闭差分 +.SetOption(Option.PatchEnabled, false) // Disable differential updates in silent mode ``` --- -### H10. HttpClient 无限超时 +### H10. HttpClient infinite timeout -| 来源 | 代码审计 M2 | +| Source | Code Audit M2 | |------|-----------| -| **根因** | `HttpClientProvider.Shared` 设置 `Timeout = InfiniteTimeSpan` | +| **Root Cause** | `HttpClientProvider.Shared` sets `Timeout = InfiniteTimeSpan` | -**修复**:更新到 v5.0+(已设置为 5 分钟安全上网限) +**Fix**: Update to v5.0+ (set to 5-minute safe timeout limit) --- -### H11. 更新成功后版本号未 WriteBack +### H11. Version number not written back after successful update -| 来源 | #467, #475 | +| Source | #467, #475 | |------|-----------| -| **根因** | manifest.json 未更新,下次启动时版本号还是旧的 | +| **Root Cause** | manifest.json is not updated, version number is still the old one on next startup | -**修复**:更新到 v5.0+(已实现 WriteBack)。旧版本手动处理: +**Fix**: Update to v5.0+ (WriteBack implemented). For older versions, handle manually: ```csharp -// 在 hooks 中手动回写 +// Manually write back in hooks public async Task OnAfterUpdateAsync(UpdateContext context) { var manifestPath = Path.Combine(context.InstallPath, "generalupdate.manifest.json"); @@ -287,366 +287,365 @@ public async Task OnAfterUpdateAsync(UpdateContext context) --- -## 🟡 三级:中等 / 需要关注 +## Level 3: Medium / Needs Attention -### M1. 增量更新报错:patch 应用失败 +### M1. Incremental update error: patch application failed -| 来源 | #II75WI, #I8T0QX | +| Source | #II75WI, #I8T0QX | |------|-----------------| -| **根因** | ① 旧 patch 临时文件残留 ② 文件已被修改导致 hash 不匹配 | +| **Root Cause** | ① Old patch temp files remain ② Files have been modified causing hash mismatch | -**修复**: +**Fix**: ```csharp .SetOption(Option.AutoCleanTemp, true) -// 手动清理: +// Manual cleanup: if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); ``` -### M2. 同名文件在不同目录时封包出错 +### M2. Same-named files in different directories cause packaging errors -| 来源 | #II77NS | +| Source | #II77NS | |------|---------| -| **根因** | `DefaultCleanMatcher.Match` 未使用相对路径匹配 | +| **Root Cause** | `DefaultCleanMatcher.Match` does not use relative path matching | -**修复**:使用自定义 CleanMatcher: +**Fix**: Use custom CleanMatcher: ```csharp new DiffPipelineBuilder() .UseCleanMatcher(new CustomRelativePathCleanMatcher()) .Build(); ``` -### M3. 多级文件夹结构更新后文件位置错乱 +### M3. Multi-level folder structure causes file location errors after update -| 来源 | #I59QRI | +| Source | #I59QRI | |------|---------| -| **根因** | 子目录文件被错误更新到根目录 | +| **Root Cause** | Subdirectory files incorrectly updated to root directory | -**修复**:更新到最新版;确保差分包路径包含完整相对路径 +**Fix**: Update to latest version; ensure differential package path includes full relative path -### M4. 中文文件名乱码 +### M4. Chinese filename garbled -| 来源 | #I502QQ | +| Source | #I502QQ | |------|---------| -| **根因** | ZIP 解压未指定编码 | +| **Root Cause** | ZIP extraction did not specify encoding | -**修复**: +**Fix**: ```csharp .SetOption(Option.Encoding, CompressionEncoding.UTF8) ``` -### M5. 版本号出现异常字符 +### M5. Abnormal characters in version number -| 来源 | #I8TNPE | +| Source | #I8TNPE | |------|---------| -| **根因** | 版本链计算缺陷,中间版本号被污染 | +| **Root Cause** | Version chain calculation defect, intermediate version numbers polluted | -**修复**:更新到 v5.0+(已修复) +**Fix**: Update to v5.0+ (fixed) -### M6. 文件被占用 / "file in use" +### M6. File in use / "file in use" -| 来源 | #479, #ID3UDN | +| Source | #479, #ID3UDN | |------|-------------| -| **根因** | 进程退出后文件句柄未完全释放 | +| **Root Cause** | File handles not fully released after process exits | -**修复**: +**Fix**: ```csharp -// 增加等待时间,或重试逻辑 +// Increase wait time, or add retry logic .SetOption(Option.DownloadTimeout, 120) ``` -### M7. Linux 下 Environment.GetEnvironmentVariable("ProcessInfo") 为空 +### M7. Environment.GetEnvironmentVariable("ProcessInfo") is null on Linux -| 来源 | #ID4ZF5 | +| Source | #ID4ZF5 | |------|---------| -| **根因** | Linux 环境变量作用域问题 | +| **Root Cause** | Linux environment variable scope issue | -**修复**:使用最新版(已改用加密文件 IPC)或 NamedPipe IPC +**Fix**: Use latest version (now uses encrypted file IPC) or NamedPipe IPC -### M8. Linux / macOS 更新后文件无执行权限 +### M8. Files lack execute permissions after update on Linux/macOS -| 来源 | #ID5049 | +| Source | #ID5049 | |------|---------| -| **根因** | 新文件缺少 Unix 可执行权限 | +| **Root Cause** | New files missing Unix executable permissions | -**修复**: +**Fix**: ```csharp bootstrap.Hooks(); ``` -### M9. IPC 加密文件被防病毒软件隔离 +### M9. IPC encrypted file quarantined by antivirus software -| 来源 | 代码审计 #1, #2 | +| Source | Code Audit #1, #2 | |------|----------------| -| **根因** | IPC 路径固定为 `%TEMP%/GeneralUpdate/ipc/process_info.enc` | +| **Root Cause** | IPC path is fixed at `%TEMP%/GeneralUpdate/ipc/process_info.enc` | -**修复**:使用 NamedPipe IPC 替代 +**Fix**: Use NamedPipe IPC instead -### M10. 版本比较错误:"1.0" 与 "1.0.0.0" 不等 +### M10. Version comparison error: "1.0" != "1.0.0.0" -| 来源 | #475, 服务端 #26 | +| Source | #475, Server #26 | |------|----------------| -| **根因** | `System.Version` 将 "1.0" 解析为 `1.0.-1.-1`,`< "1.0.0.0"` | +| **Root Cause** | `System.Version` parses "1.0" as `1.0.-1.-1`, `< "1.0.0.0"` | -**修复**:服务端和客户端版本号统一为 4 段式 +**Fix**: Server and client version numbers should be unified to 4-segment format -### M11. Assembly.GetExecutingAssembly 获取版本号不正确 +### M11. Assembly.GetExecutingAssembly returns incorrect version -| 来源 | #I5O4KV | +| Source | #I5O4KV | |------|---------| -| **根因** | 应使用 `Assembly.GetEntryAssembly()`,而非 `GetExecutingAssembly()` | +| **Root Cause** | Should use `Assembly.GetEntryAssembly()`, not `GetExecutingAssembly()` | -**修复**:在 manifest.json 中显式填写 `ClientVersion` +**Fix**: Explicitly fill `ClientVersion` in manifest.json -### M12. SignalR 推送后无反应(ObjectDisposedException) +### M12. SignalR push has no response (ObjectDisposedException) -| 来源 | #402, 代码审计 #5 | +| Source | #402, Code Audit #5 | |------|-----------------| -| **根因** | `UpgradeHubService.DisposeAsync` 不置 null,重连时崩溃 | +| **Root Cause** | `UpgradeHubService.DisposeAsync` does not set null, crashes on reconnect | -**修复**:使用 `SafeHubConnection` 包装类(见 PushStrategy.cs) +**Fix**: Use `SafeHubConnection` wrapper class (see PushStrategy.cs) -### M13. Bowl 没有生成 dump 文件 +### M13. Bowl does not generate dump files -| 来源 | #492 | +| Source | #492 | |------|------| -| **根因** | Bowl IPC 文件每次读取后自动删除,多进程竞争 | +| **Root Cause** | Bowl IPC file auto-deleted after each read, multi-process race condition | -**修复**:更新到 v5.0+(已修复 Bowl IPC 架构);手动下载 procdump +**Fix**: Update to v5.0+ (Bowl IPC architecture fixed); manually download procdump -### M14. 默认备份保留最多 3 个版本 +### M14. Default backup retains at most 3 versions -| 来源 | 默认行为 | +| Source | Default behavior | |------|---------| -| **根因** | `StorageManager.CleanBackup` 只保留最近 3 个备份 | +| **Root Cause** | `StorageManager.CleanBackup` only keeps the most recent 3 backups | -**修复**:如需更多保留,自定义 BackupConfig: +**Fix**: To retain more, customize BackupConfig: ```csharp .SetOption(Option.BackupConfig, new BackupConfig { KeepVersions = 10 }) ``` -### M15. DefaultCleanMatcher 每次调用创建新 StorageManager 实例(并发不安全) +### M15. DefaultCleanMatcher creates new StorageManager instance on each call (not thread-safe) -| 来源 | 代码审计 #17 | +| Source | Code Audit #17 | |------|------------| -| **根因** | 实例级别持有 `_fileCount` 和 `ComparisonResult`,但被并行调用 | +| **Root Cause** | Instance-level fields `_fileCount` and `ComparisonResult` are shared across parallel calls | -**修复**:更新到 v5.0+ 或在 `DiffPipeline.CleanAsync` 中添加锁 +**Fix**: Update to v5.0+ or add locking in `DiffPipeline.CleanAsync` -### M16. HttpDownloadExecutor 不校验 Content-Length +### M16. HttpDownloadExecutor does not validate Content-Length -| 来源 | 代码审计 #22 | +| Source | Code Audit #22 | |------|------------| -| **根因** | `StreamDownloadAsync` 不验证下载字节数 | +| **Root Cause** | `StreamDownloadAsync` does not verify downloaded byte count | -**修复**:更新到 v5.0+(已添加校验) +**Fix**: Update to v5.0+ (validation added) -### M17. OssStrategy.StartAppAsync 返回 Task.CompletedTask +### M17. OssStrategy.StartAppAsync returns Task.CompletedTask -| 来源 | 代码审计 #30 | +| Source | Code Audit #30 | |------|------------| -| **根因** | `appName` 为空时直接返回,调用方无法区分"已启动"和"跳过" | +| **Root Cause** | Returns directly when `appName` is empty, caller cannot distinguish "started" from "skipped" | -**修复**:显式检查空值并抛异常 +**Fix**: Explicitly check for null and throw exception -### M18. EventManager 单例 — Dispose 后仍可访问 +### M18. EventManager singleton — accessible after Dispose -| 来源 | 代码审计 #11 | +| Source | Code Audit #11 | |------|------------| -| **根因** | `Lazy` 单例,Dispose 后 `_lazy.Value` 返回已释放实例 | +| **Root Cause** | `Lazy` singleton, `_lazy.Value` returns disposed instance after Dispose | -**修复**:自行管理生命周期,在 Bootstrap 结束时调用 Clear +**Fix**: Manage lifecycle manually, call Clear at the end of Bootstrap -### M19. GeneralTracer.Dispose 清空全局 Trace.Listeners +### M19. GeneralTracer.Dispose clears global Trace.Listeners -| 来源 | 代码审计 #13 | +| Source | Code Audit #13 | |------|------------| -| **根因** | `Dispose()` 调用 `Trace.Listeners.Clear()`,影响同一进程其他库的日志输出 | +| **Root Cause** | `Dispose()` calls `Trace.Listeners.Clear()`, affecting log output of other libraries in the same process | -**修复**:更新到 v5.0+(已改为只移除自己的 Listener) +**Fix**: Update to v5.0+ (changed to only remove its own Listener) -### M20. GeneralTracer 日志只按天轮转,永不过期 +### M20. GeneralTracer logs rotate daily but never expire -| 来源 | 代码审计 #28 | +| Source | Code Audit #28 | |------|------------| -| **根因** | `generalupdate-trace {yyyy-MM-dd}.log` 永不过期 | +| **Root Cause** | `generalupdate-trace {yyyy-MM-dd}.log` never expires | -**修复**:手动配置日志保留策略,或定期清理 `Logs/` 目录 +**Fix**: Manually configure log retention policy, or periodically clean the `Logs/` directory --- -## 🔵 四级:低优先 / 代码气味 / 已知行为 +## Level 4: Low Priority / Code Smell / Known Behavior -### L1. DefaultRetryPolicy 用字符串包含判断 HTTP 状态码 +### L1. DefaultRetryPolicy uses string containment to check HTTP status codes -| 来源 | 代码审计 #10 | +| Source | Code Audit #10 | |------|------------| -| **根因** | `s.Contains("500")` 可能误匹配 URL 或响应正文中的 "500" | -| **建议** | 使用 `HttpRequestException.StatusCode` 属性 | +| **Root Cause** | `s.Contains("500")` may incorrectly match "500" in URLs or response body | +| **Suggestion** | Use `HttpRequestException.StatusCode` property | -### L2. OssDownloadSource 不区分 Main/Upgrade +### L2. OssDownloadSource does not distinguish Main/Upgrade -| 来源 | 代码审计 #27 | +| Source | Code Audit #27 | |------|------------| -| **根因** | 将 `HasMainUpdate` 和 `HasUpgradeUpdate` 都设为 `assets.Count > 0` | -| **建议** | OSS 模式接受此行为,或自行实现 IDownloadSource | +| **Root Cause** | Sets both `HasMainUpdate` and `HasUpgradeUpdate` to `assets.Count > 0` | +| **Suggestion** | Accept this behavior in OSS mode, or implement IDownloadSource yourself | -### L3. ProcessContract 构造函数空检查顺序错误 +### L3. ProcessContract constructor null-check order is wrong -| 来源 | 代码审计 #9 | +| Source | Code Audit #9 | |------|------------| -| **根因** | 先检查 `Directory.Exists(installPath)`,然后才 `?? throw` | -| **建议** | 小问题,不影响功能 | +| **Root Cause** | Checks `Directory.Exists(installPath)` first, then `?? throw` | +| **Suggestion** | Minor issue, does not affect functionality | -### L4. ConfigurationMapper.MapToUpdateContext 静默接受 null +### L4. ConfigurationMapper.MapToUpdateContext silently accepts null -| 来源 | 代码审计 #20 | +| Source | Code Audit #20 | |------|------------| -| **根因** | `source == null` 返回空的 `UpdateContext` | -| **建议** | 检查配置是否正确加载 | +| **Root Cause** | `source == null` returns an empty `UpdateContext` | +| **Suggestion** | Check whether configuration is loaded correctly | -### L5. StorageManager 跳过目录使用 string.Contains 匹配 +### L5. StorageManager uses string.Contains for directory skipping -| 来源 | 代码审计 #21 | +| Source | Code Audit #21 | |------|------------| -| **根因** | `dirName.Contains("backup-")`,目录名 `backup-custom` 因包含 "backup-" 也被跳过 | -| **建议** | 影响小,如需精确控制使用自定义跳过策略 | +| **Root Cause** | `dirName.Contains("backup-")`, so directory `backup-custom` is also skipped because it contains "backup-" | +| **Suggestion** | Low impact; use custom skip strategy for precise control | -### L6. FileTreeComparer FAT32 时间精度 2 秒漏判 +### L6. FileTreeComparer FAT32 2-second timestamp precision misses changes -| 来源 | 代码审计 #18 | +| Source | Code Audit #18 | |------|------------| -| **根因** | FAT32 文件系统时间戳精度 2 秒 | -| **建议** | 对 FAT32 卷添加哈希比对兜底 | +| **Root Cause** | FAT32 filesystem timestamp precision is 2 seconds | +| **Suggestion** | Add hash comparison fallback for FAT32 volumes | -### L7. DiffPipeline.CopyUnknownFiles 用 Replace 截取相对路径 +### L7. DiffPipeline.CopyUnknownFiles uses Replace to extract relative path -| 来源 | 代码审计 #31 | +| Source | Code Audit #31 | |------|------------| -| **根因** | `file.FullName.Replace(targetPath, "")` 当 targetPath 出现在路径中间时出错 | -| **建议** | 使用 `StartsWith + Substring` | +| **Root Cause** | `file.FullName.Replace(targetPath, "")` fails when targetPath appears in the middle of a path | +| **Suggestion** | Use `StartsWith + Substring` | -### L8. StreamingHdiffDiffer 文件超限时截断 +### L8. StreamingHdiffDiffer truncates files that exceed size limit -| 来源 | 代码审计 #32 | +| Source | Code Audit #32 | |------|------------| -| **根因** | 超过 `MaxWindowSize` (默认 128MB) 时截断读取前 128MB | -| **建议** | 大文件使用全量更新替代差分 | +| **Root Cause** | When exceeding `MaxWindowSize` (default 128MB), truncates to read only first 128MB | +| **Suggestion** | Use full update instead of differential for large files | -### L9. Bowl StorageHelper.Restore 无条件执行 +### L9. Bowl StorageHelper.Restore executes unconditionally -| 来源 | 代码审计 #33 | +| Source | Code Audit #33 | |------|------------| -| **根因** | `AutoRestore=true` 时无验证恢复结果 | -| **建议** | 回滚后增加校验 | +| **Root Cause** | When `AutoRestore=true`, no validation of restore result | +| **Suggestion** | Add verification after rollback | -### L10. OssStrategy 版本比较可能抛异常 +### L10. OssStrategy version comparison may throw exception -| 来源 | 代码审计 #23 | +| Source | Code Audit #23 | |------|------------| -| **根因** | `new Version("")` 抛 ArgumentException | -| **建议** | 使用 `ParseVersion` 安全解析 | +| **Root Cause** | `new Version("")` throws ArgumentException | +| **Suggestion** | Use `ParseVersion` for safe parsing | -### L11. 静默模式更新完自动启动应用 +### L11. Silent mode auto-starts app after update -| 来源 | #IJQ0Q5 | +| Source | #IJQ0Q5 | |------|---------| -| **建议** | 通过 `SilentAutoRestart` 选项控制 | +| **Suggestion** | Control via the `SilentAutoRestart` option | -### L12. OSS 模式下传的 ZIP 包编码无法解压 +### L12. ZIP package encoding in OSS mode cannot be extracted -| 来源 | #I59Q5W, #I502QQ | +| Source | #I59Q5W, #I502QQ | |------|----------------| -| **建议** | 构建 ZIP 时指定 UTF-8,上传前验证解压 | +| **Suggestion** | Specify UTF-8 when building ZIP, verify extraction before uploading | --- -## 📋 通用诊断流程 +## General Diagnostic Workflow -当用户报告的问题未在以上清单中找到时,执行系统性诊断: +When the user's reported issue is not found in the above checklist, perform a systematic diagnosis: -### 步骤 1:版本检查 +### Step 1: Version Check ``` -□ Client 和 Upgrade 使用相同 NuGet 版本号? -□ 使用最新稳定版(v5.0+ 推荐)? +□ Client and Upgrade use the same NuGet version? +□ Using the latest stable version (v5.0+ recommended)? ``` -### 步骤 2:配置文件检查 +### Step 2: Configuration File Check ``` -□ generalupdate.manifest.json 是否存在? -□ 格式是否正确(JSON 语法校验)? -□ ClientVersion 已填写(非空字符串)? -□ MainAppName 包含 .exe 扩展名? -□ UpdateAppName 指向存在的文件? -□ InstallPath 路径可访问? +□ Does generalupdate.manifest.json exist? +□ Is the format correct (valid JSON)? +□ Is ClientVersion filled in (non-empty string)? +□ Does MainAppName include .exe extension? +□ Does UpdateAppName point to an existing file? +□ Is InstallPath accessible? ``` -### 步骤 3:双进程检查 +### Step 3: Dual-Process Check ``` -□ UpgradeApp.exe 存在于发布目录? -□ Client 和 Upgrade 使用相同 AppSecretKey? -□ %TEMP%/GeneralUpdate/ipc/ 目录可写入? -□ 防病毒软件未隔离该目录? +□ Does UpgradeApp.exe exist in the deployment directory? +□ Do Client and Upgrade use the same AppSecretKey? +□ Is %TEMP%/GeneralUpdate/ipc/ directory writable? +□ Has antivirus software not quarantined this directory? ``` -### 步骤 4:策略配置检查 +### Step 4: Strategy Configuration Check ``` -标准模式: - □ UpdateUrl 可访问(HTTP 200)? - □ /Upgrade/Verification 接口返回正确格式? - □ AppSecretKey 与服务端一致? - -OSS 模式: - □ versions.json URL 可下载? - □ versions.json 格式正确? - □ 版本号比较正常? - -静默模式: - □ ProcessExit 能触发(非 FailFast 场景)? - □ 应用关闭时显式调用了 TryLaunchUpgrade()? - □ manifest 字段全部正确填写? +Standard Mode: + □ Is UpdateUrl accessible (HTTP 200)? + □ Does /Upgrade/Verification endpoint return correct format? + □ Is AppSecretKey consistent with the server? + +OSS Mode: + □ Can versions.json URL be downloaded? + □ Is versions.json format correct? + □ Are version comparisons working correctly? + +Silent Mode: + □ Can ProcessExit fire (non-FailFast scenario)? + □ Is TryLaunchUpgrade() explicitly called when app closes? + □ Are all manifest fields correctly filled in? ``` -### 步骤 5:日志检查 +### Step 5: Log Check ``` -□ 查看 generalupdate-trace {yyyy-MM-dd}.log(位于 {BaseDir}/Logs/) -□ EventManager 是否触发了 Exception 事件? -□ AddListenerException 是否收到异常? +□ Check generalupdate-trace {yyyy-MM-dd}.log (located at {BaseDir}/Logs/) +□ Did EventManager fire the Exception event? +□ Did AddListenerException receive an exception? ``` -### 步骤 6:平台特定检查 +### Step 6: Platform-Specific Check ``` Windows: - □ 防病毒软件是否拦截 IPC 文件或临时目录? - □ 管理员权限是否必要? + □ Is antivirus software blocking IPC files or temp directories? + □ Is administrator privilege required? Linux/macOS: - □ 文件可执行权限是否设置? - □ 环境变量作用域是否正确? - □ Mono 或 .NET 运行时版本兼容? + □ Are file executable permissions set? + □ Is Mono or .NET runtime version compatible? AOT: - □ SignalR 使用 JSON 协议 + JsonSerializerContext? - □ 反射调用被 preserve? + □ Does SignalR use JSON protocol + JsonSerializerContext? + □ Are reflection calls preserved? ``` --- -## 🛠 快速诊断命令 +## Quick Diagnostic Commands ```bash -# 1. 检查 manifest 文件 +# 1. Check manifest file cat generalupdate.manifest.json | python3 -m json.tool -# 2. 检查升级程序是否存在 +# 2. Check if upgrade program exists ls -la update/UpgradeApp.exe -# 3. 检查 IPC 文件 +# 3. Check IPC files ls -la /tmp/GeneralUpdate/ipc/ # 或 %TEMP%/GeneralUpdate/ipc/ -# 4. 检查更新日志 +# 4. Check update logs cat Logs/generalupdate-trace\ *.log | tail -100 -# 5. 验证服务端 API +# 5. Verify server API curl -X POST https://your-server.com/Upgrade/Verification \ -H "Content-Type: application/json" \ -d '{"appKey":"test","appType":0,"clientVersion":"1.0.0.0","productId":"test"}' @@ -654,9 +653,9 @@ curl -X POST https://your-server.com/Upgrade/Verification \ --- -## Issue 索引(快速跳转) +## Issue Index (Quick Navigation) -| 范围 | 内容 | GitHub | Gitee | +| Scope | Content | GitHub | Gitee | |------|------|--------|-------| | v5 重构 | 策略/配置/Bootstrap 重写 | #308–#361 | — | | 扩展点修复 | 扩展点注入不消费 | #455, #457, #373 | — | diff --git a/.claude/skills/generalupdate-ui/SKILL.md b/.claude/skills/generalupdate-ui/SKILL.md index 331bc6f..8fed648 100644 --- a/.claude/skills/generalupdate-ui/SKILL.md +++ b/.claude/skills/generalupdate-ui/SKILL.md @@ -8,7 +8,7 @@ description: | error/retry, paused, completed, upgrade-in-progress, already-latest, forced-update, rollback. Generates IDownloadService bridge to replace MockDownloadService. Triggers on: "update UI", "progress bar", "update window", "show progress", - "update界面", "进度显示", "更新窗口", "好看点", "UI样式", + "update UI", "show progress", "update window", "beautiful UI", "UI style", "how to show update progress", "need a progress UI", "update form", "beautiful update UI", "professional update appearance". ALWAYS load this skill when the user asks for auto-update + UI together. @@ -25,189 +25,189 @@ when_to_use: | allowed-tools: "Read, Write, Edit, Glob, Grep" --- -# 🎨 GeneralUpdate 更新界面生成 — 全状态覆盖 +# GeneralUpdate Update UI Generation — Full State Coverage -自动检测开发者的 UI 框架类型,生成带真实 GeneralUpdate.Core 事件绑定的完整更新窗口代码。 -覆盖所有 UI 状态、错误处理、动画和 MVVM 绑定。 +Automatically detects the developer's UI framework type and generates a complete update window with real GeneralUpdate.Core event bindings. +Covers all UI states, error handling, animations, and MVVM bindings. --- -## UI 状态机(所有模板覆盖以下状态) +## UI State Machine (all templates cover the following states) ``` ┌─────────────┐ - │ Idle │ ← 初始状态 + │ Idle │ ← Initial state └──────┬──────┘ - │ 自动/手动触发 + │ Auto/manual trigger ▼ ┌─────────────┐ - ┌─────│ Checking │ ← "正在检查更新..." indeterminate 动画 + ┌─────│ Checking │ ← "Checking for updates..." indeterminate animation │ └──────┬──────┘ │ │ │ ┌──────┴──────┐ │ ▼ ▼ │ ┌────────┐ ┌──────────┐ - │ │ Latest │ │ Found! │ ← 显示版本号/大小/更新说明 - │ │(已最新)│ └────┬─────┘ - │ └────────┘ │ 用户点击"开始更新" + │ │ Latest │ │ Found! │ ← Shows version/size/release notes + │ │(Latest)│ └────┬─────┘ + │ └────────┘ │ User clicks "Start Update" │ ▼ │ ┌──────────────┐ - │ ┌─────│ Downloading │ ← 进度条/速度/剩余时间/动画 + │ ┌─────│ Downloading │ ← Progress bar/speed/remaining time/animation │ │ └──────┬───────┘ │ │ │ │ │ ┌──────┴──────┐ │ │ ▼ ▼ │ │ ┌────────┐ ┌──────────┐ - │ │ │ Paused │ │ Error │ ← 显示错误信息 + "重试"按钮 + │ │ │ Paused │ │ Error │ ← Shows error message + "Retry" button │ │ └───┬────┘ └────┬─────┘ - │ │ │ 继续 │ 重试 + │ │ │ Resume │ Retry │ │ ▼ ▼ │ │ ┌──────────────┐ - │ │ │ Downloading │ ← 回到下载状态 + │ │ │ Downloading │ ← Back to download state │ │ └──────────────┘ │ │ │ │ ┌──────────────┐ - │ └────→│ Applying │ ← "正在安装更新..." (Upgrade 进程) + │ └────→│ Applying │ ← "Installing update..." (Upgrade process) │ └──────┬───────┘ │ │ │ ┌──────┴──────┐ │ ▼ ▼ │ ┌─────────┐ ┌──────────┐ - │ │ Success │ │ Failed │ ← 显示失败原因 + 回滚提示 + │ │ Success │ │ Failed │ ← Shows failure reason + rollback hint │ └────┬────┘ └────┬─────┘ │ │ │ │ ▼ ▼ │ ┌──────────┐ ┌──────────┐ - │ │ Restart │ │ Rollback │ ← "正在回滚到上一个版本" - │ │(重启应用) │ └──────────┘ + │ │ Restart │ │ Rollback │ ← "Rolling back to previous version" + │ │(Restart app)│ └──────────┘ │ └──────────┘ │ - └── 回到 Idle(无需更新时) + └── Back to Idle (when no update needed) ``` --- -## 工作流程 +## Workflow ``` -1. 框架探测 - ├── 扫描 .csproj → PackageReference 识别 UI 库 +1. Framework Detection + ├── Scan .csproj → PackageReference to identify UI library │ ├── Semi.Avalonia / Ursa → Avalonia + SemiUrsa │ ├── LayUI.Wpf → WPF + LayUI │ ├── WPFDevelopers → WPF + WPFDevelopers │ ├── AntdUI → WinForms + AntdUI │ ├── Microsoft.Maui → MAUI - │ └── 无 → 探测 .xaml / .Designer.cs → 原生 WPF/WinForms - ├── 如果无法识别 → 询问用户使用的框架 - └── 如果无 UI 框架 → 控制台进度条 - -2. 状态代码生成 - ├── IDownloadService 增强版接口(覆盖所有状态) - ├── RealDownloadService 桥接代码(GeneralUpdate.Core → IDownloadService) - ├── ViewModel(MVVM)或 Code-Behind - └── 窗口/页面 XAML(各框架特有) - -3. 集成指导 - ├── 如何替换 MockDownloadService → RealDownloadService - ├── DI 注册(或直接实例化) - └── Bootstrap 配置(与 generalupdate-init 配合) + │ └── None → Detect .xaml / .Designer.cs → Native WPF/WinForms + ├── If unrecognized → Ask user which framework they use + └── If no UI framework → Console progress bar + +2. Status Code Generation + ├── IDownloadService enhanced interface (covers all states) + ├── RealDownloadService bridge code (GeneralUpdate.Core → IDownloadService) + ├── ViewModel (MVVM) or Code-Behind + └── Window/Page XAML (framework-specific) + +3. Integration Guide + ├── How to replace MockDownloadService → RealDownloadService + ├── DI Registration (or direct instantiation) + └── Bootstrap configuration (works with generalupdate-init) ``` --- -## 核心桥接:RealDownloadService +## Core Bridge: RealDownloadService -所有 UI 模板共享这个桥接类,将 GeneralUpdate.Core 的全部事件映射到 `IDownloadService` 接口。 +All UI templates share this bridge class, mapping all GeneralUpdate.Core events to the `IDownloadService` interface. -### 增强版 IDownloadService 接口(覆盖所有状态) +### Enhanced IDownloadService Interface (Full State Coverage) ```csharp public enum DownloadStatus { - Idle, // 初始状态,暂无操作 - Checking, // 正在检查服务器版本 - FoundUpdate, // 已发现新版本(等待用户确认) - AlreadyLatest, // 已是最新版本 - Downloading, // 正在下载更新包 - Paused, // 下载已暂停 - DownloadError, // 下载出错,可重试 - Applying, // 正在应用更新(解压/补丁) - UpgradeProgress, // Upgrade 进程正在执行 - Success, // 更新成功,等待重启 - Failed, // 更新失败,可能需要回滚 - RollingBack // 正在回滚到上一个版本 + Idle, // Initial state, no operation + Checking, // Checking server for updates + FoundUpdate, // New version found (waiting for user confirmation) + AlreadyLatest, // Already on the latest version + Downloading, // Downloading update package + Paused, // Download paused + DownloadError, // Download error, can retry + Applying, // Applying update (extracting/patching) + UpgradeProgress, // Upgrade process is executing + Success, // Update successful, waiting for restart + Failed, // Update failed, may need rollback + RollingBack // Rolling back to previous version } public interface IDownloadService { - // === 事件 === - event Action? StatisticsChanged; // 任何状态/统计变化 - event Action? StatusChanged; // 状态变更 - event Action? ErrorOccurred; // 错误信息 - event Action? UpdateCompleted; // 更新完成 + // === Events === + event Action? StatisticsChanged; // Any state/statistics change + event Action? StatusChanged; // Status change + event Action? ErrorOccurred; // Error message + event Action? UpdateCompleted; // Update completed - // === 属性 === + // === Properties === DownloadStatistics CurrentStatistics { get; } DownloadStatus Status { get; } bool CanStart { get; } bool CanPause { get; } bool CanRetry { get; } - // === 方法 === - void CheckForUpdates(); // 检查更新 - void StartDownload(); // 开始下载 - void Pause(); // 暂停 - void Retry(); // 重试(从当前状态恢复) - void Cancel(); // 取消 - void Restart(); // 完全重新开始 + // === Methods === + void CheckForUpdates(); // Check for updates + void StartDownload(); // Start download + void Pause(); // Pause + void Retry(); // Retry (resume from current state) + void Cancel(); // Cancel + void Restart(); // Restart completely } ``` -### RealDownloadService 桥接逻辑 +### RealDownloadService Bridge Logic ```csharp -// 映射 GeneralUpdate.Core 事件到 DownloadStatus 状态机: +// Maps GeneralUpdate.Core events to DownloadStatus state machine: -Bootstrap 事件 → 状态转换 +Bootstrap Event → State Transition ────────────────────────────────────────────────── -LaunchAsync 开始 → Checking -UpdateInfo 收到 → FoundUpdate / AlreadyLatest -MultiDownloadStatistics 收到 → Downloading -MultiDownloadError 收到 → DownloadError (自动重试N次后) -MultiDownloadCompleted 收到 → Applying -MultiAllDownloadCompleted 收到 → UpgradeProgress → Success -Exception 收到 → Failed +LaunchAsync starts → Checking +UpdateInfo received → FoundUpdate / AlreadyLatest +MultiDownloadStatistics received→ Downloading +MultiDownloadError received → DownloadError (after N auto-retries) +MultiDownloadCompleted received → Applying +MultiAllDownloadCompleted recv. → UpgradeProgress → Success +Exception received → Failed ``` --- -## UI 框架模板清单 +## UI Framework Template List -| 模板文件 | 适用框架 | 包含特性 | +| Template File | Framework | Features Included | |---------|---------|---------| -| `SemiUrsaClientView.axaml` + `.cs` | Avalonia + SemiUrsa | 全状态机、暗黑切换、通知、进度条动画 | -| `SemiUrsaUpgradeView.axaml` + `.cs` | Avalonia + SemiUrsa (Upgrade) | 等待中 UI、indeterminate 进度、过渡动画 | -| `LayUIStyle.xaml` + `.cs` | WPF + LayUI.Wpf | 玻璃效果、弹窗对话框、进度条 | -| `WPFDevelopersStyle.xaml` + `.cs` | WPF + WPFDevelopers | 圆形进度、呼吸灯动画、通知图标 | -| `AntdUIStyle.cs` | WinForms + AntdUI | 暗黑主题、本地化、波浪进度按钮、取消 | -| `NativeWpfWindow.xaml` + `.cs` | 原生 WPF(无皮肤) | 简洁窗口、进度条、状态文本 | -| `NativeWinForms.cs` | 原生 WinForms | 简单表单、进度条、取消 | -| `MauiUpdatePage.xaml` + `.cs` | MAUI | 跨平台、深色模式、AppThemeBinding | -| `ConsoleProgress.cs` | 控制台应用 | ANSI 进度条、状态文本 | -| `DownloadViewModels.cs` | 所有框架共用 | 完整 ViewModel + DownloadStatistics | -| `RealDownloadService.cs` | 所有框架共用 | **核心桥接**:GeneralUpdate → IDownloadService | +| `SemiUrsaClientView.axaml` + `.cs` | Avalonia + SemiUrsa | Full state machine, dark mode toggle, notifications, progress bar animation | +| `SemiUrsaUpgradeView.axaml` + `.cs` | Avalonia + SemiUrsa (Upgrade) | Waiting UI, indeterminate progress, transition animations | +| `LayUIStyle.xaml` + `.cs` | WPF + LayUI.Wpf | Glass effect, modal dialogs, progress bar | +| `WPFDevelopersStyle.xaml` + `.cs` | WPF + WPFDevelopers | Circular progress, breath lamp animation, notification icon | +| `AntdUIStyle.cs` | WinForms + AntdUI | Dark theme, localization, wave progress button, cancel | +| `NativeWpfWindow.xaml` + `.cs` | Native WPF (no skin) | Clean window, progress bar, status text | +| `NativeWinForms.cs` | Native WinForms | Simple form, progress bar, cancel | +| `MauiUpdatePage.xaml` + `.cs` | MAUI | Cross-platform, dark mode, AppThemeBinding | +| `ConsoleProgress.cs` | Console App | ANSI progress bar, status text | +| `DownloadViewModels.cs` | Shared (all frameworks) | Complete ViewModel + DownloadStatistics | +| `RealDownloadService.cs` | Shared (all frameworks) | **Core Bridge**: GeneralUpdate → IDownloadService | --- -## 输出 +## Output -根据用户框架和需求,输出以下内容(按优先级排列): -- ✅ `RealDownloadService.cs` — 核心桥接代码(替换 MockDownloadService) -- ✅ `DownloadViewModels.cs` — 完整 MVVM ViewModel(全状态覆盖) -- ✅ 目标框架的窗口/页面 XAML + Code-Behind -- ✅ 集成步骤说明(DI 注册 / 文件替换 / Navigation) +Based on the user's framework and requirements, output the following (in priority order): +- ✅ `RealDownloadService.cs` — Core bridge code (replaces MockDownloadService) +- ✅ `DownloadViewModels.cs` — Complete MVVM ViewModel (full state coverage) +- ✅ Target framework window/page XAML + Code-Behind +- ✅ Integration step instructions (DI registration / file replacement / Navigation) -## 相关技能 +## Related Skills -- `/generalupdate-init` — 如果还未配置 Bootstrap -- `/generalupdate-troubleshoot` — 如果 UI 显示异常 +- `/generalupdate-init` — If Bootstrap is not yet configured +- `/generalupdate-troubleshoot` — If UI displays abnormal values diff --git a/.claude/skills/generalupdate-ui/templates/DownloadViewModels.cs b/.claude/skills/generalupdate-ui/templates/DownloadViewModels.cs index 37da07f..15aeb49 100644 --- a/.claude/skills/generalupdate-ui/templates/DownloadViewModels.cs +++ b/.claude/skills/generalupdate-ui/templates/DownloadViewModels.cs @@ -106,8 +106,16 @@ private void OnStatisticsChanged(DownloadStatistics stats) Dispatch(() => { Statistics = stats; - if (stats.Speed > 0) + + // 版本信息同步 + if (stats.Version != null) + VersionText = $"版本: {stats.Version}"; + + // 速度信息:小于 0.01 MB/s 视为 0 + if (stats.Speed > 0.01) SpeedText = $"{stats.Speed:F1} MB/s"; + else + SpeedText = ""; }); } @@ -118,6 +126,10 @@ private void OnStatusChanged(DownloadStatus status) Status = status; UpdateVisibility(); UpdateStatusText(); + + // 非下载状态清空速度 + if (status is not (DownloadStatus.Downloading or DownloadStatus.Applying)) + SpeedText = ""; }); } diff --git a/.claude/skills/generalupdate-ui/templates/LayUIStyle.xaml b/.claude/skills/generalupdate-ui/templates/LayUIStyle.xaml index 0715098..a452afc 100644 --- a/.claude/skills/generalupdate-ui/templates/LayUIStyle.xaml +++ b/.claude/skills/generalupdate-ui/templates/LayUIStyle.xaml @@ -1,19 +1,19 @@ - + - - - + - + - + - + - - - + diff --git a/.claude/skills/generalupdate-ui/templates/MauiUpdatePage.xaml b/.claude/skills/generalupdate-ui/templates/MauiUpdatePage.xaml index 75aa91d..4b52787 100644 --- a/.claude/skills/generalupdate-ui/templates/MauiUpdatePage.xaml +++ b/.claude/skills/generalupdate-ui/templates/MauiUpdatePage.xaml @@ -1,12 +1,12 @@ @@ -22,7 +22,7 @@ - + - - public partial class UpdateViewModel : ObservableObject { @@ -16,10 +16,10 @@ public partial class UpdateViewModel : ObservableObject private readonly string _secretKey; private CancellationTokenSource? _cts; - [ObservableProperty] private string _versionText = "检测中..."; + [ObservableProperty] private string _versionText = "Checking..."; [ObservableProperty] private string _releaseNotes = ""; [ObservableProperty] private double _progressValue; - [ObservableProperty] private string _statusText = "准备就绪"; + [ObservableProperty] private string _statusText = "Ready"; [ObservableProperty] private string _speedText = ""; [ObservableProperty] private bool _isUpdating; @@ -39,7 +39,7 @@ private async Task StartUpdateAsync() try { - StatusText = "正在连接服务器..."; + StatusText = "Connecting to server..."; var bootstrap = new GeneralUpdateBootstrap() .SetSource(_updateUrl, _secretKey) @@ -48,7 +48,7 @@ private async Task StartUpdateAsync() { MainThread.BeginInvokeOnMainThread(() => { - VersionText = e.Version ?? "未知版本"; + VersionText = e.Version ?? "Unknown version"; }); }) .AddListenerMultiDownloadStatistics((_, e) => @@ -64,21 +64,21 @@ private async Task StartUpdateAsync() { MainThread.BeginInvokeOnMainThread(() => { - StatusText = "下载完成,正在安装..."; + StatusText = "Download completed, installing..."; }); }) .AddListenerMultiAllDownloadCompleted((_, e) => { MainThread.BeginInvokeOnMainThread(() => { - StatusText = "更新完成!"; + StatusText = "Update Completed!"; }); }) .AddListenerException((_, e) => { MainThread.BeginInvokeOnMainThread(() => { - StatusText = $"错误: {e.Message}"; + StatusText = $"Error: {e.Message}"; }); }); @@ -86,7 +86,7 @@ private async Task StartUpdateAsync() } catch (Exception ex) { - StatusText = $"更新失败: {ex.Message}"; + StatusText = $"Update failed: {ex.Message}"; } finally { diff --git a/.claude/skills/generalupdate-ui/templates/RealDownloadService.cs b/.claude/skills/generalupdate-ui/templates/RealDownloadService.cs index c1f3d98..12261b7 100644 --- a/.claude/skills/generalupdate-ui/templates/RealDownloadService.cs +++ b/.claude/skills/generalupdate-ui/templates/RealDownloadService.cs @@ -1,3 +1,7 @@ +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; using GeneralUpdate.Core; using GeneralUpdate.Core.Configuration; using GeneralUpdate.Core.Enum; @@ -41,15 +45,17 @@ public class RealDownloadService : IDownloadService private readonly string _updateUrl; private readonly string _secretKey; private readonly AppType _appType; + private readonly string _productId; private CancellationTokenSource? _cts; private int _retryCount; private const int MaxRetries = 3; - public RealDownloadService(string updateUrl, string secretKey, AppType appType = AppType.Client) + public RealDownloadService(string updateUrl, string secretKey, AppType appType = AppType.Client, string productId = "unknown") { _updateUrl = updateUrl; _secretKey = secretKey; _appType = appType; + _productId = productId; CurrentStatistics = new DownloadStatistics { @@ -128,37 +134,93 @@ private async Task RunCheckAsync(CancellationToken token) try { - // ⚠️ 当前 GeneralUpdate 版本没有"仅检查不下载"的 API - // 使用 LaunchAsync 模拟,通过回调判断是否有更新 - var bootstrap = BuildBootstrap(); + using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; - // 标记是否有更新 - var hasUpdate = false; - bootstrap.AddListenerUpdateInfo((_, e) => + var payload = JsonSerializer.Serialize(new { - hasUpdate = (e.Info?.Body?.Count ?? 0) > 0; - if (hasUpdate) - { - CurrentStatistics.Version = e.Version; - CurrentStatistics.TotalBytesToReceive = e.Size; - } - StatisticsChanged?.Invoke(CurrentStatistics); + appKey = _secretKey, + appType = (int)_appType, + clientVersion = GetCurrentVersion(), + productId = _productId, + platform = GetPlatform(), + tenantId = "default" }); - var result = await bootstrap.LaunchAsync(); + // Use _updateUrl as-is; it should already point to the full Verification endpoint + var response = await client.PostAsync( + _updateUrl, + new StringContent(payload, Encoding.UTF8, "application/json"), + token); + + if (!response.IsSuccessStatusCode) + { + ErrorOccurred?.Invoke($"Server returned {(int)response.StatusCode}"); + UpdateState(DownloadStatus.DownloadError); + return; + } + + var json = await response.Content.ReadAsStringAsync(token); + using var doc = JsonDocument.Parse(json); + + // Check for server error envelope first: { code, message, body } + if (doc.RootElement.TryGetProperty("code", out var codeProp) && + codeProp.GetInt32() != 200) + { + var msg = doc.RootElement.TryGetProperty("message", out var msgProp) + ? msgProp.GetString() ?? "Unknown server error" + : "Unknown server error"; + ErrorOccurred?.Invoke($"Server error: {msg}"); + UpdateState(DownloadStatus.DownloadError); + return; + } + + if (!doc.RootElement.TryGetProperty("body", out var body) || body.GetArrayLength() == 0) + { + UpdateState(DownloadStatus.AlreadyLatest); + return; + } + + var hasUpdate = body.GetArrayLength() > 0; if (hasUpdate) + { + var first = body[0]; + CurrentStatistics.Version = first.TryGetProperty("version", out var v) ? v.GetString() : null; + CurrentStatistics.TotalBytesToReceive = first.TryGetProperty("size", out var s) ? s.GetInt64() : 0; + StatisticsChanged?.Invoke(CurrentStatistics); UpdateState(DownloadStatus.FoundUpdate); + } else + { UpdateState(DownloadStatus.AlreadyLatest); + } + } + catch (OperationCanceledException) + { + ErrorOccurred?.Invoke("Check cancelled"); + UpdateState(DownloadStatus.Idle); } catch (Exception ex) { - ErrorOccurred?.Invoke($"检查更新失败: {ex.Message}"); + ErrorOccurred?.Invoke($"Check failed: {ex.Message}"); UpdateState(DownloadStatus.DownloadError); } } + private static string GetCurrentVersion() + { + return System.Reflection.Assembly.GetEntryAssembly() + ?.GetName()?.Version?.ToString(4) ?? "1.0.0.0"; + } + + private static string GetPlatform() + { + if (OperatingSystem.IsWindows()) return "windows"; + if (OperatingSystem.IsLinux()) return "linux"; + if (OperatingSystem.IsMacOS()) return "macos"; + return "unknown"; + } + private async Task RunDownloadAsync(CancellationToken token) { UpdateState(DownloadStatus.Downloading); diff --git a/.claude/skills/generalupdate-ui/templates/SemiUrsaClientView.axaml b/.claude/skills/generalupdate-ui/templates/SemiUrsaClientView.axaml index 7e792bf..5160c11 100644 --- a/.claude/skills/generalupdate-ui/templates/SemiUrsaClientView.axaml +++ b/.claude/skills/generalupdate-ui/templates/SemiUrsaClientView.axaml @@ -149,7 +149,7 @@ - - +