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
那次恢复的处置动作)判红,而我没有"内核不读该字段"的证据。现在:可以不声明;
声明了就必须与构建机指针一致。
122 lines
5.3 KiB
Go
122 lines
5.3 KiB
Go
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
/*
|
||
* 判据:**插件清单里不许写死 sdk 版本**。
|
||
*
|
||
* 2026-09-14 的事故与它当天的复发,根因都不是"忘了重编",而是把 SDK 版本钉在源码里:
|
||
*
|
||
* · 08:09 那次:内核 v1.3.12,插件二进制还是 sdk=1.2.0 → 每次启动 SIGSEGV,
|
||
* 内核 5 分钟内崩溃 4 次后熔断("停止自动重启,需人工介入");
|
||
* · 当天稍晚复发:内核升到 1.4.0,而 `go.mod` 里 replace 指向
|
||
* `/root/.homeagent/hmapdev/sdk/v1.3.0` —— **「照原样重编」只会再造一个 1.3.0**。
|
||
*
|
||
* 写死的那份版本号,和「内核实际用的版本」没有任何机制保证一致,
|
||
* 而失配的后果是启动即崩、且要人介入才能恢复。所以:
|
||
*
|
||
* 1. `plugin.json` / `plg.json` 不得出现 `sdk` 字段 —— 它必须由构建工具
|
||
* 按**本次实际编译用的 SDK** 写入(见 `build.sh`:解析本机 current 指针 + 构建后校验);
|
||
* 2. `go.mod` 的 `replace` 不得指向带版本号的绝对路径(同样是与内核脱钩的钉子)。
|
||
*
|
||
* 这两条都是**来源**判据(验形态,不验行为):真正的一致性校验在 `build.sh` 里,
|
||
* 它拿内核内嵌的版本号跟本次构建用的比,不一致就拒绝出包。
|
||
*/
|
||
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 {
|
||
t.Fatalf("读不到 %s: %v", name, err)
|
||
}
|
||
var m map[string]any
|
||
if err := json.Unmarshal(raw, &m); err != nil {
|
||
t.Fatalf("%s 不是合法 JSON: %v", name, err)
|
||
}
|
||
v, ok := m["sdk"]
|
||
if !ok {
|
||
continue // 不声明也是合法的(已装 21 个里 18 个如此)
|
||
}
|
||
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) {
|
||
raw, err := os.ReadFile("go.mod")
|
||
if err != nil {
|
||
t.Fatalf("读不到 go.mod: %v", err)
|
||
}
|
||
pin := ""
|
||
for _, line := range strings.Split(string(raw), "\n") {
|
||
if strings.Contains(line, "replace") && strings.Contains(line, "homeagent-sdk") {
|
||
parts := strings.Split(line, "=>")
|
||
if len(parts) == 2 {
|
||
pin = strings.TrimSpace(parts[1])
|
||
}
|
||
}
|
||
}
|
||
if pin == "" {
|
||
t.Fatal("go.mod 里没有指向本机 SDK 的 replace —— 离线环境下 go test/go build 会直接失败," +
|
||
"请恢复它(路径取 <SDK_ROOT>/current 指向的目录)")
|
||
}
|
||
|
||
// 基准:构建机的 SDK 指针。取不到就是"判据读不懂",不许默默放过。
|
||
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)。"+
|
||
"这条判据判的是「钉子有没有偏离构建机的 SDK 指针」,没有基准就无法判断 —— "+
|
||
"用 HOMEAGENT_SDK_ROOT 指定 SDK 根目录。", root, err)
|
||
}
|
||
cur := strings.TrimSpace(string(curRaw))
|
||
if !strings.HasSuffix(pin, "/"+cur) {
|
||
t.Errorf("go.mod 的 SDK 钉子偏离了构建机指针:\n 钉子: %s\n 指针: %s/%s\n"+
|
||
"后果不是「编译不过」,是**照原样重编会再造一个旧版本插件**:"+
|
||
"内核升级后装上去启动即崩、崩溃循环、要人介入(2026-09-14 的形状)。\n"+
|
||
"改法:把钉子改成指针指向的目录;内核与 SDK 是否真的对齐由 build.sh 硬校验。",
|
||
pin, root, cur)
|
||
}
|
||
if _, err := os.Stat("build.sh"); err != nil {
|
||
t.Error("找不到 build.sh —— 它是「与内核版本一致性」的硬校验点。" +
|
||
"只有钉子、没有构建期校验的话,「current 与内核不一致」这种状态就没人抓了。")
|
||
}
|
||
}
|