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 @@
-
-
+
-
+
diff --git a/.claude/skills/generalupdate-ui/templates/WPFDevelopersStyle.xaml b/.claude/skills/generalupdate-ui/templates/WPFDevelopersStyle.xaml
index 531e8b8..79a9188 100644
--- a/.claude/skills/generalupdate-ui/templates/WPFDevelopersStyle.xaml
+++ b/.claude/skills/generalupdate-ui/templates/WPFDevelopersStyle.xaml
@@ -1,14 +1,14 @@
-
+
-
@@ -38,7 +38,7 @@
HorizontalAlignment="Center" />
-
+
-
+
diff --git a/.claude/skills/generalupdate-ui/templates/WpfDevelopersViewModel.cs b/.claude/skills/generalupdate-ui/templates/WpfDevelopersViewModel.cs
index cff98cd..130286d 100644
--- a/.claude/skills/generalupdate-ui/templates/WpfDevelopersViewModel.cs
+++ b/.claude/skills/generalupdate-ui/templates/WpfDevelopersViewModel.cs
@@ -4,8 +4,8 @@
namespace Upgrade.WPF.ViewModels;
///
-/// 【Skill 自动生成】WPF + WPFDevelopers 专用 ViewModel
-/// 包含圆形进度条所需的 SpeedText 属性
+/// [Skill Auto-generated] WPF + WPFDevelopers-specific ViewModel
+/// Contains SpeedText property required by circular progress bar
///
public partial class WpfDevelopersUpdateViewModel : ObservableObject
{
@@ -13,7 +13,7 @@ public partial class WpfDevelopersUpdateViewModel : ObservableObject
[ObservableProperty] private DownloadStatistics _statistics;
[ObservableProperty] private DownloadStatus _status;
- [ObservableProperty] private string _statusText = "准备就绪";
+ [ObservableProperty] private string _statusText = "Ready";
[ObservableProperty] private string _speedText = "";
public WpfDevelopersUpdateViewModel(IDownloadService downloadService)
@@ -43,9 +43,9 @@ private void OnStatusChanged(DownloadStatus status)
Status = status;
StatusText = status switch
{
- DownloadStatus.Downloading => "正在下载...",
- DownloadStatus.Completed => "更新完成!",
- DownloadStatus.Paused => "已暂停",
+ DownloadStatus.Downloading => "Downloading...",
+ DownloadStatus.Completed => "Update Completed!",
+ DownloadStatus.Paused => "Paused",
_ => ""
};
});
diff --git a/.clinerules/01-generalupdate-init.md b/.clinerules/01-generalupdate-init.md
index e0ef229..a3307c4 100644
--- a/.clinerules/01-generalupdate-init.md
+++ b/.clinerules/01-generalupdate-init.md
@@ -13,7 +13,7 @@ SetSource(url, key) -> SetOption(AppType.Client) -> LaunchAsync()
## 4 大场景: None/UpgradeOnly/MainOnly/Both
-## manifest.json 字段
+## generalupdate.manifest.json 字段
MainAppName, UpdateAppName, ProductId, InstallPath, UpdatePath, ClientVersion, UpgradeClientVersion
## 关键: UpgradeApp须随首个版本发布, 双进程AppSecretKey一致, 版本4段式
diff --git a/.clinerules/05-generalupdate-troubleshoot.md b/.clinerules/05-generalupdate-troubleshoot.md
index beff63c..985513d 100644
--- a/.clinerules/05-generalupdate-troubleshoot.md
+++ b/.clinerules/05-generalupdate-troubleshoot.md
@@ -17,7 +17,7 @@ tags: ["dotnet", "generalupdate"]
## 诊断流程 (5步)
-1. **版本号** — `manifest.json` 的 `ClientVersion` / `UpgradeClientVersion` 是否与服务端返回的版本一致? 4段式版本(string)严格匹配。
+1. **版本号** — `generalupdate.manifest.json` 的 `ClientVersion` / `UpgradeClientVersion` 是否与服务端返回的版本一致? 4段式版本(string)严格匹配。
2. **manifest 字段** — `MainAppName`、`UpdateAppName`、`ProductId`、`InstallPath`、`UpdatePath` 是否填写正确且与服务端一致?
3. **UpgradeApp 存在** — 主应用目录下是否存在 `Upgrade` 子目录及 `Upgrade.exe` (或对应平台可执行文件)? 随首个版本一同发布的。
4. **策略可访问** — `SetSource(url, key)` 配置的 API/OSS 端点能否正常响应? OSS 策略需要 `SetOSSInfo` 配置正确。
@@ -29,7 +29,7 @@ tags: ["dotnet", "generalupdate"]
|------|------|-----------|
| **升级没启动** | `LaunchAsync()` 未调用 / 调用顺序错误 / 主进程未检测到新版本 | 确认 `Bootstrap.LaunchAsync()` 在 `Main()` 中调用, 且 `SetOption(AppType.Client)` 正确 |
| **Method not found** | 引用的 GeneralUpdate 版本与服务端不匹配 / 混用不同大版本 | 统一 NuGet 版本, 清理 bin/obj 后重新生成 |
-| **路径超长 (>260)** | Windows 路径限制, `InstallPath` / `UpdatePath` 过深 | 缩短安装路径; 或启用 `SetOption` 中的长路径支持 (`LongPathSupport = true`) |
+| **路径超长 (>260)** | Windows 路径限制, `InstallPath` / `UpdatePath` 过深 | 缩短安装路径; 或在项目配置中启用长路径支持 (`app.manifest` / `runtimeconfig`) |
| **IPC 暴露** | EncryptedFile IPC 写入明文 / NamedPipe 未鉴权 | 启用加密传输; NamedPipe 设置 `AccessControl` 限制连接用户; 不使用 `AutoFallback` 回退到明文 |
| **跨租户泄露** | 多租户部署共用 `ProductId` / `AppSecretKey` | 每个租户分配独立 `ProductId` 和 `AppSecretKey`; 服务端鉴权时校验租户标识 |
@@ -38,7 +38,7 @@ tags: ["dotnet", "generalupdate"]
| 问题 | 原因 | 排查/解决 |
|------|------|-----------|
| **静默更新不生效** | `UpdateMode.Silent` 配置缺失 / 权限不足无法后台启动 | 确认 `SetOption(UpdateMode.Silent)` 且应用有后台运行权限; 检查 `LaunchCommand` 参数是否正确传递 |
-| **无限循环更新** | `ClientVersion` 与服务端最新版本不一致导致不断检测到"新版本" | 验证更新后是否更新了本地 `manifest.json` 的版本号; 确认服务端已标记该版本为"已发布"状态 |
+| **无限循环更新** | `ClientVersion` 与服务端最新版本不一致导致不断检测到"新版本" | 验证更新后是否更新了本地 `generalupdate.manifest.json` 的版本号; 确认服务端已标记该版本为"已发布"状态 |
| **OSS 无更新** | Bucket 配置错误 / 文件命名不匹配 / 签名过期 | `curl` 测试 OSS URL 是否直接可下载; 确认 `SetOSSInfo` 的 endpoint/bucket/region 正确 |
| **文件占用** | 更新时目标文件正在被使用 (主进程/杀软/搜索索引) | 关闭主进程后更新; 使用 `Bowl` 守护进程管理进程生命周期; 排除杀软扫描目录 |
@@ -50,9 +50,9 @@ tags: ["dotnet", "generalupdate"]
| **差分更新失败** | BSDIFF 补丁应用时数据不完整 / 旧版本基准文件不匹配 | 校验原始文件哈希; 确认差分策略仅在同源版本间使用 |
| **升级后配置丢失** | `UpdatePath` 未包含配置文件 / 更新后未合并 `appsettings.json` | 在更新流程中添加配置文件合并步骤; 或使用外部配置中心 |
| **信号量泄露** | IPC 信号量未正确释放, 重启后端口/资源被占用 | 重启系统; 检查 `Bowl` 是否正确清理资源; 监控信号量计数 |
-| **ZIP 遍历写入** | 恶意更新的 ZIP 包包含 `../` 路径覆盖系统文件 | 使用 `ZipSecurity` 校验项, 拒绝包含 `../` 或绝对路径的条目 |
+| **ZIP 遍历写入** | 恶意更新的 ZIP 包包含 `../` 路径覆盖系统文件 | 解压时校验 ZIP 条目路径, 拒绝包含 `../` 或绝对路径的条目 |
| **AOT 编译失败** | 动态加载 / 反射代码未适配 NativeAOT | 添加 `[DynamicDependency]` 特性; 避免 `Assembly.Load` 等动态加载 |
-| **SignalR 重连慢** | WebSocket 长连接断开后 `RetryDelay` 配置过长 | 调整 `SignalRRetryPolicy` 的 `RetryDelay` 和 `MaxRetryCount` |
+| **SignalR 重连慢** | WebSocket 长连接断开后 `RetryDelay` 配置过长 | 降低重试延迟并限制最大重试次数, 或自定义重连策略 |
| **SSL 证书错误** | 自签名证书未受信 / 服务器 SNI 不匹配 | 配置 `SslValidationPolicy`; 或添加自定义证书验证委托 |
## L级 (Low) — 非关键
@@ -69,10 +69,10 @@ tags: ["dotnet", "generalupdate"]
```bash
# 测试 API 是否可访问
-curl -v "http://your-server/api/update/check?appName=MyApp&clientVersion=1.0.0.1"
+curl -v -X POST "http://your-server/Upgrade/Verification" -H "Content-Type: application/json" -d '{"appKey":"xxx","appType":0,"clientVersion":"1.0.0.1","productId":"xxx","platform":"windows","tenantId":"default"}'
-# 验证 manifest.json 内容
-cat /path/to/app/manifest.json | jq .
+# 验证 generalupdate.manifest.json 内容
+cat /path/to/app/generalupdate.manifest.json | jq .
# 检查 UpgradeApp 是否存在
ls -la /path/to/app/Upgrade/Upgrade.exe
@@ -98,6 +98,6 @@ powershell "Get-CimInstance Win32_Semaphore | Select-Object Name, Count"
## 安全注意事项
- IPC 传输 — 不使用明文 `AutoFallback`, 始终启用加密
-- ZIP 包校验 — 启用 `ZipSecurity` 拒绝路径穿越条目
+- ZIP 包校验 — 解压时校验 ZIP 条目路径, 拒绝 `../` 和绝对路径条目
- 签名验证 — 程序集签名: 在 `SetSource` 中提供公钥, 拒绝未签名更新
- AppSecretKey 管理 — 硬编码在客户端是最后手段; 优先从启动参数或环境变量注入
diff --git a/.cursor/rules/generalupdate-init.mdc b/.cursor/rules/generalupdate-init.mdc
index 085bffd..c3cdf28 100644
--- a/.cursor/rules/generalupdate-init.mdc
+++ b/.cursor/rules/generalupdate-init.mdc
@@ -17,17 +17,17 @@ SetSource(url, key) -> SetOption(AppType.Client) -> LaunchAsync()
## 4 大更新场景
- None, UpgradeOnly, MainOnly, Both
-## manifest.json
+## generalupdate.manifest.json
MainAppName, UpdateAppName, ProductId, InstallPath, UpdatePath, ClientVersion, UpgradeClientVersion
## 关键警告
- UpgradeApp.exe 必须随首个版本发布(不能自己下载自己)
- Client 和 Upgrade 必须相同 AppSecretKey
- 版本号必须是 4 段式
-- manifest ClientVersion 不能为空
+- generalupdate.manifest.json ClientVersion 不能为空
## 3 种配置方式
-- Minimal: SetSource + manifest.json
+- Minimal: SetSource + generalupdate.manifest.json
- Standard: SetConfig(UpdateRequest) + Option + events
- appsettings: LoadFromConfiguration(config)
diff --git a/.github/instructions/backend-api.instructions.md b/.github/instructions/backend-api.instructions.md
index 11d780f..ced4397 100644
--- a/.github/instructions/backend-api.instructions.md
+++ b/.github/instructions/backend-api.instructions.md
@@ -9,7 +9,7 @@ applyTo: "**/*.cs"
## Verification Endpoint
POST /Upgrade/Verification
Request: appKey, appType, clientVersion, productId, platform, tenantId
-Response: body[{ id, version, url, hash, size, name, appType, isCrossVersion }]
+Response: { code, message, body: [{ id, version, url, hash, size, name, appType, isCrossVersion }] }
## Report Endpoint
POST /Upgrade/Report
diff --git a/RULES.md b/RULES.md
index 26c8386..538cf3e 100644
--- a/RULES.md
+++ b/RULES.md
@@ -6,7 +6,7 @@
- IPC: Encrypted file(AES, default), NamedPipe, SharedMemory, AutoFallback
## Bootstrap
-- Minimal: SetSource(url, key) -> SetOption(Client) -> LaunchAsync()
+- Minimal: SetSource(url, key) -> SetOption(AppType.Client) -> LaunchAsync()
- Standard: SetConfig(UpdateRequest) -> SetOption() -> AddListener*() -> LaunchAsync()
- From config: LoadFromConfiguration(config.GetSection("GeneralUpdate"))
@@ -46,7 +46,7 @@ Hooks, Strategy, UpdateReporter, DownloadSource, DownloadOrchestrator, DownloadP
## Diagnostics
1. NuGet versions match
-2. manifest.json valid
+2. generalupdate.manifest.json valid
3. UpgradeApp.exe exists
4. Server API reachable
5. Logs in Logs/generalupdate-trace *.log