a4api 内置完整的自更新能力:发布新版本后,应用会在启动时静默检查,或在你点击顶部「检查更新」时发现更新;经你确认后下载安装包并显示实时进度,校验通过后再次确认即可拉起安装向导完成升级。整条链路以「签名清单 + 内容哈希 + 传输白名单」保证装进来的安装包就是作者发布的那个,并带防降级与忽略版本等细节策略。本文章拆解更新的完整流程与每一环的防篡改设计。

1. 更新流程总览

graph LR A[启动静默检查
或手动检查更新] --> B[并行拉取清单
GitHub 优先 Gitee 回退] B --> C{Ed25519 验签
与字段校验} C -->|失败| D[清单作废
不弹任何提示] C -->|通过| E{版本比当前新?
且未被忽略} E -->|否| F[已是最新] E -->|是| G[弹窗展示版本号
与更新说明] G --> H[确认下载
边下边算 SHA256] H --> I{哈希与尺寸
校验} I -->|失败| J[丢弃不落盘生效] I -->|通过| K[提示立即更新
或稍后] K --> L[停代理退应用
拉起安装向导] style A fill:#e8f4f8,stroke:#1a6b8a,stroke-width:2px style B fill:#d6eaf8,stroke:#1a6b8a,stroke-width:2px style C fill:#ebdef0,stroke:#8e44ad,stroke-width:2px style D fill:#fadbd8,stroke:#c0392b,stroke-width:2px style E fill:#ebdef0,stroke:#8e44ad,stroke-width:2px style F fill:#fdebd0,stroke:#b7950b,stroke-width:2px style G fill:#ffecd6,stroke:#e67e22,stroke-width:2px style H fill:#d6eaf8,stroke:#1a6b8a,stroke-width:2px style I fill:#ebdef0,stroke:#8e44ad,stroke-width:2px style J fill:#fadbd8,stroke:#c0392b,stroke-width:2px style K fill:#d5f5e3,stroke:#27ae60,stroke-width:2px style L fill:#d5f5e3,stroke:#27ae60,stroke-width:2px

全程用户可控:下载中可随时取消;「稍后」不打断当前工作,下次启动再提示;不想看到某个版本可点「忽略此版本」。

2. 清单获取:双源并行

更新元数据是一份 latest.json 清单(含最新版本号、更新说明、安装包文件名与 SHA256 等字段):

  • 双源:优先 GitHub Releases,不可达时回退 Gitee 同名发布,两份清单字节一致。
  • 并行拉取:两个源同时发起请求,先到先用,缩短检查耗时(0.1.2 引入)。
  • TTL 缓存:清单结果缓存 600 秒,避免频繁启动重复拉取;点「检查更新」时携带强制刷新参数可绕过缓存。
  • 尺寸上限:清单超过 512KB 直接拒绝,防止异常数据拖垮解析。

3. 清单签名验证

清单是整条链路的信任根,采用 Ed25519 数字签名保护:

  • 发布侧用私钥对清单的规范化载荷签名,应用内置对应公钥验签,私钥永不随应用分发。
  • 签名载荷有固定版本号的规范化格式,测试中有 golden 基准防跨实现回归。
  • URL 不参与签名——因此 GitHub 与 Gitee 两份清单共用同一签名;而清单里被签名的安装包 SHA256 把 URL 指向的内容绑死,改内容即验签失败。
  • 任何字段异常或签名不符,清单直接作废,不弹更新提示——宁可漏报也不误装。

4. 安装包下载与完整性校验

环节 行为
边下边算 下载流式计算 SHA256,与签名清单中的值比对
落盘位置 只写入用户数据目录 %APPDATA%\a4api\updates\<版本>\,不碰系统临时目录
二次校验 点击「立即更新」时对磁盘上的文件再次计算比对,防止落盘后被篡改
尺寸上限 安装包上限 300MB,超限拒绝
进度反馈 前端实时显示进度,可取消

5. 防降级与预发布策略

版本比较不是简单的字符串判断,而是逐段数值比较:

  • 候选版本必须严格高于当前运行版本才会提示,重放旧清单无法诱导降级安装。
  • 清单可声明 min_version 下限:当前版本过旧(低于下限)时不提供增量更新提示,避免跳版本升级出问题。
  • 预发布版本只对「当前运行的也是预发布版」的用户提示,正式版用户不会被推送试验性版本。

6. 忽略版本与状态持久化

「忽略此版本」与已下载待安装的状态持久化在数据目录的 update_state.json 中(原子写入,崩溃不损坏):被忽略的版本号进入列表不再提示,除非将来出现更新的版本。

7. 应用更新:最后一步

确认「立即更新」后的动作序列经过精心安排:

graph LR A[点击立即更新] --> B[对磁盘安装包
二次校验] B --> C[停止本地翻译代理] C --> D[应用自动退出
释放单实例锁] D --> E[拉起 Inno Setup
安装向导] E --> F[覆盖安装完成
配置数据库密钥全保留] style A fill:#e8f4f8,stroke:#1a6b8a,stroke-width:2px style B fill:#ebdef0,stroke:#8e44ad,stroke-width:2px style C fill:#ffecd6,stroke:#e67e22,stroke-width:2px style D fill:#fdebd0,stroke:#b7950b,stroke-width:2px style E fill:#d6eaf8,stroke:#1a6b8a,stroke-width:2px style F fill:#d5f5e3,stroke:#27ae60,stroke-width:2px

先停代理是为了不让旧代理进程占用端口与旧文件;主动退出释放单实例互斥体是安装器顺利覆盖的前提。升级完成后首次启动,配置方案、加密密钥与备份完整保留,无需重新配置。

8. 传输白名单

所有更新相关请求仅走 HTTPS,且每次重定向都逐跳校验目标主机是否在白名单内:github.comgitee.com、两个 *.githubusercontent.com 对象存储域与 *.gitee.com。跳转到任意其他域名的重定向会被直接拦截,压缩中间人劫持的操作空间。

9. 常见问题

问:为什么我点了检查更新却一直提示已是最新?

答:清单结果有 10 分钟缓存,刚发的新版可能还没到缓存过期时间,稍后再查或等待下次启动时的静默检查;若你确认 Release 页已有新版仍看不到,可能是网络无法访问 GitHub 且 Gitee 回退也失败,见下一问。

问:公司网络访问不了 GitHub 怎么办?

答:清单与下载都会自动尝试 Gitee 镜像源,通常无感知完成;两者都不可达时会报网络错误,可在能访问时再检查更新,或从 Release 页手动下载安装包覆盖安装,效果一致。

问:更新会丢我的配置和 API Key 吗?

答:不会。配置方案、DPAPI 加密的密钥库、切换备份都在 %APPDATA%\a4api\ 用户数据目录中,覆盖安装不动它,详见安装升级与卸载

问:「忽略此版本」之后后悔了怎么办?

答:被忽略的只是那一个版本号;当更新的版本发布后会正常提示。若想立刻收到该版本提示,也可以从 Release 页手动下载安装,或在能触达更新弹窗的场景下等待更高版本。