refactor(sdk)!: 废除自定协议号,版本身份只由版本号本身承载

兼容性判据原有三处自造版本号,全部删除:
  - procProtocolVersion(模板侧握手常量)
  - shmVersion / shmOffVersion(共享段 header)
  - unifiedVersion(SuperBlock)

为什么废除:语义版本变动频繁(修 bug、加字段),再要求人去升一个额外的号,
那个号必然漏升而腐烂。实证:ProtocolVersion 常量在多次布局变更里只升过一次,
而对应的守卫测试(TestProcTemplate_ShmLayoutMatchesKernel)长期处于失败状态
无人察觉 —— 它当时期望 shmStageFieldCount=18/shmVersion=1,实际已是 19/2。

取而代之的两个判据都是**结构的派生事实**,不需要谁记得去同步:
  1. 主版本号:内核 meta.Version ↔ 插件 sdk.SDKVersion 的主版本段比对。
     主版本跃迁正是破坏性变更的定义(1.0.0 从 C ABI 改子进程即属此列),
     所以它天然是正确分界。
  2. field_count:StageContext 描述符个数,决定标志位基址。

顺带修正了 shmFlagsOff:原实现用本插件的编译期常量推算标志位基址,
内核加字段时会静默错位。现改为从 header 读取内核下发的描述符个数。
SuperBlock 的 version 槽位保留(不动其余偏移),值不再写入也不再校验。

判据:TestProcTemplate_RejectsIncompatiblePeer(含 banned 列表,
防这些号被重新引入)。
This commit is contained in:
JianFeeeee
2026-10-13 09:39:15 +08:00
parent bd26c2a402
commit b2872f3702
6 changed files with 144 additions and 67 deletions

View File

@ -32,14 +32,14 @@ HomeAgent 插件开发 SDK,用于构建与 HomeAgent 平台交互的智能插
**1.0.x 插件升到 1.1.x:不需要改代码,也不需要重编。** 1.1.0 的新增全部是 **1.0.x 插件升到 1.1.x:不需要改代码,也不需要重编。** 1.1.0 的新增全部是
「插件调用、内核实现」方向,不调就不受影响(已用 SDK 0.9.2 编的旧 `plugin.bin` 「插件调用、内核实现」方向,不调就不受影响(已用 SDK 0.9.2 编的旧 `plugin.bin`
实测验证:在新内核上直接建链通过,因为握手校验的是 `ProtocolVersion`、不是 SDK 版本)。 实测验证:在新内核上直接建链通过,因为握手校验的是主版本号、不是 SDK 全等版本)。
想用新字段时重编即可。 想用新字段时重编即可。
**1.1.x 插件升到 1.2.x:接口纯追加,但必须重编。** 公开接口没有签名变更(新增 **1.1.x 插件升到 1.2.x:接口纯追加;若内核共享区布局改了则需重编。** 公开接口没有签名变更
`InjectOptions` 与六个 `*Opts` 变体、`ChannelDef.ContextPolicy`),不调新能力就不受影响; (新增 `InjectOptions` 与六个 `*Opts` 变体、`ChannelDef.ContextPolicy`),不调新能力就不受影响。
但内核的**插件运行协议升到了 2**(统一共享内存区的 fd3 布局改变,**不支持滚动升级**), 1.2.0 同时把 fd3 换成了统一共享区(SuperBlock + StageContext + EvtRing),
所以 `plugin.bin` 必须用配套的 `hmapdev` 重编后与内核**同批**安装——否则握手时协议版本 这类**布局**变化由握手时的结构判据拦下(统一区魔数、`field_count` 描述符个数),
不匹配会被拒绝(错误信息会明确提示用配套 hmapdev 重编,不会静默降级)。 而不是靠某个版本号——它们都是二进制里的派生事实,不会「忘同步」。
## 1.3.0 新增:注入优先级与动态输出通道 ## 1.3.0 新增:注入优先级与动态输出通道
@ -624,9 +624,9 @@ cd tools/vscode-hmapdev && npm install && npm run compile # 然后在 VSCode
| [sanitizer](example/sanitizer) | Go | 内容清洗/安全过滤 | | [sanitizer](example/sanitizer) | Go | 内容清洗/安全过滤 |
**发版时附带预编译示例产物**:SDK 的 release 除 5 平台 `hmapdev` 外,还包含各示例插件的 **发版时附带预编译示例产物**:SDK 的 release 除 5 平台 `hmapdev` 外,还包含各示例插件的
`.hmap` 与 `SHA256SUMS`/`MANIFEST.txt`。原因是插件二进制与内核**协议绑定**(`ProtocolVersion` `.hmap` 与 `SHA256SUMS`/`MANIFEST.txt`。原因是插件二进制与内核在**布局上绑定**(主版本号
+ 共享内存区魔数),只发工具链不发示例产物,很容易拿旧产物去装而握手失败——那看起来像 + 统一共享区魔数 + `field_count`),只发工具链不发示例产物,很容易拿旧产物去装而握手失败
「插件坏了」而不是「版本不配套」。 ——那看起来像「插件坏了」而不是「版本不配套」。
## Remote Device SDK ## Remote Device SDK

