From a66739e59b802fe224801e42eb1117dcc32b9d7a Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sun, 6 Sep 2026 11:35:09 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20README=20=E9=A1=B6=E9=83=A8=E8=A1=A5?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=85=BC=E5=AE=B9=E6=80=A7=E8=A1=A8=E4=B8=8E?= =?UTF-8?q?=E5=B9=B6=E5=8F=91=E7=BA=A6=E5=AE=9A=EF=BC=8C=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E9=93=BE=E6=8E=A5=E5=8D=87=E5=88=B0=20v1.1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两件事此前没写进 README,会让读者拿到错的现状: ## 版本兼容性表 README 开头没有版本号、没有兼容性说明,读者无从判断「我这个版本能不能用新接口」。 补一张「内核版本 ↔ SDK 版本」表,说清 patch 位恒为 .0 的语义,以及 1.0.x 升 1.1.x 不需要改代码也不需要重编(新增是「插件调用、内核实现」方向, 不调就不受影响;实测用 SDK 0.9.2 编的旧 plugin.bin 在新内核上直接建链通过)。 ## 并发约定 PluginSDK 是被多个 goroutine 同时使用的共享对象,这一点此前没有写在明处。 列出 SDK 已保证的(访问器/注入/注册/handler 幂等)与开发者必须自己保证的 (StageContext 字段全导出,并发读写要自己持锁;Extra 的 map 并发写是直接 fatal)。 并顺手把下载链接从 v1.0.0 升到 v1.1.0——照旧链接去 release 页面会找不到 v1.1.0 的产物,因为那条 curl 用的是 v1.0.0。 --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++-- README_EN.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 20d86d3..3465cf3 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,26 @@ HomeAgent 插件开发 SDK,用于构建与 HomeAgent 平台交互的智能插件。 +## 版本与兼容性 + +当前:**SDK 1.1.0**(需要内核 **1.1.1+** 才能用媒体接口;媒体之外的能力内核 1.0.0 即可)。 + +**版本号跟随内核的中版本,patch 位恒为 `.0`**: + +| 内核版本 | 对应 SDK | +|---|---| +| 1.0.0 / 1.0.1 / … / 1.0.4 | 1.0.0 | +| 1.1.0 / 1.1.1 / … / 1.1.N | **1.1.0** | +| 1.2.0 起 | 1.2.0 | + +内核的 patch 位专用于 bugfix 与漏洞修复,不碰公开接口,所以 SDK 版本号不跟着动—— +否则你要么被迫跟版、要么怀疑自己版本过时,而接口其实一个字都没变。 + +**1.0.x 插件升到 1.1.x:不需要改代码,也不需要重编。** 1.1.0 的新增全部是 +「插件调用、内核实现」方向,不调就不受影响(已用 SDK 0.9.2 编的旧 `plugin.bin` +实测验证:在新内核上直接建链通过,因为握手校验的是 `ProtocolVersion`、不是 SDK 版本)。 +想用新字段时重编即可。 + ## SDK API 接口 ### Plugin 接口 @@ -226,8 +246,8 @@ func New(name string, sett SettingsAPI, regTool ToolRegistrar, regStage StageReg [Releases](https://gitcode.com/JianFeeeee/homeagent-sdk/releases) 下载后加入 PATH 即可: ```bash -# 从 release 附件下载(以 v1.0.0 / linux amd64 为例) -curl -Lo plugindev https://gitcode.com/JianFeeeee/homeagent-sdk/releases/download/v1.0.0/plugindev_linux_amd64 +# 从 release 附件下载(以 v1.1.0 / linux amd64 为例) +curl -Lo plugindev https://gitcode.com/JianFeeeee/homeagent-sdk/releases/download/v1.1.0/plugindev_linux_amd64 chmod +x plugindev # 或从源码自己编 @@ -368,6 +388,31 @@ enabled := sdk.AutoRestart() 插件崩溃时平台自动拉起,保障服务可用性。 +> ⚠️ `SetAutoRestart` 的典型用法是「外部连接建好后再判定能否自动重启」,而连接建立 +> 通常在后台 goroutine 里,内核又在另一个 goroutine 读它——这对读写天然并发。 +> **SDK 1.1.0 已给这个标志与全部 API 字段加锁**(`-race` 实测 11 处竞态, +> 生产表现是插件重载瞬间偶发 nil 解引用崩溃)。早于 1.1.0 的版本建议升级。 + +## 插件开发者的并发约定 + +`PluginSDK` 是**被多个 goroutine 同时使用的共享对象**:你在 `Start()` 里起的轮询、 +监听、定时器都拿着同一份 `*PluginSDK` 往里注消息,而内核会在加载/重载时写它的 + API 字段。因此: + +- **SDK 侧已保证的**:全部 API 访问器(`Memory()`/`DocMemory()`/…)、全部注入方法、 + `SetAutoRestart`/`AutoRestart`、`RegisterTool`/`RegisterStage`、 + `RunStopHandlers`/`RunOnRemoveHandlers`(幂等,并发调也只执行一次)。 +- **你需要自己保证的**:`StageContext` 的字段全部导出,并发读写必须自己持 + `ctx.Lock()`/`ctx.RLock()`。尤其是 `ctx.Extra`——**map 的并发写在 Go 里是直接 fatal, + `recover` 接不住**。 + +```go +ctx.Lock() +ctx.Extra["mykey"] = value +ctx.FinalText += "补充说明" +ctx.Unlock() +``` + ## 受限 SDK vs 完整 SDK 外部插件(第三方分发)使用**受限 SDK**,仅暴露安全子集: diff --git a/README_EN.md b/README_EN.md index 9bb56ff..6bf4148 100644 --- a/README_EN.md +++ b/README_EN.md @@ -2,6 +2,29 @@ Plugin development SDK for building intelligent plugins that interact with the HomeAgent platform. +## Version and Compatibility + +Current: **SDK 1.1.0** (the media APIs need kernel **1.1.1+**; everything else works on kernel 1.0.0). + +**The version tracks the kernel's minor version, with the patch position pinned at `.0`**: + +| Kernel version | Matching SDK | +|---|---| +| 1.0.0 / 1.0.1 / … / 1.0.4 | 1.0.0 | +| 1.1.0 / 1.1.1 / … / 1.1.N | **1.1.0** | +| 1.2.0 onward | 1.2.0 | + +The kernel's patch position is reserved for bugfixes and vulnerability fixes, which never touch the +public interface, so the SDK version has no reason to move with it — otherwise you would either be +forced to chase releases or suspect your version is stale, when not one character of the interface +has changed. + +**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 +(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 +the new fields. + ## SDK API Surface ### Plugin Interface @@ -233,8 +256,8 @@ Plugin developers only need to implement the `Plugin` interface and export a `Ne [Releases](https://gitcode.com/JianFeeeee/homeagent-sdk/releases) and put it on your PATH: ```bash -# From release assets (v1.0.0 / linux amd64 shown) -curl -Lo plugindev https://gitcode.com/JianFeeeee/homeagent-sdk/releases/download/v1.0.0/plugindev_linux_amd64 +# From release assets (v1.1.0 / linux amd64 shown) +curl -Lo plugindev https://gitcode.com/JianFeeeee/homeagent-sdk/releases/download/v1.1.0/plugindev_linux_amd64 chmod +x plugindev # Or build from source @@ -341,6 +364,33 @@ enabled := sdk.AutoRestart() The platform automatically restarts the plugin on crash, ensuring service availability. +> ⚠️ `SetAutoRestart` is typically used to decide whether auto-restart is safe *after* an +> external connection has been established, and that connection setup usually happens in a +> background goroutine while the kernel reads the flag from another one — which is inherently +> concurrent. **SDK 1.1.0 locks this flag and all API fields** (`-race` reported 11 data races; +> in production this showed up as sporadic nil-dereference crashes during plugin reload). Upgrade +> if you are on anything earlier. + +## Concurrency Contract for Plugin Developers + +`PluginSDK` is a **shared object used by multiple goroutines**: the polling, listening and timer +callbacks you start in `Start()` all hold the same `*PluginSDK` and push messages into it, while +the kernel writes its API fields during load/reload. So: + +- **Guaranteed by the SDK**: all API accessors (`Memory()`/`DocMemory()`/…), all injection methods, + `SetAutoRestart`/`AutoRestart`, `RegisterTool`/`RegisterStage`, and + `RunStopHandlers`/`RunOnRemoveHandlers` (idempotent; concurrent calls still run it once). +- **Your responsibility**: every field of `StageContext` is exported, and concurrent read/write + must hold `ctx.Lock()`/`ctx.RLock()`. Especially `ctx.Extra` — **concurrent map writes are a + fatal in Go, and `recover` cannot catch it**. + +```go +ctx.Lock() +ctx.Extra["mykey"] = value +ctx.FinalText += "supplementary note" +ctx.Unlock() +``` + ## Restricted SDK vs Full SDK External plugins (third-party distribution) use a **restricted SDK** that only exposes a safe subset: