Update Rules
通用小工具更新方案,供各个桌面工具复用。
# 通用小工具更新方案
本文档描述一套适合多个个人小工具复用的轻量更新系统。目标是方便维护和复制,不追求复杂的企业级安全发布流程。
## 1. 总体约定
- 更新域名:`https://update.with-haomo.online`
- 服务端目录:`/var/www/updates`
- 每个软件一个独立子目录,目录名就是工具名。
- 更新包统一使用 `.zip`。
- 更新元数据统一使用 `latest.json`。
- 完整性校验使用 SHA256。
- 不做签名校验;HTTPS + SHA256 已满足当前方便优先的需求。
URL 结构:
```text
https://update.with-haomo.online/<tool>/latest.json
https://update.with-haomo.online/<tool>/<tool>_latest.zip
https://update.with-haomo.online/<tool>/<tool>_latest.zip.sha256
https://update.with-haomo.online/<tool>/<tool>_<version>.zip
https://update.with-haomo.online/<tool>/<tool>_<version>.zip.sha256
```
示例:
```text
https://update.with-haomo.online/icon-ring-launcher/latest.json
https://update.with-haomo.online/icon-ring-launcher/icon-ring-launcher_latest.zip
https://update.with-haomo.online/icon-ring-launcher/icon-ring-launcher_20260618130003.zip
https://update.with-haomo.online/icon-ring-launcher/icon-ring-launcher_20260618130003.zip.sha256
```
## 2. 每个软件需要确定的变量
每个软件接入时,只需要固定下面这些值:
```text
TOOL_NAME=<工具目录名,例如 icon-ring-launcher>
APP_DATA_DIR=<用户数据目录,例如 %APPDATA%/IconRingLauncher>
PROJECT_FILE=<构建入口,例如 IconRingLauncher.csproj>
PUBLISH_OUTPUT=<本地发布目录,例如 publish 或 publish-next>
```
规则:
- `TOOL_NAME` 必须和服务器目录一致。
- `latest.json` 里的 `tool` 必须等于 `TOOL_NAME`。
- zip 文件名必须是 `<TOOL_NAME>_<VERSION>.zip`。
- 公开下载链接使用 `<TOOL_NAME>_latest.zip`,由发布脚本在每次发布时覆盖更新。
- `VERSION` 使用 UTC 时间戳,格式固定为 `yyyyMMddHHmmss`,例如 `20260618130003`。
- 版本号用字符串比较即可判断新旧。
## 3. latest.json 格式
`latest.json` 是客户端检查更新的唯一入口。
示例:
```json
{
"tool": "icon-ring-launcher",
"version": "20260618130003",
"file": "icon-ring-launcher_20260618130003.zip",
"sha256": "0b78c8b11fd27a491ebff9a37acb5d7ad371d440232d274ad0f60cf5f250d15d",
"signature": "",
"size": 134474,
"publishedAt": "2026-06-18T13:00:05Z",
"notes": "Initial public update package",
"minClientVersion": ""
}
```
字段说明:
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `tool` | 是 | 工具名,必须和服务器目录一致 |
| `version` | 是 | 版本号,使用 `yyyyMMddHHmmss` |
| `file` | 是 | zip 更新包文件名 |
| `sha256` | 是 | zip 文件 SHA256 |
| `signature` | 否 | 保留字段,当前为空 |
| `size` | 否 | zip 字节数 |
| `publishedAt` | 否 | 发布时间,UTC ISO 字符串 |
| `notes` | 否 | 更新说明 |
| `minClientVersion` | 否 | 保留字段,未来可限制最低更新器版本 |
## 4. 服务端 Nginx 规则
当前服务器已经配置:
- 域名:`update.with-haomo.online`
- HTTPS:Let's Encrypt 证书
- 静态根目录:`/var/www/updates`
- 目录列表关闭
- 根路径返回 `404`
- `latest.json` 不缓存
- zip 和 sha256 长缓存
核心 Nginx 配置模板见:
```text
deploy/nginx-update.with-haomo.online.conf
```
新增软件时不需要改 Nginx,只要创建新目录:
```bash
mkdir -p /var/www/updates/<tool>
```
发布脚本会自动创建目录。
## 5. 发布脚本
Windows 项目优先使用:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\deploy.ps1
```
常用参数:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\deploy.ps1 `
-ToolName "icon-ring-launcher" `
-RemoteHost "root@120.26.109.85" `
-RemoteRoot "/var/www/updates" `
-ReleaseNotes "更新说明"
```
脚本行为:
1. 执行 `dotnet publish`。
2. 将发布目录打包成 `<tool>_<version>.zip`。
3. 生成 zip 的 SHA256。
4. 生成 `latest.json`。
5. 上传 zip 和 `.sha256`。
6. 上传 `latest.json.tmp`。
7. 远程复制当前 zip 为 `<tool>_latest.zip`,方便网页直接下载最新版。
8. 远程原子替换为 `latest.json`。
8. 清理旧版本,只保留最近 5 个 zip。
发布顺序很重要:
```text
先上传 zip 和 sha256
再上传 latest.json.tmp
再刷新 <tool>_latest.zip
最后 mv latest.json.tmp latest.json
```
这样客户端不会读到一个已经声明但还没上传完成的新版本。
Linux/macOS 或 Git Bash 可使用:
```bash
./deploy.sh
```
如果某个工具不是 .NET 项目,可以复制脚本后只替换构建部分,保留打包、sha256、latest.json、上传逻辑。
## 6. 客户端更新逻辑
每个软件客户端都复用同一套流程:
1. 启动时先检查是否已有 pending update。
2. 如果已有 pending update,弹窗询问用户是否现在重启并更新。
3. 正常启动软件。
4. 后台静默请求:
```text
https://update.with-haomo.online/<tool>/latest.json
```
5. 如果远程版本号大于本地版本号,下载对应 zip。
6. 计算 zip SHA256。
7. SHA256 和 `latest.json` 一致后,解压到 pending 目录。
8. 写入 pending 标记。
9. 弹窗询问用户:
- 现在更新:立即重启软件并应用更新。
- 下次启动更新:保留 pending 标记,下次启动时再次提示。
网络错误、404、JSON 错误、SHA256 不一致都只写日志,不影响软件正常启动。手动检查更新时,这些错误应显示给用户。
避免重复提醒的规则:
- 启动时如果已有 `pending.json`,必须先判断 `pending.version` 是否仍然大于本地版本。
- 如果 `pending.version <= 本地版本`,说明更新已经应用或本地版本不需要该 pending,必须删除 `pending.json`,不要弹窗。
- 如果已有有效 `pending.json`,启动时只提示这一个 pending,不要同时继续后台下载同一个版本。
- 手动检查更新时,如果已有有效 pending,应直接提示“更新已准备好”,不要重复下载同版本包。
- 更新成功后必须同时写入 `%APPDATA%/<AppName>/version.txt` 并删除 `pending.json`。
## 7. Windows 桌面软件的推荐实现
Windows 运行中的 exe 通常不能被覆盖,所以不要尝试“运行中直接替换”。
推荐流程:
```text
下载新版本
校验 SHA256
解压到 %APPDATA%/<AppName>/updates/<version>/payload
写入 %APPDATA%/<AppName>/updates/pending.json
弹窗询问现在重启更新,或者下次启动更新
```
pending 文件示例:
```json
{
"version": "20260618130003",
"extractedDirectory": "C:\\Users\\xxx\\AppData\\Roaming\\App\\updates\\20260618130003\\payload",
"preparedAt": "2026-06-18T13:00:05Z"
}
```
替换策略:
1. 用户选择“现在更新”。
2. 启动一个 updater 脚本或 helper 进程。
3. 主程序退出。
4. updater 等待主程序退出。
5. 备份当前安装目录。
6. 将 payload 复制到安装目录。
7. 写入本地 `version.txt`。
8. 删除 pending 标记。
9. 重新启动软件。
如果用户选择“下次启动更新”,保留 `pending.json`,下次启动时再次提示。
## 8. 本地版本记录
本地版本推荐按这个优先级读取:
1. 用户数据目录:`%APPDATA%/<AppName>/version.txt`
2. 安装目录:`version.txt`
3. 程序程序集版本
4. 空字符串
发布脚本会把 `version.txt` 写入 zip 包。
更新成功后,客户端应写入:
```text
%APPDATA%/<AppName>/version.txt
```
内容就是版本号,例如:
```text
20260618130003
```
## 9. 如何新增一个软件
以新工具 `note-taker` 为例:
1. 复制 `deploy.ps1` 到 `note-taker` 项目根目录。
2. 修改默认变量:
```powershell
[string]$ToolName = "note-taker"
```
3. 如果不是 .NET 项目,替换脚本中的构建命令,但保留后面的 zip、sha256、manifest、上传逻辑。
4. 在客户端更新模块中修改:
```csharp
private const string ToolName = "note-taker";
private const string UpdateBaseUrl = "https://update.with-haomo.online";
```
5. 修改用户数据目录,例如:
```csharp
AppDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"NoteTaker");
```
6. 发布:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\deploy.ps1 -ReleaseNotes "首次发布"
```
7. 验证:
```powershell
Invoke-RestMethod https://update.with-haomo.online/note-taker/latest.json
```
8. 在主站新增公开入口页:
```text
https://www.with-haomo.online/apps/note-taker
```
该页面至少说明:
- 软件用途和工具名。
- 直接下载链接:`https://update.with-haomo.online/<tool>/<tool>_latest.zip`
- `latest.json` 地址。
- zip 更新包地址规则。
- SHA256 校验文件地址规则。
- 客户端自动更新和手动更新入口。
9. 在 `https://www.with-haomo.online/` 的 App 模块中加入该软件链接。
## 10. 发布后验证清单
每次发布后建议检查:
```powershell
$m = Invoke-RestMethod https://update.with-haomo.online/<tool>/latest.json
$zip = Join-Path $env:TEMP $m.file
Invoke-WebRequest "https://update.with-haomo.online/<tool>/$($m.file)" -OutFile $zip
$hash = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLowerInvariant()
$hash -eq $m.sha256
Remove-Item $zip
```
期望结果:
```text
True
```
还可以检查目录不可浏览:
```powershell
Invoke-WebRequest https://update.with-haomo.online/<tool>/ -SkipHttpErrorCheck
```
期望状态码:
```text
404
```
## 11. 常见问题
### latest.json 可以访问,但 zip 404
通常是发布顺序或文件名不一致。
检查:
- `latest.json` 里的 `file`
- 服务器目录里实际 zip 文件名
- Nginx 文件名规则是否匹配 `<tool>_<14位版本号>.zip`
### 客户端一直不更新
检查:
- 本地 `version.txt` 是否大于或等于远程版本
- `tool` 字段是否和客户端 `ToolName` 一致
- `sha256` 是否为 64 位十六进制
- 客户端日志:`%APPDATA%/<AppName>/updates/update.log`
### 每次启动都提示发现新版本
通常是 pending 状态没有被正确清理,或者本地版本没有写入成功。
检查:
- `%APPDATA%/<AppName>/updates/pending.json` 是否残留。
- `%APPDATA%/<AppName>/version.txt` 是否存在,内容是否大于或等于远程 `latest.json.version`。
- 更新脚本是否在成功复制文件后删除了 `pending.json`。
- 启动逻辑是否在读取 pending 时判断了 `pending.version > 本地版本`。
- 启动时已有有效 pending 的情况下,是否还继续后台检查并重复准备同一个版本。
Windows 路径包含中文时,updater PowerShell 脚本必须用带 BOM 的 UTF-8 写入,否则 Windows PowerShell 可能把中文路径读成乱码,导致安装目录替换失败,`pending.json` 继续残留,从而每次启动都提示更新。
### 更新包已下载但没有立即生效
这是 Windows 桌面软件的预期行为。
当前 exe 运行中不能可靠覆盖,所以更新包会先准备好;用户可以选择立即重启软件应用更新,也可以下次启动时再更新。
### publish 目录无法覆盖
如果软件正在运行,Windows 会锁定 exe。
处理方式:
- 从托盘退出软件后重新发布。
- 或发布到 `publish-next`,手动替换。
- 或依赖自动更新流程让下次启动替换。
## 12. 当前 IconRingLauncher 配置
当前工具名:
```text
icon-ring-launcher
```
当前更新地址:
```text
https://update.with-haomo.online/icon-ring-launcher/latest.json
```
当前服务器目录:
```text
/var/www/updates/icon-ring-launcher
```
当前客户端更新模块:
```text
Models/UpdateManifest.cs
Services/UpdateService.cs
```
当前发布脚本:
```text
deploy.ps1
deploy.sh
```
## 13. 主站 App 模块与公开说明页
主站地址:
```text
https://www.with-haomo.online/
```
主站分为三类入口:
- `Doc`:公共文档,例如 `update_rules`。
- `App`:桌面小工具,每个工具一页,说明更新地址和更新方法。
- `PWA`:已经部署在子域名上的在线应用。
App 页面约定:
```text
https://www.with-haomo.online/apps/<tool>
```
例如:
```text
https://www.with-haomo.online/apps/icon-ring-launcher
```
每个 App 页面应包含:
- 工具名:必须和更新服务里的 `<tool>` 一致。
- 直接下载:`https://update.with-haomo.online/<tool>/<tool>_latest.zip`
- 更新元数据:`https://update.with-haomo.online/<tool>/latest.json`
- 更新包规则:`https://update.with-haomo.online/<tool>/<tool>_<version>.zip`
- 校验文件规则:`https://update.with-haomo.online/<tool>/<tool>_<version>.zip.sha256`
- 用户侧更新方式:启动检查、准备更新、立即重启更新、下次启动更新、手动检查更新。
新增软件时,需要同步维护两处:
```text
/var/www/updates/<tool> # 更新包和 latest.json
/var/www/suma-site/apps/<tool>.html # 公开说明页
```
如果只上传了更新包,但没有维护主站 App 页面,客户端更新仍然可用;只是公开下载和说明入口不完整。