View File

@ -52,7 +52,7 @@ has been withdrawn — any SDK tag with a non-zero patch position is wrong.)
**Upgrading a 1.0.x plugin to 1.1.x: no code changes, no rebuild.** Everything added in 1.1.0 is **Upgrading a 1.0.x plugin to 1.1.x: no code changes, no rebuild.** Everything added in 1.1.0 is
in the "plugin calls, kernel implements" direction, so not calling it means not being affected in the "plugin calls, kernel implements" direction, so not calling it means not being affected
(verified with an old `plugin.bin` built against SDK 0.9.2: it handshakes fine on the new kernel, (verified with an old `plugin.bin` built against SDK 0.9.2: it handshakes fine on the new kernel,
because the handshake validates `ProtocolVersion`, not the SDK version). Rebuild only when you want because the handshake validates the **major** version, not the exact SDK version). Rebuild only when you want
the new fields. the new fields.
**Upgrading a 1.1.x plugin to 1.2.x: the interface is purely additive, but a rebuild is required.** **Upgrading a 1.1.x plugin to 1.2.x: the interface is purely additive, but a rebuild is required.**
@ -526,8 +526,8 @@ Internal plugins (platform built-in) have full SDK access including SocialAPI wr
**Prebuilt example artifacts ship with every release**: besides the 5-platform `hmapdev`, an SDK **Prebuilt example artifacts ship with every release**: besides the 5-platform `hmapdev`, an SDK
release contains the example plugins' `.hmap` files plus `SHA256SUMS`/`MANIFEST.txt`. The reason is release contains the example plugins' `.hmap` files plus `SHA256SUMS`/`MANIFEST.txt`. The reason is
that plugin binaries are **protocol-bound** to the kernel (`ProtocolVersion` + the shared-memory that plugin binaries are **layout-bound** to the kernel (major version + unified-region magic +
magic), so shipping the toolchain without matching artifacts invites installing an old artifact — `field_count`), so shipping the toolchain without matching artifacts invites installing an old artifact —
which fails the handshake and looks like "the plugin is broken" rather than "the versions don't which fails the handshake and looks like "the plugin is broken" rather than "the versions don't
match". match".

View File

