meta: 内核版本升到 1.0.0;生产切换脚本改走 hmap 正规通道(Part 6.5)

## 版本号

1.0.0:外部插件从 C ABI 动态库迁到子进程 + 共享内存。首个不再加载
`.so`/`.dll` 的版本,与 0.9.x 不兼容(存量插件必须用新版 plugindev 重编)。
SDKCompatibleVersion 同步升 1.0.0。

同时删掉 ABIVersion / CABINum / 51 个 Core<Method> 整数 ID —— 随 Part 6.2
删 internal/plugin/cabi/ 就已无使用者(grep 确认只剩定义处)。留着会让人
以为 C 层协商还在生效,或以为加 method 要同步维护那张整数表。

⚠️ 注意 Makefile 的 `VERSION ?= $(shell git describe --tags --dirty)`:
实际注入值来自 git tag,meta.go 里的默认值只在不带 ldflags 时生效。
make build 当前注入 v0.9.1-56-g2572688-dirty。要让 1.0.0 真正生效需打
v1.0.0 tag 或显式传 VERSION=1.0.0。

## 生产切换脚本重写

第一版是手工拷 plugin.bin + 手改 plugin.json 的 entry —— 那等于**重新实现
了一遍 hmap 解包逻辑,且实现得更差**。漏掉的东西:

  platforms 字段          hmap 内的 plugin.json 本来就写对了
  平台二进制选择          我硬编码 _linux_amd64,正规路径用 platformBinary()
  overwrite 语义          StopAndUnload 停旧实例但**保留配置表**
  失败回滚                os.Rename 备份旧目录,解包失败自动恢复
  校验                    validatePackage 查 manifest + 各平台二进制齐全

配置保留那条尤其关键:生产 17 个插件都有配置(qq 账号、weather 默认城市、
browser profile 路径)。我的脚本恰好没碰配置表所以侥幸不丢,但那是运气
不是设计。

改为 POST 到 pluginmgr 的 HTTP 端点(127.0.0.1:9876/plugins),
传 {path, overwrite:true} 走 installFromPath → installFromData。

保留的一个设计:**先全部校验再动手**。任一插件缺 hmap 就整批中止——
新 homed 不认 .so,「一半装了一半没装」的中间态最难排查。

## 生产切换已执行

顺序(先换二进制再装包,而非反过来):
  1. systemctl stop homeagent
  2. 换 /usr/local/bin/homed
  3. 起服务 —— 15 个 .so 插件报可操作错误被跳过,homed 本体与 16 个内置正常
  4. 逐个 POST 装 17 个 hmap(overwrite=true)
  5. 待重启核对

第 3 步顺带在真实二进制上验证了 Part 6.2 的可操作错误:
  [plugin] dynamic weather: 检测到旧 C ABI 产物(plugin.so/.dll/.dylib)。
  外部插件已改为子进程模式,请用新版 plugindev 重编产出 plugin.bin
  (业务代码无需修改)
不崩溃,只跳过。若反过来先装包,旧 homed 的 StopAndUnload 会停掉 qq
消息通道且无法重载 .bin,会卡在「插件全挂」的状态。

结果:17/17 成功,全部 config_kept=true;0 个残留 .so;17 个 plugin.bin
均有执行位;17 个 manifest 的 entry 均为 plugin.bin;无 .bak 残留。
bundle 包正确挑了当前平台(weather 目录只留 8.7MB 的 linux/amd64 那份)。

备份:/home/newqqagent-migration-backup-20260902-214812
(plugins 全目录 + homed.old + homeagent.service,162MB)

验证:go build ./... 通过;go test ./... 全仓无失败。

Ref: docs/zh/plugin-migration-plan.md Part 6.5
This commit is contained in:
JianFeeeee
2026-09-02 22:40:47 +08:00
parent 2572688c51
commit 62bdfa2b54
2 changed files with 150 additions and 81 deletions

View File

