Files
HomeAgent/internal/update/semver.go
JianFeeeee 475aac867b refactor(update): 内核与插件更新合并为 update 子系统(三端共享一个底层)
## 为什么合并(用户口径:更新部分应独立为子系统)

之前 kernelupgrade / pluginupdate 两个包,但它们共享三样东西,
分开写就要各复制一份,然后慢慢不一致:

① **版本比较** —— 合并前共 6 个函数做同一件事:
   kernel 侧 ParseSemver / CmpSemver / versionNewer / trimTagV
   plugin 侧 trimV / isNewer / semverNums(还自带一份正则)
   且**行为不一致**:kernel 侧只挡 len<2,"1.4" 漏过去被填成 1.4.0;
   plugin 侧的正则要求两段所以也漏。两份都把 "1.4" 当合法版本。

② **两阶段交互**(检查 → 确认 → 执行)—— 同一套用户口径。

③ **失败要响亮** —— 都不静默降级。

## 收敛结果

internal/update/
  doc.go     子系统头:职责边界、三端共享契约、为何不分家
  semver.go  ParseSemver / CmpSemver / Newer / TrimTag(**唯一一份**)
  kernel.go  内核升级闭环(解包→校验→备份→替换→重启→复核→回滚)
  release.go GitHub Release 查询
  plugin.go  插件升级(查仓库→拉源码→hmapdev 编译→产物)

三端共享:
  agent 工具(plugin_update / kernel_update)  ┐
  WebAPI(/plugins/update、/kernel/update)    ├─ internal/update
  CLI(homed-upgrade)                         ┘

入口层只做参数解析与结果呈现,不含业务逻辑 —— 否则会出现
"agent 说没有新版、WebUI 却升了级"的分裂状态。

## 合并时抓到的两个真实 bug

① **ParseSemver 只挡 len<2** ⇒ "1.4" 被零值填成 1.4.0,
   「1.4 → 1.4.0」被判完全相同(说"无需升级"),而两者内容未必相同。
   现在要求恰好三段。

② **不认 v 前缀** ⇒ GitHub release tag 全是 v1.4.0 形态,
   CLI 对"当前 v1.4.0 vs 包内 v1.4.0"判不可比、只能退回字符串比较。
   现在 TrimTag 去 v 前缀后再比。

## 新增入口

· plugin_update(agent 工具)+ GET/POST /plugins/update
  —— 两阶段:apply=false 只检查(列出 updatable / no_repo 两类,
     后者是"未声明仓库、无法自动更新"的内置/私有插件);
     apply=true 需显式 plugins=all 或名单(不默认全升)
· kernel_update(agent 工具)+ GET/POST /kernel/update
  —— 两阶段同构;apply 只收本机包路径(URL 下载是 CLI 职责,
     agent 不该在推理中发起长下载)

判据:
· internal/update: Newer / TrimTag / ParseSemver 的边界(含上述两个 bug 的回归)
· pluginmgr 入口层 5 条:检查形状、apply 必须显式给范围、
  kernel apply 必须给 pkg 且指向 homed-upgrade、拒绝目录当包
2026-10-26 01:05:09 +08:00

113 lines
3.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package update
// 版本号工具:**子系统内唯一一份**语义版本实现。
//
// ## 为什么必须收敛到一份(合并前的教训)
//
// 合并前有 6 个函数散在两个包里做同一件事:
//
// internal/kernelupgrade: ParseSemver / CmpSemver / versionNewer / trimTagV
// internal/pluginupdate: trimV / isNewer / semverNums
//
// 三份实现的差异是真实的 bug 温床:
// · kernel 侧只挡 len<2 ⇒ "1.4" 被当成 1.4.0(已修)
// · plugin 侧用 map+正则另写一套,行为与 kernel 侧不一致
// · trimV / trimTagV 名字不同、逻辑相同
//
// 版本比较错一次的后果很具体:把"已是最新"说成"有新版"(白编一场),
// 或把"有新版本"说成"无需升级"(用户永远拿不到修复)。
//
// ⇒ 现在只导出一组:ParseSemver / CmpSemver / Newer / TrimTag。
import (
"fmt"
"regexp"
)
// Semver 是解析后的三段式版本号。
type Semver struct{ Major, Minor, Patch int }
var pureSemverRe = regexp.MustCompile(`^v?(\d+)\.(\d+)\.(\d+)$`)
// ParseSemver 解析纯 X.Y.Z(可带 v 前缀)。
//
// 返回 nil 表示"不是可比的三段式版本"——调用方应据此降级
// (如退回字符串比较、或明确告知无法比较),而不是猜一个值。
//
// ★ 必须恰好三段。早期实现只挡 len<2,于是 "1.4" 漏过去、
// 缺失的 patch 被零值填成 1.4.0 ⇒ "1.4 → 1.4.0" 被判成完全相同,
// 进而说"无需升级",而两者内容未必相同。
func ParseSemver(s string) *Semver {
m := pureSemverRe.FindStringSubmatch(s)
if m == nil {
return nil
}
var v Semver
if _, err := fmt.Sscanf(m[1]+"."+m[2]+"."+m[3], "%d.%d.%d", &v.Major, &v.Minor, &v.Patch); err != nil {
return nil
}
return &v
}
// CmpSemver 比较两个已解析版本:<0 / 0 / >0。
//
// 任一侧为 nil 时返回 0(不可比)——调用方必须先判 nil,
// 因为"无法比较"与"相等"是两回事,混同会让升级判定失效。
func CmpSemver(a, b *Semver) int {
if a == nil || b == nil {
return 0
}
switch {
case a.Major != b.Major:
return a.Major - b.Major
case a.Minor != b.Minor:
return a.Minor - b.Minor
default:
return a.Patch - b.Patch
}
}
// Newer 判断版本串 a 是否严格新于 b。
//
// 语义要点:
// - 只认纯三段式;任一侧不可解析 ⇒ false(不猜)
// - "1.4.0" 与 "1.4.0dev" 视为**相同**(TrimTag 去 dev 后比)
// —— 开发分支通常领先于同名正式版,不误报"有新版"
// - 空串 b 表示"本地无版本信息",此时 a 非空即算新
func Newer(a, b string) bool {
if b == "" {
return a != ""
}
pa, pb := ParseSemver(TrimTag(a)), ParseSemver(TrimTag(b))
if pa == nil || pb == nil {
return false
}
return CmpSemver(pa, pb) > 0
}
// TrimTag 把版本串归一成可比形态:去 v 前缀、去 dev/预发布后缀。
//
// 处理 "v1.4.4" → "1.4.4"、"1.4.0dev" → "1.4.0"。
// 非纯三段式(如 "1.4.4-rc1"、"release1")原样返回,
// 由 ParseSemver 判为不可比 —— 这里不做有损猜测。
func TrimTag(s string) string {
out := s
if len(out) > 0 && (out[0] == 'v' || out[0] == 'V') {
out = out[1:]
}
// 去 dev 及其后(1.4.0dev / 1.4.0dev.1)
if i := indexOf(out, "dev"); i >= 0 {
out = out[:i]
}
return out
}
func indexOf(s, sub string) int {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return i
}
}
return -1
}