@ -32,17 +32,33 @@
想用新字段的重编即可。 想用新字段的重编即可。
!!! tip "什么时候必须重编" !!! tip "什么时候必须重编"
只有两种情况:① 内核跨了中版本(如 1.1 → 1.2)且你用了新 API; 只有一种情况:**内核跨了主版本**(如 1.x → 2.x)。
② 内核的 RPC 协议版本变了(`.hmap` 里的 `protocol` 字段与内核不匹配)。 主版本跃迁是破坏性变更的定义,此后的帧格式与共享段布局都不兼容,
后者的错配**不会静默失效** —— 握手时会显式拦下。 必须用配套 `hmapdev` 重编。
## RPC 协议版本 **中版本跃迁(如 1.1 → 1.2)不需要重编**:1.x 内的新增全部是
「插件调用、内核实现」,不调新面就不受影响。
插件包里带 `protocol` 字段,必须等于内核的 `ProtocolVersion`(当前 **2**)。 ## 协议兼容性:只认主版本号
协议 v2 引入了调用帧(tool / cleaner / output)与 `blocks_ref` 媒体块。 **没有独立的协议版本号。** 兼容性由双方版本号的**主版本段**表态:
v1 插件遇上 v2 内核会拿到空参数,反过来 v2 插件发 `blocks_ref` 会被 v1 内核静默忽略 —— 内核的 `meta.Version` 与插件的 `sdk_version`(插件包里的 `sdk_version` 字段,
**两边错配都不报错、只是静默失效**,所以协议版本在握手上显式校验。 即编译时链接的 SDK 版本)。
| 内核 | 插件 SDK | 结果 |
|---|---|---|
| 1.4.0 | 1.0.0 / 1.1.0 / 1.3.0 | ✅ 建链(同主版本) |
| 1.4.0 | 2.0.0 | ❌ 拒绝(跨主版本) |
**为什么不设独立的协议号**:语义版本变动频繁(修 bug、加字段),如果每个
这类改动都要人去升一个额外的号,那个号必然会被漏升而腐烂(历史实证:
曾有一个 `ProtocolVersion` 常量,多次布局变更里只升过一次,而对应的守卫
测试长期处于失败状态、无人察觉)。让兼容性只挂在**始终会被发的那个号**上,
就没有「忘同步」这个失效模式。
跨主版本错配**不会静默失效**——握手时会显式拦下,并提示用配套 `hmapdev` 重编。
此外握手还会比对 `field_count`(StageContext 描述符个数):它决定标志位基址,
不一致会让插件读到错位字段,因此也在握手上显式拒绝。
## 怎么确认自己在用什么 ## 怎么确认自己在用什么

View File

@ -103,15 +103,15 @@ func FullVersion() string {
return SDKName + " v" + Version + " (" + Commit + ")" return SDKName + " v" + Version + " (" + Commit + ")"
} }
// ---- 协议版本 ---- // ---- 协议兼容性 ----
// //
// 子进程 RPC 的协议版本是一个独立的小整数,与 SDK/内核语义版本解耦: // **不自定协议号**:兼容性只认双方版本号的**主版本段**。
// 语义版本变动频繁(修 bug、加字段),而 wire 协议只在**帧格式或握手语义** // 主版本相同即兼容(1.x 内全是纯追加);跨主版本则是破坏性变更
// 变化时才升。当前值见核心仓 internal/plugin/proc/protocol.go 的 ProtocolVersion。 // (如 1.0.0 从 C ABI 改子进程),握手时显式拒绝并提示重编。
// //
// C ABI 时代的 ABIVersion / CABINum / 51 个 Core<Method> 整数 ID 已随 // C ABI 时代的 ABIVersion / CABINum / 51 个 Core<Method> 整数 ID 已随
// Part 6.2 删除 internal/plugin/cabi/ 一并退场: // Part 6.2 删除 internal/plugin/cabi/ 一并退场:
// - 整数 method id 平移为 method 名字符串(proc/protocol.go 的 Method* 常量) // - 整数 method id 平移为 method 名字符串(proc/protocol.go 的 Method* 常量)
// - 版本协商改为握手帧里的 protocol 字段 // - 版本协商改为握手帧里的 core_version / sdk_version 主版本比对
// //
// 保留那些常量只会让人以为它们还在生效。 // 保留那些常量只会让人以为它们还在生效。

View File

@ -26,6 +26,21 @@ func loadProcTemplate(t *testing.T) string {
return string(data) return string(data)
} }
// extractShmFlagsOff 取出模板里 shmFlagsOff 函数的函数体,供断言其取值来源。
func extractShmFlagsOff(t *testing.T, src string) string {
t.Helper()
const sig = "func shmFlagsOff() uint32 {"
i := strings.Index(src, sig)
if i < 0 {
t.Fatal("模板缺少 shmFlagsOff()")
}
rest := src[i+len(sig):]
if j := strings.Index(rest, "\n}"); j >= 0 {
return rest[:j]
}
return rest
}
// stripComments 去掉源码中的注释(用空白填充以保持偏移),只留可执行代码。 // stripComments 去掉源码中的注释(用空白填充以保持偏移),只留可执行代码。
func stripComments(t *testing.T, src string) string { func stripComments(t *testing.T, src string) string {
t.Helper() t.Helper()
@ -218,17 +233,18 @@ func TestProcTemplate_ShmLayoutMatchesKernel(t *testing.T) {
// 与内核 shm.go 的 offXxx 常量对齐(值比较,不依赖 gofmt 的对齐空白) // 与内核 shm.go 的 offXxx 常量对齐(值比较,不依赖 gofmt 的对齐空白)
layout := map[string]string{ layout := map[string]string{
"shmOffMagic": "0", "shmOffMagic": "0",
"shmOffVersion": "4", "shmOffFieldCount": "4",
"shmOffArenaBase": "8", "shmOffArenaBase": "8",
"shmOffArenaCap": "12", "shmOffArenaCap": "12",
"shmOffArenaUsed": "16", "shmOffArenaUsed": "16",
"shmOffCtxBase": "20", "shmOffCtxBase": "20",
"shmOffSeq": "24", "shmOffSeq": "24",
// 与内核 stageFieldCount / sliceSize 对齐 // 与内核 stageFieldCount / sliceSize 对齐。
"shmStageFieldCount": "18", // 注意:这里不校验人工版本号 —— 它已被删除,布局一致性
// 改由描述符个数这种派生事实校验(见下方全局规约断言)。
"shmStageFieldCount": "19",
"shmSliceSize": "8", "shmSliceSize": "8",
"shmVersion": "1",
} }
constRe := func(name, want string) bool { constRe := func(name, want string) bool {
// gofmt 会对齐常量块,故容许 name 与 = 之间有任意空白 // gofmt 会对齐常量块,故容许 name 与 = 之间有任意空白
@ -241,6 +257,26 @@ func TestProcTemplate_ShmLayoutMatchesKernel(t *testing.T) {
} }
} }
// 布局必须自描述:标志位基址由内核下发的描述符个数推出,
// 不能用本插件的编译期常量。否则内核加字段时会静默错位。
if !strings.Contains(src, "func shmFieldCount()") {
t.Error("模板缺少 shmFieldCount():标志位基址必须从 header 读取,不能自行推算")
}
flagsOff := extractShmFlagsOff(t, src)
if strings.Contains(flagsOff, "shmStageFieldCount") {
t.Errorf("shmFlagsOff 不得使用编译期常量 shmStageFieldCount(应经 shmFieldCount() 从 header 读取):\n%s", flagsOff)
}
if !strings.Contains(flagsOff, "shmFieldCount()") {
t.Errorf("shmFlagsOff 应基于 shmFieldCount():\n%s", flagsOff)
}
// 不得再出现人工版本号(内核与模板都不应有)。
for _, banned := range []string{"shmVersion", "shmOffVersion"} {
if strings.Contains(src, banned) {
t.Errorf("模板不得再有 %s:布局一致性由描述符个数校验,不靠人工版本号", banned)
}
}
// 字段枚举顺序:内核 stageField 的前若干项 // 字段枚举顺序:内核 stageField 的前若干项
fieldOrder := []string{ fieldOrder := []string{
"fRawMessage = iota", "fUserID", "fGroupID", "fLLMText", "fRawMessage = iota", "fUserID", "fGroupID", "fLLMText",
@ -343,15 +379,24 @@ func TestProcTemplate_DispatchesRequestsConcurrently(t *testing.T) {
} }
} }
// 协议与共享内存区域版本/魔数不匹配必须拒绝,不得半兼容运行。 // 主版本与共享内存区域结构不匹配必须拒绝,不得半兼容运行。
func TestProcTemplate_RejectsVersionMismatch(t *testing.T) { //
// 注意:这里不再检查任何「协议版本」/「共享段版本」自定号 ——
// 它们已废除。兼容性只认双方版本号的**主版本段**(跨主版本即破坏性变更),
// 布局一致性由描述符个数把关。
func TestProcTemplate_RejectsIncompatiblePeer(t *testing.T) {
src := loadProcTemplate(t) src := loadProcTemplate(t)
// §13.1 起共享段合并为单一「统一区域」,魔数校验文案随之更新。 for _, want := range []string{"主版本不匹配", "字段数不匹配", "统一区域魔数不匹配"} {
for _, want := range []string{"协议版本不匹配", "共享段版本不匹配", "统一区域魔数不匹配"} {
if !strings.Contains(src, want) { if !strings.Contains(src, want) {
t.Errorf("握手应校验并拒绝 %q", want) t.Errorf("握手应校验并拒绝 %q", want)
} }
} }
// 自定版本号必须已彻底移除,否则会出现「谁负责升这个号」的腐烂面。
for _, banned := range []string{"procProtocolVersion", "shmVersion", "unifiedVersion"} {
if strings.Contains(src, banned) {
t.Errorf("模板不得再有自定版本号 %s", banned)
}
}
} }
// 全平台统一产出 plugin.bin。 // 全平台统一产出 plugin.bin。

View File

@ -18,30 +18,38 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"strings"
"sync" "sync"
sdk "github.com/JianFeeeee/homeagentsdk/sdk" sdk "github.com/JianFeeeee/homeagentsdk/sdk"
) )
// ---- 协议常量(须与内核 internal/plugin/proc/protocol.go 一致)---- // majorVersion 取版本号的主版本段("1.4.0" → "1")。
// 兼容性只认主版本:1.x 内的中版本跃迁都是纯追加,不调新面就不受影响;
// 主版本跃迁才是破坏性变更(如 1.0.0 从 C ABI 改子进程)。
func majorVersion(v string) string {
v = strings.TrimPrefix(strings.TrimSpace(v), "v")
if i := strings.IndexByte(v, '.'); i >= 0 {
return v[:i]
}
return v
}
// procProtocolVersion 必须与内核的 proc.ProtocolVersion 完全一致。 // ---- 协议常量(须与内核 internal/plugin/proc/protocol.go 一致)----
// //
// v2:内核→插件的 payload 改用调用帧(tool/cleaner/output),媒体块改走 // 不自定协议号:兼容性由双方版本号的**主版本**表态(内核 meta.Version、
// blocks_ref。v1 插件只读内联 args,遇上 v2 内核会拿到空参数;反过来 v2 // 插件 sdk.SDKVersion)。主版本相同即兼容(1.x 内全是纯追加),
// 插件发 blocks_ref,v1 内核也会静默忽略。两边错配都不报错、只是静默失效, // 跨主版本则拒绝建链(主版本跃迁正是破坏性变更的定义)。
// 所以靠这个常量在握手上显式拦下。 // 布局一致性另由 field_count(描述符个数)把关。
const procProtocolVersion = 2
// ---- 统一共享内存区域布局(与内核 internal/plugin/proc/unified.go 一致)---- // ---- 统一共享内存区域布局(与内核 internal/plugin/proc/unified.go 一致)----
const ( const (
unifiedMagic = 0x554D5352 // "UMSR" — Unified Memory Shared Region unifiedMagic = 0x554D5352 // "UMSR" — Unified Memory Shared Region
unifiedVersion = 1
superBlockSize = 64 superBlockSize = 64
sbOffMagic = 0 sbOffMagic = 0
sbOffVersion = 4 sbOffReserved0 = 4 // 保留(原 version 槽位,布局不变以免动其余偏移)
sbOffGeneration = 8 sbOffGeneration = 8
sbOffCapacity = 16 sbOffCapacity = 16
sbOffCtxOff = 20 sbOffCtxOff = 20
@ -59,16 +67,15 @@ const (
shmStageFieldCount = 19 shmStageFieldCount = 19
shmSliceSize = 8 shmSliceSize = 8
shmOffMagic = 0 shmOffMagic = 0
shmOffVersion = 4 shmOffFieldCount = 4 // 描述符个数,插件据此定位标志位(不再用自身常量推算)
shmOffArenaBase = 8 shmOffArenaBase = 8
shmOffArenaCap = 12 shmOffArenaCap = 12
shmOffArenaUsed = 16 shmOffArenaUsed = 16
shmOffCtxBase = 20 shmOffCtxBase = 20
shmOffSeq = 24 shmOffSeq = 24
shmMagic = 0x48415348 shmMagic = 0x48415348
shmVersion = 2
) )
// 字段索引(顺序须与内核 stageField 枚举一致) // 字段索引(顺序须与内核 stageField 枚举一致)
@ -430,6 +437,13 @@ func shmArenaBase() uint32 { return shmU32(shmOffArenaBase) }
func shmArenaCap() uint32 { return shmU32(shmOffArenaCap) } func shmArenaCap() uint32 { return shmU32(shmOffArenaCap) }
func shmCtxBase() uint32 { return shmU32(shmOffCtxBase) } func shmCtxBase() uint32 { return shmU32(shmOffCtxBase) }
// shmFieldCount 返回内核写入的描述符个数。
//
// 标志位紧跟在描述符数组之后,其偏移**必须**由内核下发的个数推出,
// 不能用本插件的编译期常量:内核加字段时两者会不一致,
// 自行推算会静默错位(而握手时已按此值做过一致性检查)。
func shmFieldCount() uint32 { return shmU32(shmOffFieldCount) }
func shmDescOff(field int) uint32 { func shmDescOff(field int) uint32 {
return shmCtxBase() + uint32(field*shmSliceSize) return shmCtxBase() + uint32(field*shmSliceSize)
} }
@ -446,7 +460,7 @@ func shmSetDesc(field int, off, ln uint32) {
} }
func shmFlagsOff() uint32 { func shmFlagsOff() uint32 {
return shmCtxBase() + uint32(shmStageFieldCount*shmSliceSize) return shmCtxBase() + shmFieldCount()*shmSliceSize
} }
func shmGetFlag(bit int) bool { return shm[shmFlagsOff()+uint32(bit)] != 0 } func shmGetFlag(bit int) bool { return shm[shmFlagsOff()+uint32(bit)] != 0 }
@ -1511,21 +1525,24 @@ func handleKernelRequest(req *rpcRequest) {
func handleHandshake(req *rpcRequest) { func handleHandshake(req *rpcRequest) {
var p struct { var p struct {
Protocol int `json:"protocol"` CoreVersion string `json:"core_version"`
ShmVersion uint32 `json:"shm_version"` FieldCount uint32 `json:"field_count"`
ShmSize int `json:"shm_size"` ShmSize int `json:"shm_size"`
PluginName string `json:"plugin_name"` PluginName string `json:"plugin_name"`
EvtRingSize int `json:"evt_ring_size,omitempty"` EvtRingSize int `json:"evt_ring_size,omitempty"`
} }
json.Unmarshal(req.Params, &p) json.Unmarshal(req.Params, &p)
if p.Protocol != procProtocolVersion { // 主版本号是唯一兼容判据:跨主版本必是被破坏性变更分开的两端。
respondErr(req.ID, fmt.Errorf("协议版本不匹配(内核 %d,插件 %d)——请用配套 plugindev 重编", if mine, theirs := majorVersion(sdk.SDKVersion), majorVersion(p.CoreVersion); mine != theirs {
p.Protocol, procProtocolVersion)) respondErr(req.ID, fmt.Errorf("主版本不匹配(内核 %s,插件 SDK %s)——请用配套 hmapdev 重编",
p.CoreVersion, sdk.SDKVersion))
return return
} }
if p.ShmVersion != shmVersion { // 字段个数必须与编译期一致:它决定标志位基址,不一致会静默错位。
respondErr(req.ID, fmt.Errorf("共享段版本不匹配(内核 %d,插件 %d)", p.ShmVersion, shmVersion)) if p.FieldCount != shmStageFieldCount {
respondErr(req.ID, fmt.Errorf("StageContext 字段数不匹配(内核 %d,插件 %d)——请用配套 hmapdev 重编",
p.FieldCount, shmStageFieldCount))
return return
} }
if p.PluginName != "" { if p.PluginName != "" {
@ -1573,7 +1590,6 @@ func handleHandshake(req *rpcRequest) {
} }
respond(req.ID, map[string]interface{}{ respond(req.ID, map[string]interface{}{
"protocol": procProtocolVersion,
"sdk_version": sdk.SDKVersion, "sdk_version": sdk.SDKVersion,
"plugin_name": pluginName, "plugin_name": pluginName,
"pid": os.Getpid(), "pid": os.Getpid(),