@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""生产切换:经 pluginmgr 正规通道安装 17 个 hmapPart 6.5)。
与手工拷贝方案的区别 —— 这里复用内核自己的安装逻辑:
validatePackage 校验 manifest + 平台二进制齐全
StopAndUnload 停旧实例但**保留配置表**
os.Rename 备份 解包失败自动回滚到旧版本
platformBinary() 按 runtime 挑当前平台那份,重命名为 plugin.bin
chmod 0755 补执行位
手工拷贝会重新实现这一套,且必然实现得更差(第一版就漏了 platforms 字段
与配置保留语义)。
用法:
switch-production.py 演练
switch-production.py --apply 实际安装
"""
import json
import os
import sys
import urllib.error
import urllib.request
PROD_PLUGINS = "/home/newqqagent/plugins"
SDK_EXAMPLE = "/home/program/TrueAgent/third_party/homeagent-sdk/example"
PLUGINMGR = "http://127.0.0.1:9876/plugins"
def find_hmap(name):
"""找插件的 hmap 包。
bundle:true -> <snake>_bundle.hmap含多平台二进制
bundle:false -> <snake>_<goos>_<goarch>.hmapqq 是这种)
"""
dist = os.path.join(SDK_EXAMPLE, name, "dist")
if not os.path.isdir(dist):
return None
cands = [f for f in os.listdir(dist) if f.endswith(".hmap")]
if not cands:
return None
for c in cands:
if c.endswith("_bundle.hmap"):
return os.path.join(dist, c)
return os.path.join(dist, sorted(cands)[0])
def install(path):
"""POST 到 pluginmgr。overwrite=true 走原地更新分支,保留配置表。"""
body = json.dumps({"path": path, "overwrite": True}).encode()
req = urllib.request.Request(
PLUGINMGR, data=body,
headers={"Content-Type": "application/json"},
method="POST")
try:
with urllib.request.urlopen(req, timeout=180) as resp:
return json.loads(resp.read().decode()), None
except urllib.error.HTTPError as e:
return None, "HTTP %d: %s" % (e.code, e.read().decode()[:300])
except Exception as e:
return None, str(e)
def main():
apply = "--apply" in sys.argv
targets = sorted(
d for d in os.listdir(PROD_PLUGINS)
if os.path.isfile(os.path.join(PROD_PLUGINS, d, "plugin.so"))
or os.path.isfile(os.path.join(PROD_PLUGINS, d, "plugin.bin"))
)
print("生产外部插件: %d" % len(targets))
# 先全部校验,任一缺包就整批中止。
# 理由:新 homed 不认 .so「一半装了一半没装」的中间态最难排查。
plan = []
missing = []
for name in targets:
h = find_hmap(name)
if h is None:
missing.append(name)
else:
plan.append((name, h))
if missing:
print("\n✗ 中止:以下插件缺 hmap 包:")
for m in missing:
print(" " + m)
print("\n先跑 rebuild-plugins.sh 重编。")
return 1
print("✓ 全部 %d 个 hmap 就位\n" % len(plan))
for name, h in plan:
print(" %-16s %-44s %6d KB" % (
name, os.path.basename(h), os.path.getsize(h) // 1024))
if not apply:
print("\n[演练] 加 --apply 才实际安装")
return 0
print("\n经 pluginmgr 安装overwrite=true保留配置...")
ok = 0
failed = []
for name, h in plan:
result, err = install(h)
if err:
print("%-16s %s" % (name, err))
failed.append(name)
continue
if "error" in result:
print("%-16s %s: %s" % (
name, result["error"], result.get("details", "")))
failed.append(name)
continue
print("%-16s %-12s v%s -> v%s config_kept=%s" % (
name,
result.get("action", "?"),
result.get("previous_version", "?"),
result.get("version", "?"),
result.get("config_kept", False)))
ok += 1
print("\n成功 %d / 失败 %d" % (ok, len(failed)))
if failed:
print("失败: " + " ".join(failed))
return 1
return 0
if __name__ == "__main__":
sys.exit(main())