diff --git a/plugins/homeagent-mail-bridge/build.sh b/plugins/homeagent-mail-bridge/build.sh index b7779c4..9470673 100755 --- a/plugins/homeagent-mail-bridge/build.sh +++ b/plugins/homeagent-mail-bridge/build.sh @@ -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 < /root/.homeagent/hmapdev/sdk/v1.3.0 -// 这个 replace 是**构建机的 SDK 指针**,不是源码常量:它必须跟着 +// 这个 replace 是**构建机的 SDK 指针**,不是源码常量:它跟着 // `/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)+ 一次真实握手,不是版本号相等。 diff --git a/plugins/homeagent-mail-bridge/manifest_sdk_test.go b/plugins/homeagent-mail-bridge/manifest_sdk_test.go index 6b6936f..6f5e29d 100644 --- a/plugins/homeagent-mail-bridge/manifest_sdk_test.go +++ b/plugins/homeagent-mail-bridge/manifest_sdk_test.go @@ -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) {