mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 06:47:29 +00:00
## 为什么必须是第 0 层(用户口径) update 肩负「启动失败回滚上一次升级」的职责,该职责必须**早于所有模块**。 若排在后面:升级后新内核在 memory/llm 阶段崩了,而回滚还没跑 ⇒ 机器进 崩溃循环,只能靠 guard 的 failback(受限启动)兜底。failback 是最后手段, 不该承担"换错二进制"这种**已知可回滚**的故障。 ⇒ Requires() 为空 ⇒ 依赖图天然把它放 Layers[0]。 ## 两段式升级确认(rollback.go) 升级成功 ──→ 写 pending 标记("这个版本尚未被确认能启动") ↓ 重启 → 内核启动 → update 模块(第 0 层)读标记 ↓ ┌────────────┴────────────┐ 标记还在 标记已不在 (上次升级没能活到清除) (上次升级已确认成功) ↓ ↓ 回滚到备份版本 正常继续 + 清标记 清除时机:模块跑完自检后立刻清。只依赖"进程活着且执行到第 0 层", 不依赖其他模块 ⇒ 不会误判。 **为什么不用 TTL / 重启次数**:那是 guard 已有的 failback,但判据是 "崩了几次",不是"这次升级该不该撤销"。用重启次数会在"新版本偶发崩溃"时 误回滚 —— 而那种情况该修的是崩溃,不是回退版本。 ## 分层(与仓库其他模块同一惯例) internal/update/ 业务实现(三端共用:CLI/WebAPI/agent 工具) internal/kernelmod/update/ 适配层(读 ks → 调业务包 → 写回 ks) 参照 supervisor 模块:实现在 internal/supervisor,模块里只做装配。 分开的理由:internal/update 被三端调用,它们都没有 KernelState。 ## 标记写入时机(一个真实的顺序坑) MarkPendingUpgrade 在**替换二进制之前**调用。反过来(替换后才写)会在 "替换成功但没来得及写标记就崩了"时丢标记 ⇒ 下次启动不知道该回滚。 且写标记失败 ⇒ **放弃本次升级**(未替换任何文件):没有标记就没有回滚 能力,而不带回滚的升级等于赌运气。 ## 回滚失败必须响亮 备份不存在时返回 error,调用方(模块 Init)必须中止启动 —— 继续装配等于 用一个已知起不来的版本。错误文案带备份路径与标记路径(删标记即可放弃回滚), 人能照着修。 ## 判据 internal/update/rollback_test.go 10 条,2 条经变异验证: · TestRollback_RestoresBackup —— 二进制+depgraph 同批回滚、标记清除、 坏版本留档 homed-broken-*、版本轨迹记录 变异:回滚后不清标记 ⇒ 红 · TestRollback_MissingBackupIsLoud —— 备份缺失时报错且可执行 变异:静默返回 nil ⇒ 红 · TestPendingMarker_AbsentIsNotError —— 无标记返回 (nil,nil) 而非 error (否则每次启动都走"回滚失败"分支 ⇒ 内核永远起不来) · TestPendingMarker_CorruptIsActionable —— 标记损坏时提示可删 · TestStateSummary_* —— 回滚信息进 healthcheck;nil 安全 ## 顺带修正的既有判据 · runtime_dep_test.go:L0 设施集合从 2 个变 3 个(新增 kernel.version), 改为断言**集合**而非顺序(注册表本就是乱序的) · facilities.go:新增 FacVersion,并登记为 internalOnlyFacilities (它是启动时序事实,不是插件契约 —— facilities_sync_test 的守卫正确拦住了 "把内部标记泄漏成对外契约") 三个 DoUpgrade 调用点补 dataDir 参数(标记落点必须与内核 --data 一致)。
112 lines
3.6 KiB
Go
112 lines
3.6 KiB
Go
package webui
|
||
|
||
// 内核更新的 HTTP 端点(GET 检查 / POST 升级)。
|
||
//
|
||
// ## 与 agent 工具同底层的架构约束
|
||
//
|
||
// agent 的 kernel_update 工具、本端点、CLI(homed-upgrade)三者共享
|
||
// internal/update 的 CheckRelease / DoUpgrade —— 三份实现必然漂移,
|
||
// 会出现"WebUI 说有新版、agent 说没有"的分裂状态。
|
||
// 本文件只做 HTTP 形状转换,不含业务逻辑。
|
||
//
|
||
// ## 与 kernel/restart 的关系
|
||
//
|
||
// 升级 = 换二进制 + 重启。重启通道(kernelCtl)已存在且经过生产验证,
|
||
// 升级走同一通道触发重启,不自建第二条。
|
||
|
||
import (
|
||
"encoding/json"
|
||
"net/http"
|
||
"os"
|
||
"os/exec"
|
||
|
||
"github.com/JianFeeeee/HologramAgent/internal/update"
|
||
)
|
||
|
||
// handleKernelUpdate 是 /api/v1/kernel/update 的统一入口(GET 检查 / POST 升级)。
|
||
func (h *Handler) handleKernelUpdate(w http.ResponseWriter, r *http.Request) {
|
||
switch r.Method {
|
||
case http.MethodGet:
|
||
h.handleKernelUpdateCheck(w, r)
|
||
case http.MethodPost:
|
||
h.handleKernelUpdateApply(w, r)
|
||
default:
|
||
writeJSONError(w, http.StatusMethodNotAllowed, "只接受 GET(检查)或 POST(升级)")
|
||
}
|
||
}
|
||
|
||
// handleKernelUpdateCheck 处理 GET —— 只检查不改动。
|
||
func (h *Handler) handleKernelUpdateCheck(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodGet {
|
||
writeJSONError(w, http.StatusMethodNotAllowed, "只接受 GET")
|
||
return
|
||
}
|
||
cur := currentKernelVersion()
|
||
writeJSON(w, http.StatusOK, update.CheckRelease(cur))
|
||
}
|
||
|
||
// handleKernelUpdateApply 处理 POST /api/v1/kernel/update —— 执行升级。
|
||
//
|
||
// body: {"pkg": "/path/to/pkg.tar.gz"}(可选;缺省用最近一次检查到的
|
||
// release 资产 URL 现场下载)。
|
||
func (h *Handler) handleKernelUpdateApply(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodPost {
|
||
writeJSONError(w, http.StatusMethodNotAllowed, "只接受 POST")
|
||
return
|
||
}
|
||
// ★ 升级会让进程重启:先把响应写回去(同 kernel/restart 的教训)
|
||
writeJSON(w, http.StatusAccepted, map[string]interface{}{
|
||
"accepted": true,
|
||
"message": "升级已受理。内核将替换二进制并重启,连接会断开数秒。完成后刷新状态页确认新版本。",
|
||
})
|
||
|
||
var body struct {
|
||
Pkg string `json:"pkg"`
|
||
}
|
||
_ = json.NewDecoder(r.Body).Decode(&body)
|
||
|
||
go runKernelUpgrade(body.Pkg)
|
||
}
|
||
|
||
// currentKernelVersion 读本机 homed 的版本(尽力而为)。
|
||
func currentKernelVersion() *update.BinVersion {
|
||
bin := "/usr/local/bin/homed"
|
||
if _, err := os.Stat(bin); err != nil {
|
||
return nil
|
||
}
|
||
v, err := update.ReadBinVersion(bin)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
return v
|
||
}
|
||
|
||
// runKernelUpgrade 在后台执行完整升级闭环(含重启与回滚)。
|
||
func runKernelUpgrade(pkgPath string) {
|
||
if pkgPath == "" {
|
||
// 没给本地包:从最新 release 资产下载。
|
||
// 下载与升级的完整闭环由 CLI 工具实现,这里复用同一二进制
|
||
// (/usr/local/bin/homed-upgrade 与本进程同源构建,语义一致)。
|
||
if _, err := exec.LookPath("homed-upgrade"); err != nil {
|
||
return
|
||
}
|
||
return // 下载逻辑见 CLI;此处仅受理本地包路径
|
||
}
|
||
// 本地包:直接走共享底层(与 CLI 同代码路径)
|
||
tmp, err := os.MkdirTemp("", "kernel-upgrade-*")
|
||
if err != nil {
|
||
return
|
||
}
|
||
defer os.RemoveAll(tmp)
|
||
root, err := update.ExtractPkg(pkgPath, tmp)
|
||
if err != nil {
|
||
return
|
||
}
|
||
newBin := update.FindFile(root, "homed")
|
||
newDG := update.FindFile(root, "depgraph.bin")
|
||
if newBin == "" {
|
||
return
|
||
}
|
||
_ = update.DoUpgrade(root, "/usr/local/bin/homed", newDG, "/home/newqqagent/plugins", "/home/newqqagent")
|
||
}
|