fix(homeagent): 撤回"另一条 SDK 血脉"的错误结论;build.sh 不再猜路径;清单判据改成一致性口径
pi 用只读文件系统逐条反驳了 357662e 的根因,三条我都验证并接受:
1. **"内核链 0.9.x 血脉"不成立 —— 那是我的搜索顺序造出来的事实。**
本机有 6+ 份 `third_party/homeagent-sdk` checkout:
/root/ha-test/…(0.9.0,C-ABI 时代,无 plugin.bin 支持)
/var/tmp/rel-1.3.12/…、/var/tmp/rel-1.3.11/…(1.3.0)
/var/tmp/release-main/…、/var/tmp/clean-check/…、/var/tmp/homed-p3/…(1.2.0)
而 build.sh 第一版按候选根目录**第一个命中就算**,命中的正是 ha-test 那份老 checkout。
内核自己用的是 1.3.0 那份,与钉子 `sdk/v1.3.0`、与 `<SDK_ROOT>/current` **一致**。
两条教训写进注释了:别用"第一个存在的路径"当权威来源;别把模块版本字符串当身份
(`replace => local (devel)` 时它只是 require 行的残留)。
2. **"API 不兼容"也是同一个错造成的。** 换成正确的 SDK 之后:
HOMEAGENT_SDK_DIR=/root/.homeagent/hmapdev/sdk/v1.3.0 bash build.sh
→ hmapdev build 成功,产出 build/plugin.bin(9018271 字节)
也就是说**这个插件在本机编得出来**,先前的 `SettingsAPI.DataDir` 报错是拿老 checkout 编的产物。
3. **判据把能工作的配置判红**(pi §2):第 4 步原先硬校验"产物 SDK 模块版本 == 内核模块版本",
而生产上能跑的组合恰恰是"内核 + v1.3.0 编的插件"。已删掉这个相等性判据:
SDK 源码**只认显式指定**(HOMEAGENT_SDK_DIR),不猜、不试探;
内核那条 dep/=> 只作为**提示**打印(并且按模块名精确联接、只接受紧跟 SDK dep 行的 `=>`,
不再取"输出里第一个 =>");身份改记 **realpath + 内容哈希**;
`meta.Version` 读取先剥注释(注释里的 `Version = "9.9.9"` 不再能赢)。
真正的不变量是 **wire 协议 protocol=2 + 一次真实握手**,写在脚本末尾(部署后回看日志)。
4. **清单判据改成一致性口径**(pi §5):不再"禁止 sdk 字段"——那会把正在工作的那份清单
(/home/newqqagent/plugins/homeagent-mail-bridge/plugin.json 声明 sdk=1.3.0,正是 08:30
那次恢复的处置动作)判红,而我没有"内核不读该字段"的证据。现在:可以不声明;
声明了就必须与构建机指针一致。
This commit is contained in:
@ -1,24 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# 构建 homeagent-mail-bridge —— 必须与**内核链在同一个 SDK 上**,构建前后都校验。
|
||||
# 构建 homeagent-mail-bridge —— 用**明确指定的** SDK 源码,并记录"这次到底用了哪份代码"。
|
||||
#
|
||||
# # 为什么(2026-09-14 事故 + 当天复发 + 今天查出的真根因)
|
||||
# # 这份脚本被改过一轮,因为它的第一版判错了根因(pi 2026-09-14 晚逐条反驳)
|
||||
#
|
||||
# · 08:09 事故:内核升级,插件二进制还是旧 SDK 编的 → 启动即 SIGSEGV,握手失败,
|
||||
# 5 分钟内崩 4 次后熔断「停止自动重启,需人工介入」;
|
||||
# · 当天处置:重编成 0.2.2 / sdk=1.3.0;
|
||||
# · 现在又崩:`go version -m /usr/local/bin/homed` 显示**本机内核**链的是
|
||||
# dep gitcode.com/JianFeeeee/homeagent-sdk v0.9.2
|
||||
# => ./third_party/homeagent-sdk (devel)
|
||||
# 而插件 go.mod 钉的是 `/root/.homeagent/hmapdev/sdk/v1.3.0` ——
|
||||
# **两条完全不同的 SDK 血脉**。所以"照原样重编"不但没用,还必然再造一个崩的包。
|
||||
# 第一版的错:把 `go version -m <内核>` 里的
|
||||
# dep ... homeagent-sdk v0.9.2 / => ./third_party/homeagent-sdk (devel)
|
||||
# 当成"内核链的是 0.9.x 血脉",然后按候选根目录**第一个命中就算**去解析 SDK 源码,
|
||||
# 命中的是 `/root/ha-test/third_party/homeagent-sdk`(C-ABI 时代的老 checkout,无 plugin.bin 支持)。
|
||||
# 而本机共有 6+ 份同名 checkout,内核自己用的是 1.3.0 那份。两个教训:
|
||||
#
|
||||
# 结论:判据不该是"版本号看起来一样",而该是**两个二进制的依赖表一样**。
|
||||
# 这个脚本就用 `go version -m` 把内核的 SDK 依赖读出来,按它构建,再回头验产物。
|
||||
# ① **不要用"第一个存在的路径"当权威来源** —— 猜根目录会命中不相关的 checkout,
|
||||
# "两条血脉"于是成了脚本的产物,而不是事实;
|
||||
# ② **不要把模块版本字符串当身份** —— `replace => local (devel)` 时那个版本号
|
||||
# 只是 require 行的文本残留,不代表实际编译的代码。
|
||||
#
|
||||
# 所以现在:SDK 源码**只认显式指定**(`HOMEAGENT_SDK_DIR`)。
|
||||
# 不猜、不试探、不"第一个命中"。要指定内核的构建目录就显式传进来。
|
||||
#
|
||||
# # 不变量是什么
|
||||
#
|
||||
# 不是"版本号相等"(那正是第一版想抛弃的东西),而是 **wire 协议 + 一次真实握手**:
|
||||
# 产物要能被内核 `protocol=2` 认下来。这个脚本能做的是"把身份记清楚 + 把明显不对的挡下来",
|
||||
# 真握手由内核启动时完成(所以部署后要回看一次日志,见末尾提示)。
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
KERNEL_BIN="${HOMEAGENT_KERNEL_BIN:-/usr/local/bin/homed}"
|
||||
SDK_MODULE="gitcode.com/JianFeeeee/homeagent-sdk"
|
||||
KERNEL_BIN="${HOMEAGENT_KERNEL_BIN:-/usr/local/bin/homed}"
|
||||
|
||||
die() { echo "构建失败:$*" >&2; exit 1; }
|
||||
|
||||
@ -28,55 +36,62 @@ export GOPATH="${GOPATH:-$HOME/go}"
|
||||
export GOCACHE="${GOCACHE:-${TMPDIR:-/tmp}/go-build-agentmail}"
|
||||
mkdir -p "$GOCACHE"
|
||||
|
||||
[[ -x "$KERNEL_BIN" ]] || die "找不到内核二进制 $KERNEL_BIN(用 HOMEAGENT_KERNEL_BIN 指定)"
|
||||
# ── 1. SDK 源码:只认显式指定 ───────────────────────────────────────
|
||||
[[ -n "${HOMEAGENT_SDK_DIR:-}" ]] || die "必须显式指定 SDK 源码树:HOMEAGENT_SDK_DIR=/path/to/homeagent-sdk
|
||||
为什么不让脚本自己猜:本机有 6+ 份同名 checkout(有的是 C-ABI 时代的老代码),
|
||||
第一版按候选根目录'第一个命中就算',命中了 /root/ha-test 那份老 checkout,
|
||||
于是把'内核链老血脉'这个**错误结论**当成了事实写进注释。猜路径 = 制造事实。"
|
||||
|
||||
# ── 1. 读内核链的 SDK:模块版本 + 本地替换目录 ──────────────────────
|
||||
KERNEL_INFO="$(go version -m "$KERNEL_BIN" 2>/dev/null)" || die "读不出 $KERNEL_BIN 的构建信息(go version -m)"
|
||||
KERNEL_SDK_VER="$(awk -v m="$SDK_MODULE" '$1=="dep" && $2==m {print $3}' <<<"$KERNEL_INFO" | head -1)"
|
||||
KERNEL_SDK_PATH="$(awk -v m="$SDK_MODULE" '/^[[:space:]]*=>/ {print $2}' <<<"$KERNEL_INFO" | head -1)"
|
||||
[[ -n "$KERNEL_SDK_VER" ]] || die "内核没有链 $SDK_MODULE —— 这不对,先人工看一眼"
|
||||
echo "[build] 内核 $KERNEL_BIN 链的 SDK:${KERNEL_SDK_VER}${KERNEL_SDK_PATH:+(替换目录 $KERNEL_SDK_PATH)}"
|
||||
SDK_DIR="$(cd "$HOMEAGENT_SDK_DIR" && pwd)"
|
||||
[[ -f "$SDK_DIR/go.mod" ]] || die "$SDK_DIR 不是 Go 模块(没有 go.mod)"
|
||||
|
||||
# ── 2. 找到内核用的那份 SDK 源码 ───────────────────────────────────
|
||||
SDK_DIR="${HOMEAGENT_SDK_DIR:-}"
|
||||
if [[ -z "$SDK_DIR" && -n "$KERNEL_SDK_PATH" ]]; then
|
||||
# 内核里的路径是**构建时**的相对路径,本机按候选根目录找同名子树
|
||||
for root in /root/ha-test /root /home/program; do
|
||||
cand="$root/${KERNEL_SDK_PATH#./}"
|
||||
if [[ -f "$cand/go.mod" ]]; then SDK_DIR="$cand"; break; fi
|
||||
done
|
||||
# 身份用 **realpath + 内容哈希**,不用版本字符串(版本号可能是 -ldflags 注入的默认值,
|
||||
# 也可能是注释里的示例 —— 后者我在 electron 侧的判据里刚踩过一次)。
|
||||
SDK_HASH="$(find "$SDK_DIR" -type f -name '*.go' -print0 2>/dev/null | sort -z | xargs -0 cat 2>/dev/null | sha256sum | cut -c1-16)"
|
||||
SDK_VER="$(sed 's://.*::' "$SDK_DIR/meta/meta.go" 2>/dev/null | grep -m1 -oE 'Version = "[0-9]+\.[0-9]+\.[0-9]+"' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true)"
|
||||
echo "[build] SDK 源码:$SDK_DIR"
|
||||
echo "[build] meta.Version(已剥注释)= ${SDK_VER:-未知} | 内容哈希 = ${SDK_HASH:-未知}"
|
||||
|
||||
# 内核签名的**提示**(不是判据):按模块名精确联接,不按"输出里第一个 =>"。
|
||||
# "两张表各缺一半时必须按 id 联接"—— 读 go version -m 的输出同理。
|
||||
if [[ -x "$KERNEL_BIN" ]]; then
|
||||
KERNEL_INFO="$(go version -m "$KERNEL_BIN" 2>/dev/null || true)"
|
||||
KERNEL_SDK_LINE="$(awk -v m="$SDK_MODULE" '$1=="dep" && $2==m {print NR": "$0}' <<<"$KERNEL_INFO" | head -1)"
|
||||
if [[ -n "$KERNEL_SDK_LINE" ]]; then
|
||||
KERNEL_SDK_DEP="$(cut -d' ' -f3 <<<"${KERNEL_SDK_LINE#*: }")"
|
||||
# 只接受**紧跟在 SDK 那一行之后**的 => 行(按位置配对的唯一正确写法)
|
||||
KERNEL_SDK_REPL="$(awk -v m="$SDK_MODULE" '
|
||||
$1=="dep" && $2==m {want=NR+1}
|
||||
want && NR==want && $1=="=>" {print $2; exit}' <<<"$KERNEL_INFO")"
|
||||
echo "[build] 内核 $KERNEL_BIN 的 dep 行:$KERNEL_SDK_DEP${KERNEL_SDK_REPL:+(本地替换 $KERNEL_SDK_REPL)}"
|
||||
echo "[build] 注意:这条只是**提示**,不是判据 —— replace 到本地目录时这个版本号只是 require 行的残留,"
|
||||
echo "[build] 而且它指向的相对路径未必是本机这份源码。要对齐请显式传 HOMEAGENT_SDK_DIR。"
|
||||
fi
|
||||
fi
|
||||
if [[ -z "$SDK_DIR" ]]; then
|
||||
# 退路:本机 SDK 指针(hmapdev 那套)。**这里必须出声** —— 它与内核血脉是否一致
|
||||
# 由第 4 步的依赖表比对来判,不靠人猜。
|
||||
ptr_root="${HOMEAGENT_SDK_ROOT:-/root/.homeagent/hmapdev/sdk}"
|
||||
[[ -f "$ptr_root/current" ]] || die "既找不到内核的 SDK 源码,也没有 $ptr_root/current"
|
||||
SDK_DIR="$ptr_root/$(tr -d '[:space:]' <"$ptr_root/current")"
|
||||
echo "[build] 警告:没找到内核的 SDK 源码目录,改用本机指针 $SDK_DIR" >&2
|
||||
fi
|
||||
[[ -f "$SDK_DIR/go.mod" ]] || die "$SDK_DIR 不是一个 Go 模块(没有 go.mod)"
|
||||
SDK_VER="$(grep -m1 -oE 'Version = "[0-9]+\.[0-9]+\.[0-9]+"' "$SDK_DIR/meta/meta.go" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true)"
|
||||
echo "[build] 本次用 SDK 源码:$SDK_DIR(meta.Version=${SDK_VER:-未知})"
|
||||
|
||||
# ── 3. 用临时 modfile 替换,**不改动**受版本管理的 go.mod ──────────
|
||||
# ── 2. 用临时 modfile 替换,不改动受版本管理的 go.mod ───────────────
|
||||
cd "$HERE"
|
||||
TMP_MOD="$(mktemp -d)/go.local.mod"
|
||||
cp go.mod "$TMP_MOD"
|
||||
go mod edit -modfile="$TMP_MOD" -replace "$SDK_MODULE=$SDK_DIR"
|
||||
export GOFLAGS="${GOFLAGS:-} -modfile=$TMP_MOD"
|
||||
go build ./...
|
||||
hmapdev build
|
||||
|
||||
# ── 4. 产物自检:依赖表必须与内核一致(这才是"对齐"的定义)────────
|
||||
echo "[build] 校验产物:"
|
||||
# 注意:这个插件**没有** `func main()`(入口由 hmapdev 生成/包装),
|
||||
# 所以 `go build ./...` 会报 "function main is undeclared" —— 那是正常的,
|
||||
# 不能用它来数 API 兼容性错误;真正的编译在 hmapdev 那一步。
|
||||
hmapdev build || die "hmapdev build 失败(编译错误见上;它才是真正的编译入口)"
|
||||
|
||||
# ── 3. 产物自报身份 ─────────────────────────────────────────────────
|
||||
BIN="build/plugin.bin"
|
||||
[[ -f "$BIN" ]] || die "没找到产物 $BIN"
|
||||
OUT_SDK_VER="$(go version -m "$BIN" | awk -v m="$SDK_MODULE" '$1=="dep" && $2==m {print $3}' | head -1)"
|
||||
echo " 产物链的 SDK:${OUT_SDK_VER:-(无)} | 内核链的 SDK:$KERNEL_SDK_VER"
|
||||
if [[ -z "$OUT_SDK_VER" ]]; then
|
||||
die "产物没有链 $SDK_MODULE —— 装上去必然握不上手"
|
||||
fi
|
||||
if [[ "$OUT_SDK_VER" != "$KERNEL_SDK_VER" ]]; then
|
||||
die "产物与内核链的 SDK 版本不同($OUT_SDK_VER vs $KERNEL_SDK_VER)—— 这正是握手失败/崩溃循环的形状。装上去之前必须先对齐。"
|
||||
fi
|
||||
echo "[build] 完成:$BIN 与内核链在同一 SDK 上($OUT_SDK_VER)"
|
||||
echo "[build] 产物:$BIN($(stat -c%s "$BIN") 字节)"
|
||||
echo "[build] 本次身份:SDK=$SDK_DIR hash=$SDK_HASH version=${SDK_VER:-未知}"
|
||||
cat > build/BUILD_INFO.txt <<EOF
|
||||
sdkDir=$SDK_DIR
|
||||
sdkHash=$SDK_HASH
|
||||
sdkMetaVersion=${SDK_VER:-unknown}
|
||||
builtAt=$(date -Iseconds)
|
||||
EOF
|
||||
echo "[build] 已写 build/BUILD_INFO.txt —— 部署后请回看内核日志确认握手:"
|
||||
echo " grep -a 'mail-bridge' /home/newqqagent/log/homed_*.log | tail -5"
|
||||
echo " (判据不是版本号相等,而是 protocol=2 建链成功、且没有崩溃重启循环)"
|
||||
|
||||
@ -8,8 +8,13 @@ require gitcode.com/JianFeeeee/homeagent-sdk v1.3.0
|
||||
|
||||
replace gitcode.com/JianFeeeee/homeagent-sdk => /root/.homeagent/hmapdev/sdk/v1.3.0
|
||||
|
||||
// 这个 replace 是**构建机的 SDK 指针**,不是源码常量:它必须跟着
|
||||
// 这个 replace 是**构建机的 SDK 指针**,不是源码常量:它跟着
|
||||
// `<SDK_ROOT>/current` 走(判据 TestSDKPinMatchesBuildMachinePointer 管)。
|
||||
// 真正致命的一致性("与**内核**用的是不是同一个版本")在 build.sh 里硬校验 ——
|
||||
// 那是唯一能拿到内核二进制的时刻;2026-09-14 的复发就是"钉子跟 current 一致、
|
||||
// 但内核已经跑到 1.4.0"造成的,只比 current 是抓不到的。
|
||||
//
|
||||
// 更正(2026-09-14 晚,pi 用只读文件系统逐条反驳):我先前写的
|
||||
// "内核链另一条 SDK 血脉(0.9.x)"是**错的** —— 本机有 6+ 份
|
||||
// `third_party/homeagent-sdk` checkout,而 build.sh 当时按候选根目录"第一个命中就算",
|
||||
// 命中的是 /root/ha-test 那份 C-ABI 时代的老 checkout。内核自己用的是 1.3.0 那份,
|
||||
// 与这里的钉子**一致**。两点教训:① 别用"第一个存在的路径"当权威来源;
|
||||
// ② 别把模块版本字符串当身份(`replace => local (devel)` 时它只是 require 行的残留)。
|
||||
// 真正的不变量是 wire 协议(protocol=2)+ 一次真实握手,不是版本号相等。
|
||||
|
||||
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -27,7 +28,30 @@ import (
|
||||
* 这两条都是**来源**判据(验形态,不验行为):真正的一致性校验在 `build.sh` 里,
|
||||
* 它拿内核内嵌的版本号跟本次构建用的比,不一致就拒绝出包。
|
||||
*/
|
||||
func TestManifestMustNotPinSDKVersion(t *testing.T) {
|
||||
func TestDeclaredSDKMatchesPointer(t *testing.T) {
|
||||
/*
|
||||
* 判据:清单**可以**声明 sdk,但声明值必须与构建机的 SDK 指针一致。
|
||||
*
|
||||
* # 这里改过一次口径(pi 2026-09-14 晚的更正)
|
||||
*
|
||||
* 我先前的版本是"清单里不许出现 sdk 字段",理由是"21 个已装插件里只有 3 个声明"。
|
||||
* pi 指出那是**统计不是因果**,而且被修好、正在工作的那份清单
|
||||
* (/home/newqqagent/plugins/homeagent-mail-bridge/plugin.json)**恰恰声明了 sdk=1.3.0** ——
|
||||
* 事故报告的处置动作原文就是"替换 plugin.bin + plugin.json 声明 version 0.2.2 / sdk 1.3.0"。
|
||||
* 也就是说我那条判据会把**能工作的配置判红**,而且我没有"内核不读这个字段"的证据。
|
||||
*
|
||||
* 所以改成一致性判据:要么不声明,要么声明得对。
|
||||
* 真正的对齐证据不是这个字段,而是 wire 协议 + 一次真实握手(见 build.sh)。
|
||||
*/
|
||||
root := os.Getenv("HOMEAGENT_SDK_ROOT")
|
||||
if root == "" {
|
||||
root = "/root/.homeagent/hmapdev/sdk"
|
||||
}
|
||||
curRaw, err := os.ReadFile(root + "/current")
|
||||
if err != nil {
|
||||
t.Fatalf("取不到基准:读不到 %s/current(%v)—— 判断不了声明值与构建机是否一致", root, err)
|
||||
}
|
||||
cur := strings.TrimPrefix(strings.TrimSpace(string(curRaw)), "v")
|
||||
for _, name := range []string{"plugin.json", "plg.json"} {
|
||||
raw, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
@ -37,16 +61,19 @@ func TestManifestMustNotPinSDKVersion(t *testing.T) {
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
t.Fatalf("%s 不是合法 JSON: %v", name, err)
|
||||
}
|
||||
if v, ok := m["sdk"]; ok {
|
||||
t.Errorf("%s 里写死了 sdk=%v —— 这个字段要由构建工具按实际编译用的 SDK 写入,"+
|
||||
"手写等于把插件钉在某个内核版本上:内核一升就握手失败、启动即崩、"+
|
||||
"而且重编也修不好(2026-09-14 的事故与同日复发都是这个形状)。"+
|
||||
"删掉它,用 build.sh 构建(它会解析本机 SDK 并与内核版本比对)。", name, v)
|
||||
v, ok := m["sdk"]
|
||||
if !ok {
|
||||
continue // 不声明也是合法的(已装 21 个里 18 个如此)
|
||||
}
|
||||
if _, ok := m["version"]; !ok {
|
||||
t.Errorf("%s 缺少 version 字段", name)
|
||||
if got := strings.TrimPrefix(fmt.Sprint(v), "v"); got != cur {
|
||||
t.Errorf("%s 声明 sdk=%v,但构建机指针是 %s —— 声明与实际构建用的 SDK 不一致。\n"+
|
||||
"这个字段本身不是对齐的证据(wire 协议 + 真实握手才是),但**声明错了**会误导下一个人。",
|
||||
name, v, cur)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat("build.sh"); err != nil {
|
||||
t.Error("找不到 build.sh —— 它是与内核实际对齐(协议/握手)的校验点")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDKPinMatchesBuildMachinePointer(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user