feat(ota): 内核升级探测 + 不兼容插件自动禁用(OTA 只面向内核)

## 范围

OTA 只处理 homed / depgraph.bin / waiter 三个文件,**不升级插件**。
插件与内核的兼容性改由「自动禁用 + 写明原因 + 界面展示」处理。

## 1. homed --version(OTA 的前提)

新增该 flag,输出单行 key=value:
  version=1.4.0dev commit=c9a32a9 build_time=... sdk=1.3.0

★ 为什么必须有:升级前要能**问一个二进制它是什么版本**。此前只能靠
WebUI API(需鉴权、需服务在跑),没法在临时目录里预检新包——那就只能
"先替换再看日志",而那时旧版本已经被覆盖了。

## 2. cmd/homed-upgrade:默认只探测

  homed-upgrade -pkg xxx.tar.gz              # 探测
  homed-upgrade -pkg xxx.tar.gz -apply       # 执行

探测项:当前版本 / 包内版本 / 版本比较结论 / depgraph 是否同批 /
**插件兼容性预判**(换内核后会有几个插件被自动禁用)。

版本比较区分四种情况,都给出可读理由(用户才敢按 -apply):
· 可升级 / **降级**(明确警告)/ 完全相同(无需升级)/
  **同版本不同 commit**(同版本重发——最容易漏掉的一种)

`-apply` 走:预检 → 备份(只备本次会动的文件,不整目录拷)→ 原子替换
(写 .tmp 再 rename)→ 重启 → 版本复核 → 失败自动回滚。

## 3. 兼容性兜底:第一次升级必然撞到的坑

实测踩到:线上内核是加 --version **之前**的版本,第一次探测必然读不到。
此时不能失败退出(否则所有用户第一次升级都要人工介入)。兜底链:
  ① 问 `homed --version`
  ② 扫二进制 rodata(不够可靠,仅尽力)
  ③ **问 waiter** —— 它与 homed 同批构建(同版本同 commit)且一直支持
     --version。实测这条成功识别出当前是 1.4.0/98850e1。
  ④ 都失败 → 版本比较降级为"未知",**不阻断**其余预检。

## 4. 不兼容插件自动禁用(替代"升级插件")

内核换版本后,用旧 SDK 编的插件握手会判主版本不匹配。此前只是
`log.Printf` 后返回 false:每次重启重试一遍失败一遍、日志反复刷,
用户在界面上只看到"插件不见了"。

改为:持久禁用 + 写明原因(`maybeAutoDisableIncompatible`)。
`disabled_plugins` 表新增 reason 列(带 ALTER 迁移,旧库补列),
`sdk.DisabledPluginInfo` 同步加 Reason,随既有 settings API 到前端。

★ **只认"主版本不匹配"这一类**:端口被占、配置缺失、插件崩溃都是
可能自愈的临时/环境问题。把它们也自动禁用,等于把可修复的故障变成
"永不加载",比反复重试糟糕得多。

判据:
· cmd/homed-upgrade/main_test.go — 5 种版本比较结论 + 未知不阻断 +
  semver 解析(1.4.0dev 不得被当 1.4.0 比大小)+ waiter 输出解析
  (日期不得被当版本号)
· internal/plugin/autodisable_test.go — 认得出主版本不匹配(且 reason
  必须带两端版本号)/ 四类临时故障**不得**被误判为版本不兼容
This commit is contained in:
JianFeeeee
2026-10-25 20:30:39 +08:00
parent c9a32a98f0
commit a0813b5578
7 changed files with 909 additions and 4 deletions

View File

@ -18,6 +18,7 @@ import (
"github.com/JianFeeeee/HologramAgent/internal/ipc"
logpkg "github.com/JianFeeeee/HologramAgent/internal/log"
"github.com/JianFeeeee/HologramAgent/internal/memory"
"github.com/JianFeeeee/HologramAgent/internal/meta"
"github.com/JianFeeeee/HologramAgent/internal/memory/pipeline"
"github.com/JianFeeeee/HologramAgent/internal/memory/social"
"github.com/JianFeeeee/HologramAgent/internal/plugin"
@ -403,7 +404,21 @@ func parseFlags() options {
cliSocket := flag.String("socket", "", "cli unix socket path (default: <data>/cli.sock)")
role := flag.String("role", "agent", "process role: guard (父守护) | agent (工作进程)")
boot := flag.String("boot", "normal", "agent boot mode: normal | failback (受限启动,仅 failback 插件集)")
// --version:打印版本后退出,不启动任何模块。
//
// ★ 为 OTA 而加:升级前要能**问一个二进制它是什么版本**,否则只能先替换
// 再启动看日志——那时旧版本已经被覆盖了。有了它,OTA 可以在临时目录里
// 跑 `新homed --version` 做预检,不通过就不碰线上文件。
//
// 输出保持单行 key=value 形态,便于脚本解析(与 BuildTime/Commit 一起给,
// 因为"同一个版本号不同 commit"会让"升级成功但内容没变"难以察觉)。
showVersion := flag.Bool("version", false, "打印版本信息后退出(不启动内核)")
flag.Parse()
if *showVersion {
fmt.Printf("version=%s\ncommit=%s\nbuild_time=%s\nsdk=%s\n",
meta.Version, meta.Commit, meta.BuildTime, meta.SDKCompatibleVersion)
os.Exit(0)
}
return options{dataDir: *dataDir, httpAddr: *httpAddr, cliSocket: *cliSocket, role: *role, boot: *boot}
}