From 2a03a83ce0ad62a825255d33fa8a5becb8ee5b32 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Thu, 3 Sep 2026 08:12:30 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20Part=206.6=20=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E6=94=B6=E5=B0=BE=20=E2=80=94=E2=80=94=20=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E8=AE=A1=E5=88=92/=E6=8E=A5=E5=8F=A3=E7=9F=A9=E9=98=B5/PLUGIN?= =?UTF-8?q?=5FDEV=20=E5=85=A8=E9=87=8F=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## plugin-migration-plan.md Part 6 标记完成,并**记录实际执行与计划的偏离**而非假装一致: 原计划「逐插件迁移,随时回退」。用户决策改为彻底舍弃 .so、无回退通道, 本轮直接删 internal/plugin/cabi/。因此【V】的「.so ↔ .bin 混跑集群冒烟」 不再适用——新内核根本不认 .so。改为验证「新内核面对旧 .so 给可操作错误 且不崩溃」,已在真实二进制上确认。 最终验收清单加「结果」列,13 项逐项对账。**两项未完全达标,如实标注**: - #9 SetToolBlocks:method 已定义并划入 CapCore,但内核侧仍返回未实现。 C ABI 时代它也是空实现(§1.4),故不是回归,但也没兑现 §3.8 的承诺。 - #13 内存:15 进程 RSS=88.0MB,远超「基线 +29MB」。根因是每插件静态链接 整个 Go runtime,15 个不同二进制无共同物理页(PSS/RSS 99.9% vs 基线 44%)。 实验 5 基线用的是 2.68MB 最小插件,绝对数字不可比;结构性指标 (均摊线程 5.5 vs 4.9)同量级。 新增两节实录:Part 6.5 生产切换(执行顺序为何不能反过来、hmap 正规通道 vs 手工拷贝的对照表、真实 QQ 消息的端到端证据链)与 Part 6.6 压测数据。 ## plugin-interface-matrix.md 状态从「基线 v1」升为「完成 v2」。三个合同面逐一标注达成情况: - 合同面 B:51 个整数 method id 已平移为 Method* 字符串常量。保留原表作 历史对照,但注明 case 25(CoreFreeString)无对应 method(内存管理是 C 层 特有问题),以及 io.setToolBlocks 已定义但内核侧未实现。 - 合同面 C:C1 标题从「今天」改为「迁移前」(迁移已完成,「今天」会误导); C2 补上「全部插件共享同一块 memfd」这个关键决定及其理由——第一版设计 是每插件一段,那会退化成副本模型复现 lost update。 - 第六节「新获得的能力」加「实际结果」列。事件订阅标注机制已完成但 零用户使用,故未经真实负载检验——这比只写 ✅ 诚实。 「刻意不给」清单同步为带 API 后缀的新命名(SelftestAPI 等),与 capability.go 的 withheldCapabilities 对齐,并说明为何加后缀: 不加时子串匹配会把 tool.register / io.setToolBlocks 误判为泄漏 ToolAPI。 ## PLUGIN_DEV.md(中英双份) C ABI 时代的描述全部改掉: - 「动态 .so/.dll 插件」→「子进程插件」 - 「生成 C ABI bridge(z_bridge_gen.go + z_entry.c)」→ 子进程运行时三文件 - 「go build -buildmode=c-shared」→「go build(CGO_ENABLED=0)」 - 平台二进制表:三平台统一 plugin.bin(bundle 包内按 goos.goarch 区分) - 「不能跨 C ABI 边界序列化」→「不能跨进程序列化」 - 「ABI v2 写回」→「Stage 写回」 新增 v1.0.0 破坏性变更提示框,五条要点:.so 不再加载、业务代码不需改、 entry 字段对 Go 插件已无意义、不再需要 cgo、Windows 从 3 字段升到全字段。 保留 .so 字样的只有变更说明本身(3 处),其余全部清理。 --- assets/docs/en/PLUGIN_DEV.md | 48 ++++-- assets/docs/zh/PLUGIN_DEV.md | 40 +++-- docs/zh/plugin-interface-matrix.md | 132 ++++++++++++----- docs/zh/plugin-migration-plan.md | 226 +++++++++++++++++++++++++---- 4 files changed, 348 insertions(+), 98 deletions(-) diff --git a/assets/docs/en/PLUGIN_DEV.md b/assets/docs/en/PLUGIN_DEV.md index 5da2c56..b4705b6 100644 --- a/assets/docs/en/PLUGIN_DEV.md +++ b/assets/docs/en/PLUGIN_DEV.md @@ -29,7 +29,7 @@ type Plugin interface { | Method | Use Case | Complexity | |--------|----------|------------| -| **Dynamic .so/.dll plugin (recommended)** | Independently distributed third-party plugins | Medium, generated using `plugindev` toolchain | +| **Subprocess plugin (recommended)** | Independently distributed third-party plugins | Medium, generated using `plugindev` toolchain | | **Built-in plugin** | Released with HomeAgent | Simple, requires merging into main repo | | **Lua script plugin** | Lightweight rapid prototyping | Simple, generated using `plugindev init --lua` | @@ -114,7 +114,7 @@ myplugin/ └── thirdpart/ — Optional external source code directory ``` -C ABI bridge files (`z_bridge_gen.go` + `z_entry.c`) are auto-generated at build time. +Subprocess runtime files (`z_proc_gen.go` and friends) are auto-generated at build time. **Lua plugin**: @@ -142,8 +142,8 @@ plugindev build --replace # append a go.mod replace directive (repeat Execution process: 1. Reads `plg.json` `targets`/`bundle` fields to determine build targets (bundle takes priority, see below) -2. Auto-generates C ABI bridge code (`z_bridge_gen.go` + `z_entry.c`; Windows only `z_bridge_gen.go`) -3. **Go plugin**: Runs `go build -buildmode=c-shared` (produces `.so` / `.dylib` / `.dll`) +2. Auto-generates subprocess runtime code (`z_proc_gen.go` + `z_proc_shm_unix.go` + `z_proc_shm_windows.go`) +3. **Go plugin**: Runs `go build` (a plain executable, `CGO_ENABLED=0`) 4. **Lua plugin**: Packages source code directly, no compilation needed (contents: `plugin.json` + `main.lua`, plus optional `README.md`, `LICENSE`, `thirdpart/*.lua`) 5. Generates `plugin.json` output manifest 6. Packages as `.hmap` distribution (zip format, containing `plugin.json` + binary) @@ -155,13 +155,28 @@ Execution process: | `plg.json` | Project metadata, maintained by developer | `targets` — single-target build list (e.g. `"linux/amd64,windows/amd64"`); `bundle` — multi-platform bundle switch (default `true`) | | `plugin.json` | Build artifact manifest, auto-generated | `entry` — entry filename; `platforms` — declared platforms | -Each target produces a separate `.hmap`; binary name by platform: +Each target produces a separate `.hmap`. Subprocess plugins are plain executables with +**no platform-specific extension**: | Platform | Binary | |----------|--------| -| Linux | `plugin.so` | -| macOS | `plugin.dylib` | -| Windows | `plugin.dll` | +| Linux / macOS / Windows | `plugin.bin` | + +Inside a bundle package the per-platform entries are named `plugin.bin..`; +the kernel picks the one matching the current platform and renames it to `plugin.bin`. + +> ⚠️ **v1.0.0 breaking change**: external plugins moved from C ABI shared libraries to +> **subprocess + shared memory**. +> +> - `plugin.so` / `plugin.dylib` / `plugin.dll` are **no longer loaded**. The new kernel +> skips legacy artifacts with an actionable error instead of crashing. +> - **Business code needs no changes** — the public SDK interface is unchanged; just +> rebuild with the new `plugindev`. +> - The `entry` field in `plg.json` is **meaningless for Go plugins** now (leaving +> `plugin.so` there is harmless); it only distinguishes Lua plugins. +> - Artifacts no longer need cgo, so cross-compiling requires no target C toolchain. +> - Windows went from "only 3 stage fields delivered, no writeback" to all 16 fields +> visible plus writeback, sharing the same RPC implementation as Unix. ### Build Targets & Multi-platform Bundle @@ -269,8 +284,11 @@ func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { } ``` -At build time, `plugindev build` auto-generates C ABI bridge code (`z_bridge_gen.go` + `z_entry.c`), -shared by both Windows DLL and Linux/macOS .so builds. No manual bridge code needed. +At build time, `plugindev build` auto-generates subprocess runtime code +(`z_proc_gen.go` for the platform-independent part, plus `z_proc_shm_unix.go` / +`z_proc_shm_windows.go`). All three platforms share the same entry point and the same +RPC logic; only the cross-process resource-passing mechanism differs (inherited fds on +Unix, named kernel objects on Windows). No manual bridge code needed. ### PluginSDK Core API @@ -322,7 +340,7 @@ Tool output → valuable for LLM attention? └── No → Normal memory, no extra handling ``` -> **Note**: `Cleaner` is a Go `func` type (`json:"-"`), cannot cross C ABI boundaries, so it is unavailable for C/C++/Rust remote plugins. **Lua plugins are not affected**: pass a Lua function in the def table (`cleaner = function(text) return text end`) — the Go bridge calls it back per invocation during memory computation. +> **Note**: `Cleaner` is a Go `func` type (`json:"-"`), cannot be serialized across process boundaries, so it is unavailable for C/C++/Rust remote plugins. **Lua plugins are not affected**: pass a Lua function in the def table (`cleaner = function(text) return text end`) — the Go bridge calls it back per invocation during memory computation. #### Stage Hooks — Intervene in message processing flow @@ -527,7 +545,7 @@ Lua plugins run inside the kernel process on a gopher-lua interpreter (single Lu - **Passive callback model**: `main.lua` executes only once at load time. Afterward, tools, stage hooks, output/input channels, and registered APIs are all invoked by the kernel via callbacks into Lua functions. Plugins cannot start background tasks on their own. - **No concurrency / no long-running services**: Lua has no goroutines, coroutine scheduling, `os`/`io` libraries, or socket listening. The only outbound capability is `sdk.http.get/post` (synchronous). Any blocking loop will stall every call of that plugin while holding the lock. -- **For long-running services (listening on a port, background polling, timers) use a Go plugin** (`.so`/`.dll` built with the toolchain, which may spawn goroutines — see the webui/cli plugins). The Lua equivalent is event-driven: register tools/stage hooks/channels to be called back by the kernel, or interact with external processes via `sdk.http`. +- **For long-running services (listening on a port, background polling, timers) use a Go plugin** (`plugin.bin` built with the toolchain, which may spawn goroutines — see the webui/cli plugins). The Lua equivalent is event-driven: register tools/stage hooks/channels to be called back by the kernel, or interact with external processes via `sdk.http`. ### Plugin Structure @@ -576,7 +594,7 @@ When running inside the kernel, `sdk.*` global variables are injected by the Go ### Lua SDK API -The `sdk.*` API of Lua plugins is fully aligned with external plugins (C ABI / toolchain-built `.so`/`.dll`): registration functions raise a Lua error on failure; data functions uniformly return `(result, err)` with `err == nil` on success. Subsystems not wired by the core (e.g. SocialAPI) return empty values instead of errors. +The `sdk.*` API of Lua plugins is fully aligned with external plugins (toolchain-built `plugin.bin` subprocesses): registration functions raise a Lua error on failure; data functions uniformly return `(result, err)` with `err == nil` on success. Subsystems not wired by the core (e.g. SocialAPI) return empty values instead of errors. **Registration** @@ -594,7 +612,7 @@ The `sdk.*` API of Lua plugins is fully aligned with external plugins (C ABI / t Stage handlers receive the full context (same as external plugins): `raw_message`, `user_id`, `group_id`, `phase`, `llm_text`, `final_text`, `no_memory`, `response` (when responded), `tool_calls`, `tool_results`. -**Stage writeback (ABI v2)**: the `ctx` table passed to the handler is a reference — mutating writable fields inside the handler syncs back to the core `StageContext` (aligned with the C ABI v2 external-plugin capability): +**Stage writeback**: the `ctx` table passed to the handler is a reference — mutating writable fields inside the handler syncs back to the core `StageContext` (aligned with subprocess external-plugin capability): ```lua sdk.register_stage("on_input", function(ctx) @@ -622,7 +640,7 @@ Writable fields: `raw_message`, `llm_text`, `final_text`, `user_id`, `group_id`, | `sdk.inject_interrupt(source, channel, text)` | Interrupt delivery | | `sdk.inject_text_no_memory(source, channel, text)` | Deliver without memory computation | -**Data APIs (aligned with C ABI, all return `(result, err)`)** +**Data APIs (aligned with subprocess external plugins, all return `(result, err)`)** | Sub-table | Functions | |-----------|-----------| diff --git a/assets/docs/zh/PLUGIN_DEV.md b/assets/docs/zh/PLUGIN_DEV.md index 2b56859..2e947e0 100644 --- a/assets/docs/zh/PLUGIN_DEV.md +++ b/assets/docs/zh/PLUGIN_DEV.md @@ -30,7 +30,7 @@ type Plugin interface { | 方式 | 适用场景 | 复杂度 | |------|---------|--------| -| **动态 .so/.dll 插件(推荐)** | 独立分发的第三方插件 | 中等,使用 `plugindev` 工具链生成 | +| **子进程插件(推荐)** | 独立分发的第三方插件 | 中等,使用 `plugindev` 工具链生成 | | **内置插件** | 随 HomeAgent 一起发布 | 简单,需合入主仓库 | | **Lua 脚本插件** | 轻量快速原型 | 简单,使用 `plugindev init --lua` 生成 | @@ -115,7 +115,7 @@ myplugin/ └── thirdpart/ — 外部源码存放目录(可选) ``` -编译时自动生成 C ABI bridge 文件(`z_bridge_gen.go` + `z_entry.c`),无需手动创建。 +编译时自动生成子进程运行时文件(`z_proc_gen.go` 等),无需手动创建。 **Lua 插件**: @@ -143,8 +143,8 @@ plugindev build --replace # 追加 go.mod replace 指令(可多次 执行过程: 1. 读取 `plg.json` 的 `targets`/`bundle` 字段确定构建目标(bundle 模式优先,见下节) -2. 自动生成 C ABI bridge 代码(`z_bridge_gen.go` + `z_entry.c`,Windows 仅 `z_bridge_gen.go`) -3. **Go 插件**:执行 `go build -buildmode=c-shared`(生成 `.so` / `.dylib` / `.dll`) +2. 自动生成子进程运行时代码(`z_proc_gen.go` + `z_proc_shm_unix.go` + `z_proc_shm_windows.go`) +3. **Go 插件**:执行 `go build`(普通可执行文件,`CGO_ENABLED=0`) 4. **Lua 插件**:直接打包源码,无需编译(打包内容:`plugin.json` + `main.lua`,以及可选的 `README.md`、`LICENSE`、`thirdpart/*.lua`) 5. 生成 `plugin.json` 输出清单 6. 打包为 `.hmap` 分发包(zip 格式,内含 `plugin.json` + 二进制) @@ -156,13 +156,25 @@ plugindev build --replace # 追加 go.mod replace 指令(可多次 | `plg.json` | 项目元信息,由开发者维护 | `targets` — 单平台构建目标(如 `"linux/amd64,windows/amd64"`);`bundle` — 多平台合集开关(默认 `true`)| | `plugin.json` | 构建产物清单,`plugindev build` 自动生成 | `entry` — 入口文件名;`platforms` — 声明的支持平台 | -每个目标生成单独的 `.hmap`,二进制文件名由平台决定: +每个目标生成单独的 `.hmap`。子进程插件是普通可执行文件,**不分平台后缀**: | 平台 | 二进制 | |------|--------| -| Linux | `plugin.so` | -| macOS | `plugin.dylib` | -| Windows | `plugin.dll` | +| Linux / macOS / Windows | `plugin.bin` | + +bundle 包内按 `plugin.bin..` 区分各平台,安装时内核挑当前平台 +那份重命名为 `plugin.bin`。 + +> ⚠️ **v1.0.0 破坏性变更**:外部插件从 C ABI 动态库改为**子进程 + 共享内存**。 +> +> - `plugin.so` / `plugin.dylib` / `plugin.dll` **不再被加载**。新内核遇到旧产物 +> 会跳过并报可操作错误,不崩溃。 +> - **业务代码不需要改一行**——公开 SDK 接口零改动,只需用新版 `plugindev` 重编。 +> - `plg.json` 的 `entry` 字段对 Go 插件**已无意义**(写着 `plugin.so` 也无妨), +> 它现在只用于区分 Lua 插件。 +> - 产物不再需要 cgo,交叉编译无需目标平台 C 工具链。 +> - Windows 从「只下发 3 个 stage 字段、无写回」升级到 16 字段全可见 + 写回, +> 与 Unix 共用同一套 RPC 实现。 ### 构建目标与多平台打包(bundle) @@ -269,7 +281,7 @@ func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { } ``` -编译时 `plugindev build` 根据目标平台自动生成 C ABI bridge 代码(`z_bridge_gen.go` + `z_entry.c`),无需手动编写。Windows DLL 和 Linux/macOS .so 共享同一入口。 +编译时 `plugindev build` 自动生成子进程运行时代码(`z_proc_gen.go` 平台无关 + `z_proc_shm_unix.go` / `z_proc_shm_windows.go` 平台特定),无需手动编写。三平台共享同一入口与同一套 RPC 逻辑,仅跨进程资源传递机制不同(Unix 继承 fd,Windows 命名内核对象)。 ### PluginSDK 核心 API @@ -321,7 +333,7 @@ s.RegisterTool("weather_query", sdk.ToolDef{ └── 否 → 正常记忆,无需额外处理 ``` -> **注意**:`Cleaner` 是 Go `func` 类型(`json:"-"`),不能跨 C ABI 边界序列化,因此 C/C++/Rust 等远程插件无法使用。**Lua 插件不受此限**:def 表中直接传 Lua 函数即可(`cleaner = function(text) return text end`),Go 桥接层会在计算层调用时逐次回调 Lua。 +> **注意**:`Cleaner` 是 Go `func` 类型(`json:"-"`),不能跨进程序列化,因此 C/C++/Rust 等远程插件无法使用。**Lua 插件不受此限**:def 表中直接传 Lua 函数即可(`cleaner = function(text) return text end`),Go 桥接层会在计算层调用时逐次回调 Lua。 #### 阶段钩子 — 干预消息处理流 @@ -526,7 +538,7 @@ Lua 插件运行在内核进程内的 gopher-lua 解释器中(单 Lua 状态 + - **被动回调模型**:`main.lua` 仅在加载时执行一次,此后插件的工具、阶段钩子、输出/输入通道、注册 API 全部由内核事件驱动回调 Lua 函数;插件不能自己启动后台任务。 - **无并发/无常驻服务能力**:Lua 侧没有 goroutine、协程调度、`os`/`io` 库和 socket 监听能力,唯一主动出站通道是 `sdk.http.get/post`(同步请求)。任何阻塞循环都会持锁卡死该插件的所有调用。 -- **常驻服务(如监听端口、后台轮询、定时任务)请使用 Go 插件**(工具链编译的 `.so`/`.dll`,可自行启动 goroutine,参见 webui/cli 插件)。Lua 插件的等价做法是事件驱动:注册工具/阶段钩子/通道由内核回调,或经 `sdk.http` 与外部进程交互。 +- **常驻服务(如监听端口、后台轮询、定时任务)请使用 Go 插件**(工具链编译的 `plugin.bin`,可自行启动 goroutine,参见 webui/cli 插件)。Lua 插件的等价做法是事件驱动:注册工具/阶段钩子/通道由内核回调,或经 `sdk.http` 与外部进程交互。 ### 插件结构 @@ -575,7 +587,7 @@ lua main.lua ### Lua SDK API -Lua 插件的 `sdk.*` API 与外部插件(C ABI / 工具链编译的 `.so`/`.dll`)能力完全对齐:注册类函数调用即时报错(抛 Lua error),数据类函数统一返回 `(result, err)`,`err` 为 nil 表示成功。核心未装配的子系统(如 SocialAPI)返回空值而非报错。 +Lua 插件的 `sdk.*` API 与外部插件(工具链编译的 `plugin.bin` 子进程)能力完全对齐:注册类函数调用即时报错(抛 Lua error),数据类函数统一返回 `(result, err)`,`err` 为 nil 表示成功。核心未装配的子系统(如 SocialAPI)返回空值而非报错。 **注册类** @@ -593,7 +605,7 @@ Lua 插件的 `sdk.*` API 与外部插件(C ABI / 工具链编译的 `.so`/`.d `register_stage` 的 handler 收到完整上下文(与外部插件一致):`raw_message`、`user_id`、`group_id`、`phase`、`llm_text`、`final_text`、`no_memory`、`response`(已响应时)、`tool_calls`、`tool_results`。 -**Stage 写回(ABI v2)**:handler 收到的 `ctx` 是引用 table——在 handler 内直接修改可写回字段并同步至内核 `StageContext`(与 C ABI v2 外部插件能力对齐): +**Stage 写回**:handler 收到的 `ctx` 是引用 table——在 handler 内直接修改可写回字段并同步至内核 `StageContext`(与子进程外部插件能力对齐): ```lua sdk.register_stage("on_input", function(ctx) @@ -621,7 +633,7 @@ end) | `sdk.inject_interrupt(source, channel, text)` | 中断投递 | | `sdk.inject_text_no_memory(source, channel, text)` | 免记忆投递 | -**数据类(与 C ABI 对齐,均返回 `(result, err)`)** +**数据类(与子进程外部插件对齐,均返回 `(result, err)`)** | 子表 | 函数 | |------|------| diff --git a/docs/zh/plugin-interface-matrix.md b/docs/zh/plugin-interface-matrix.md index 7ef44f7..e771b94 100644 --- a/docs/zh/plugin-interface-matrix.md +++ b/docs/zh/plugin-interface-matrix.md @@ -1,11 +1,14 @@ # 外部插件接口不变矩阵(多进程化整改基线) -> 状态:**基线 v1**(2026-08-31,update 分支) +> 状态:**完成 v2**(2026-09-03)——迁移已落地并上生产,内核 v1.0.0。 > 目的:钉死「暴露给外部插件的接口不变」这一约束的**合同面**——迁移前、迁移后外部插件看到/调用的 SDK 接口完全一致; > 所有改造落在**核心(homed 侧)+ 工具链(plugindev)**,外部插件业务代码零改动,只需用新 plugindev 重编。 > -> 维护规则:每次改动公开 SDK 接口面 `third_party/homeagent-sdk/sdk/` 或 bridge 模板 `tools/plugindev/templates.go` 后, -> 必须同步更新本矩阵;`11.1~11.6` 任一落地后,在对应行标注「已修复」。 +> **结果(已验证)**:`git diff third_party/homeagent-sdk/sdk/` 全程为空;17 个 `example/*/plugin.go` 逐字节未改 +> (`git status example/` 无输出);生产 17 插件全部经子进程通道运行。 +> +> 维护规则:每次改动公开 SDK 接口面 `third_party/homeagent-sdk/sdk/` 或模板 `tools/plugindev/templates/` 后, +> 必须同步更新本矩阵。 > > 权威编号:plan.md 第 11 节(11.1~11.9)。本文档只做接口面盘点,不做实现。 @@ -111,9 +114,14 @@ type Plugin interface { ## 三、合同面 B:bridge 51 个 method id ↔ SDK 方法映射(改造基线) -> 文件:`third_party/homeagent-sdk/tools/plugindev/templates.go` 的 `tmplLinuxBridge`。 -> 迁移后这些整数 method id **改为 RPC method 名**(迁移评估 3.2),语义不变、编号扔掉。 -> 下表是「51 个 case 平移为 method 名」的完整清单,也是新 RPC 协议的一等公民。 +> ⏹️ **已完成(2026-09-03)**:整数 method id 已全部平移为 RPC method 名字符串, +> 定义在 `internal/plugin/proc/protocol.go` 的 `Method*` 常量(共 60 个,含内核→插件方向)。 +> 原 `tmplLinuxBridge` 与 `meta.Core` 整数表**均已删除**。 +> +> 两个遗留点:`case 25`(`CoreFreeString`)无对应 method(内存管理是 C 层特有问题); +> `io.setToolBlocks` 已定义但内核侧仍返回未实现(C ABI 时代也是空实现,非回归)。 +> +> 下表保留作为历史对照。 | # | method id(今天 C ABI) | SDK 背的方法 | 迁移后 RPC method 名(建议) | |---|---|---|---| @@ -182,7 +190,11 @@ type Plugin interface { ## 四、合同面 C:StageContext 跨 ABI 现状 → 共享内存目标 -### C1. 今天(C ABI 副本模型):插件只看到 10 个字段 +> ✅ **已达成(2026-09-03)**:子进程插件现在看到全部 18 个字段(枚举见 +> `internal/plugin/proc/shm.go`),且可写回。生产实测:sanitizer 在另一个进程里 +> 改写 13590 字节文本,内核读到改写结果(`stage post_action 改写了 1 个字段`)。 + +### C1. 迁移前(C ABI 副本模型):插件只看到 10 个字段 `stageContextWritable`(templates.go:762)下发/回传的字段: @@ -193,18 +205,31 @@ raw_message user_id group_id phase llm_text final_text no_memory **看不到的 6 个字段**:`ContextMsgs` / `ReasoningContent` / `TokenUsage` / `Memory` / `Extra` / `Errors` -### C2. 迁移后(共享内存 + 锁仲裁):插件可看到/改写全部 16 个字段 +### C2. 迁移后(共享内存 + 锁仲裁):插件可看到/改写全部字段 — ✅ 已实现 -`ShmStageCtx`(迁移评估 3.3)· 插件进程内保留原生 `StageContext`,handler 照常读写, -`Lock/RLock` 映射到跨进程锁仲裁 RPC(`stage.lock`/`stage.unlock`),handler 返回时脏字段写回共享段。 +字段级 `Slice{Off,Len}` 描述符 + 内核仲裁锁。插件进程内保留原生 `StageContext`, +handler 照常读写,`Lock/RLock` 映射到跨进程锁仲裁 RPC(`stage.lock`/`stage.unlock`), +handler 返回时脏字段写回共享段。 -→ **接口形式不变,能力变强**(这是「能力断层消除」合同面的一部分:外部插件拿回 ContextMsgs 等)。 +**关键设计决定**:全部子进程插件共享**同一块 memfd**。第一版设计是每插件一段, +那会退化成副本模型,复现 §8.4 的 35.8~36.8% lost update。 -### C3. 11.3 修复的合同面定义(lost update) +→ **接口形式不变,能力变强**(能力断层消除:外部插件拿回 ContextMsgs 等)。 -今天 `stageContextWritable` **无条件回传 10 个字段的当前快照**——两个插件(sanitizer 改 ToolResults + -weather 只读)并行时,weather 的回传会覆盖 sanitizer 的清洗结果(实测 1.6~4.3%)。 -迁移后共享内存模型天然解决(并发改写同一对象);迁移前需 `stageContextWritable` 只回传**真正变更**的字段。 +Windows 同步受益:从「只下发 3 字段、无写回」升到全字段可见 + 写回, +与 Unix 共用同一套 RPC 实现与共享段布局。 + +### C3. lost update 的合同面定义 — ✅ 已消除 + +C ABI 时代 `stageContextWritable` **无条件回传 10 个字段的当前快照**——两个插件 +(sanitizer 改 ToolResults + weather 只读)并行时,weather 的回传会覆盖 sanitizer +的清洗结果(实测 1.6~4.3%,高并发下 35.8~36.8%)。 + +Part 0.2 先做了过渡补丁(只回传真正变更的字段);Part 4 的共享内存模型从根上解决 +(字段级描述符 + 锁仲裁,并发改写同一对象)。 + +回归基线:`TestPlugin_FiveProcessesConcurrentAppendNoLostUpdate`、 +`TestSegment_ProductionScenario_SanitizerNotOverwrittenByWeather`。 --- @@ -224,36 +249,69 @@ weather 只读)并行时,weather 的回传会覆盖 sanitizer 的清洗结 ## 六、迁移后外部插件「新获得」的能力(合同面扩展——只增不减) -| 能力 | 今天 | 迁移后 | -|---|---|---| -| 事件订阅 `Events().Subscribe`(case 23/24) | ❌ 空实现 | ✅ 事件环(EvtRing + eventfd + 独立游标) | -| `SetToolBlocks` 多模态注入 | ❌ 空实现 | ✅ 二进制落 arena,Slice 描述符回传 | -| `ContextMsgs`/`ReasoningContent`/`TokenUsage`/`Memory`/`Extra`/`Errors` | ❌ 看不到 | ✅ 共享内存全字段 | -| 插件崩溃隔离 | ❌ panic 带崩 homed | ✅ 子进程独立崩溃 | -| 热重载 `.so` | ❌ `DF_1_NODELETE` no-op | ✅ 同路径替换 `.bin` 即生效 | -| 工具超时取消 | ❌ cgo 不可中断(泄漏线程) | ✅ `Process.Kill()` 真取消 | -| `output_send` 结果 | ❌ 永远假成功 | ✅ 可同步等真实结果 | -| Lua/Windows DLL 路径 | ❌ 三套 ABI 分裂 | ✅ 收敛为单一 RPC 实现 | +| 能力 | 迁移前 | 迁移后 | 实际结果 | +|---|---|---|---| +| 事件订阅 `Events().Subscribe`(case 23/24) | ❌ 空实现 | ✅ 事件环(EvtRing + eventfd + 独立游标) | ✅ 已接线(当前零用户) | +| `SetToolBlocks` 多模态注入 | ❌ 空实现 | ✅ 二进制落 arena,Slice 描述符回传 | ⚠️ method 已定义,内核侧仍未实现 | +| `ContextMsgs`/`ReasoningContent`/`TokenUsage`/`Memory`/`Extra`/`Errors` | ❌ 看不到 | ✅ 共享内存全字段 | ✅ 18 字段全可见可写 | +| 插件崩溃隔离 | ❌ panic 带崩 homed | ✅ 子进程独立崩溃 | ✅ 测试 + 生产验证 | +| 热重载 `.so` | ❌ `DF_1_NODELETE` no-op | ✅ 同路径替换 `.bin` 即生效 | ✅ 生产实测 | +| 工具超时取消 | ❌ cgo 不可中断(泄漏线程) | ✅ `Process.Kill()` 真取消 | ✅ 整套新架构零 cgo | +| `output_send` 结果 | ❌ 永远假成功 | ✅ 可同步等真实结果 | ✅ 生产实测 `map[status:sent]` | +| Lua/Windows DLL 路径 | ❌ 三套 ABI 分裂 | ✅ 收敛为单一 RPC 实现 | ⚠️ Windows 已收敛;Lua 仍独立(留待后续) | -**刻意不给**(权限梯度显式化,非技术限制):`Selftest`/`Supervisor`/`Tracker`/`Status`/`Adapter`/`Config`/`Tool`/`Indexer`/`OutputChan`/`Publish`(内核内部机制)。 +**三项未完全兼得的说明**: + +- `SetToolBlocks`:`io.setToolBlocks` 已在 protocol 定义并划入 `CapCore`,但内核侧 handler + 仍返回未实现。C ABI 时代它也是空实现(§1.4),故**不是回归**,但也没兑现承诺。 +- Lua:`lua_plugin.go`/`dynamic_lua.go` 仍走自己的路径。Lua 经解释器不经 C ABI, + 不属于本轮要消除的 6 类缺陷,因此不阻塞。收敛第三套 ABI 是独立优化。 +- 事件订阅:机制已完成(内核侧 `EvtRing` + 模板侧 `evtConsumerLoop`), + 但**无任何现有插件使用 `Events().Subscribe`**,所以生产上未经真实负载检验。 + +**刻意不给**(权限梯度显式化,非技术限制):`SelftestAPI`/`SupervisorAPI`/`TrackerAPI`/ +`StatusAPI`/`AdapterAPI`/`ConfigAPI`/`ToolAPI`/`IndexerAPI`/`OutputChanRaw`/`EventPublish` +(内核内部机制)。清单与理由记在 `internal/plugin/proc/capability.go` 的 +`withheldCapabilities`,`TestCapability_WithheldListIsDocumented` 守护。 + +这一项从「C ABI 表达能力的意外产物」变成**显式策略**:以前拿不到是因为 +C 结构体不好传函数指针(那是运气,任何人给 dispatch 加个 case 就能捅穿); +现在是三道闸:类型层(`procCore` 命名字段不嵌入)+ 能力集(manifest 声明) ++ RPC 边界(返回明确错误而非静默忽略)。 --- -## 七、整改推进时的接口冻结检查点 +## 七、接口冻结检查点(全部已通过) -1. **阶段 2(子进程通道原型)完成时**:`plugindev` 用 `tmplProcMain` 重编 weather → `weather.bin` → 端到端跑通。 - 验收:weather 业务代码与 `build/` 目录下旧 `.so` 时代的 `plugin.go` **逐字节可对比**(唯一改动是被工具链改写,非手工)。 -2. **阶段 3(共享内存)完成时**:任意改写型插件(sanitizer/weather 并发)在子进程下并发改写 StageContext, - 丢失率 = 0%(对比今天 35.8~36.8%)。 -3. **阶段 5 完成时**:17 个外部插件全部 `.bin` 化、cabi 删除;执行一遍全量 `go build ./...` + example 编译。 -4. **任何时候**:`git diff` 公开 SDK `sdk/` 目录为零(接口冻结的硬证据)。 +1. ✅ **阶段 2(子进程通道原型)**:`plugindev` 重编 weather → `plugin.bin` → 端到端跑通。 + 验收:weather 业务代码逐字节未改(`git status example/` 无输出)。 +2. ✅ **阶段 3(共享内存)**:子进程并发改写 StageContext 丢失率 = 0% + (`TestPlugin_FiveProcessesConcurrentAppendNoLostUpdate` 与 + `TestSegment_ProductionScenario_SanitizerNotOverwrittenByWeather`)。 +3. ✅ **阶段 5**:17 个外部插件全部 `.bin` 化、cabi 删除(-3198 行); + `go build ./...` 与全仓 `go test ./...` 均通过。 +4. ✅ **全程**:`git diff third_party/homeagent-sdk/sdk/` 为零——接口冻结的硬证据。 + +生产端到端(2026-09-03,真实 QQ 消息): + +``` +input from qq → response (83293ms, tools=[qq_get_message qq_get_history + output_send__qq output_send__qq qq_mark_read]) +[sanitizer] cleaned 2 bytes (before=13590 after=13588) +[proc] sanitizer stage post_action 改写了 1 个字段 +tool output_send__qq result: 已通过 [qq] 通道发送: map[status:sent] +``` --- ## 八、关联文档 -- `docs/zh/架构迁移评估.md` — 完整论证(§3.2 method id 平移、§3.3 数据面、§3.4 SDK 封装、§3.5 回调型资源) +- `docs/zh/架构迁移评估.md` — 完整论证(§3.2 method id 平移、§3.3 数据面、§3.4 SDK 封装、§3.5 回调型资源、§3.8 能力对齐) +- `docs/zh/plugin-migration-plan.md` — Part 0~6 执行计划与完成实录(含 Part 6.5 生产切换、Part 6.6 压测) - `plan.md` §11 — 11.1~11.9 修复清单(唯一权威编号) -- `third_party/homeagent-sdk/sdk/` — 合同面 A 的代码实现 -- `third_party/homeagent-sdk/tools/plugindev/templates.go` — bridge 模板(合同面 B 的代码实现) -- `docs/zh/experiments/plugin-arch/` — 18 项可行性实验(跨进程并发改写零丢失等数字来源) \ No newline at end of file +- `third_party/homeagent-sdk/sdk/` — 合同面 A 的代码实现(全程零 diff) +- `internal/plugin/proc/protocol.go` — 合同面 B 的代码实现(`Method*` 常量,取代已删的 bridge 模板) +- `internal/plugin/proc/shm.go` — 合同面 C 的代码实现(共享段布局与 18 字段枚举) +- `internal/plugin/proc/capability.go` — 权限梯度(capability 组 + `withheldCapabilities`) +- `third_party/homeagent-sdk/tools/plugindev/templates/` — 子进程运行时模板(三文件) +- `docs/zh/experiments/plugin-arch/` — 18 项可行性实验 + `19-migration-verify/` 迁移执行期工具 \ No newline at end of file diff --git a/docs/zh/plugin-migration-plan.md b/docs/zh/plugin-migration-plan.md index 362837d..6c318fd 100644 --- a/docs/zh/plugin-migration-plan.md +++ b/docs/zh/plugin-migration-plan.md @@ -404,54 +404,72 @@ $ git diff third_party/homeagent-sdk/sdk/ --- -## Part 6:迁移与收尾(阶段 5.1~5.4,~2 周) +## Part 6:迁移与收尾(阶段 5.1~5.4,~2 周)— ✅ **已完成**(2026-09-03) -> 依据:迁移评估 §4.5 双通道共存、§5 权限梯度。逐插件迁移,随时回退。 +> 依据:迁移评估 §4.5 双通道共存、§5 权限梯度。 +> +> ⚠️ **实际执行偏离计划的一处**:原计划「逐插件迁移,随时回退」。 +> 用户决策改为**彻底舍弃 `.so` 能力,无回退通道**(不做 `--cabi` 开关), +> 本轮直接删 `internal/plugin/cabi/`,生产全量切换。代价是某插件出问题 +> 只能紧急修复或 `git revert` 整批。因此下方【V】的「`.so` ↔ `.bin` 混跑」 +> 不再适用——新内核根本不认 `.so`。 ### 修改 -- 【M】17 个外部插件逐个用新 plugindev 重编为 `.bin`(`plugin_install(overwrite=true)`),每个回归验证。 -- 【M】`plugins/` 目录逐个把 `entry` 从 `plugin.so` 改为 `plugin.bin`。 -- 【M】删除 `internal/plugin/cabi/`(1096 行)+ bridge 模板 `tmplLinuxBridge`/`tmplBridge`(385 行)+ `dynamic_dll_*`/`dynamic_loader_windows.go`。 -- 【M】权限梯度显式化:manifest 声明 caps + 内核侧白名单(`Selftest`/`Supervisor`/`Tracker`/`Status`/`Adapter`/`Config`/`Tool`/`Indexer`/`OutputChan`/`Publish` 确认不给)。 -- 【M】`lua_plugin.go`/`dynamic_lua.go`:统一走 RPC(收敛三套 ABI 为单一 RPC)。 -- 【M】文档:`PLUGIN_DEV.md` 更新、迁移说明。 +- ✅【M】**6.1** 工具链 entry 语义收敛(SDK 仓 `9f84412`):`isProcEntry` 删除,Go 插件一律产出 `plugin.bin` 不看 entry 值;`templates.go` 1296→516 行。 +- ✅【M】**6.3** 17 插件全量重编(`1d7f011`):16 个×3 平台 + qq×1;`git status example/` 无输出(业务代码零改动)。 +- ✅【M】**6.5** 生产切换(`62bdfa2`):经 `pluginmgr` 的 hmap 正规通道安装,17/17 成功且 `config_kept=true`。 +- ✅【M】**6.6** 压测 + 版本 1.0.0 + 文档(`2572688`、`670efcd`、tag `v1.0.0`)。 +- ✅【M】**6.2** 内核侧 Windows(`d027c96`)+ 删 C ABI(`b20121f`,-3198 行):删 `internal/plugin/cabi/`(1156)、`dynamic_dll_windows.go`(272)、`dynamic_loader_unix.go`(79) + bridge 模板;新增 `shmalloc_windows.go` + `evtfd_windows.go` + `shmpass_{unix,windows}.go`;顺带修 macOS pipe 写端被 GC 回收的真 bug。 +- ✅【M】**6.4** 权限梯度显式化(`2ebdb9a`):54 个 method 划入 11 个 capability 组;`coreHandler.Handle` 入口强制;`withheldCapabilities` 表记录 10 项刻意不提供的内核机制及理由(`SelftestAPI`/`SupervisorAPI`/`TrackerAPI`/`StatusAPI`/`AdapterAPI`/`ConfigAPI`/`ToolAPI`/`IndexerAPI`/`OutputChanRaw`/`EventPublish`)。 +- ⏭️【M】`lua_plugin.go`/`dynamic_lua.go` 统一走 RPC —— **留待后续**。Lua 走解释器不经 C ABI,不阻塞本轮目标(消除 C ABI 前提缺陷)。收敛第三套 ABI 是独立优化。 +- ✅【M】文档:本文与 `plugin-interface-matrix.md` 更新;切换实录见下方。 ### 审查 -- 【R】每删一个 cabi 依赖项,`go build ./...` + `go vet ./...` 干净。 -- 【R】权限梯度:外部插件无权访问的 API 在 RPC 边界被**拒绝**(非忽略)。 -- 【R】接口冻结:`sdk/` 零 diff。 +- ✅【R】每删一个 cabi 依赖项,`go build ./...` + `go vet ./...` 干净。 +- ✅【R】权限梯度:被拒 API 在 RPC 边界返回**明确错误**(非忽略)。错误消息含四要素:哪个插件、哪个调用、缺什么能力、在哪声明。`TestCapability_DeniedErrorIsActionable` 守护。 +- ✅【R】接口冻结:`git diff third_party/homeagent-sdk/sdk/` 全程为空。 ### 验证(全量回归) -- 【V】17 插件每个 `.bin` 独立回归(工具/设置/通道/阶段)。 -- 【V】`.so` ↔ `.bin` 混跑集群冒烟(Part 1 分派 + 双通道共存)。 -- 【V】`make test` 全量绿 + `go build ./...`。 -- 【V】内存/RSS 对比:迁移后常驻 ≤ 基线 +29MB(实验 5 量级)。 -- 【V】工具调用 RPC 延迟 p50 ≤ 20µs 量级(实验 11)。 +- ✅【V】17 插件经 `plugin_install(overwrite=true)` 加载,工具/设置/通道/阶段 e2e。 +- ⏭️【V】~~`.so` ↔ `.bin` 混跑集群冒烟~~ —— 不适用(无回退通道,见上方偏离说明)。改为验证**新内核面对旧 `.so` 给可操作错误且不崩溃**,已在真实二进制上确认。 +- ✅【V】`make test` 全量绿 + `go build ./...`。 +- ⚠️【V】内存:**未达成计划目标**。15 个插件进程 RSS=88.0MB / PSS=87.9MB,远超「基线 +29MB」。根因是每插件静态链接整个 Go runtime,15 个不同二进制无共同物理页可映射(PSS/RSS 99.9% vs 基线 44%)。这是「每插件独立二进制」的固有代价,实际开销高于 §4.3 乐观估计。压缩方向:共享 launcher 二进制 + 各自业务模块。 +- ✅【V】工具调用 RPC 延迟 24.1µs(实验 11 基线 19.6µs,同量级)。 -**Part 6 出口条件**:全部外部插件 `.bin` 化,cabi 删除,接口零改动,权限显式化,无回归。 +**Part 6 出口条件**:全部外部插件 `.bin` 化 ✅,cabi 删除 ✅,接口零改动 ✅,权限显式化 ✅,无回归 ✅。 --- ## 最终验收清单(对照接口不变矩阵 §7 检查点) -| # | 检查点 | 通过标准 | -|---|---|---| -| 1 | 公开 SDK 接口冻结 | `git diff third_party/homeagent-sdk/sdk/` **为空**(全程) | -| 2 | 外部插件业务代码零改动 | 17 个 `example/*/plugin.go` 与基线逐字节可比 | -| 3 | 17 插件 `.bin` 化 | 全部经 `plugin_install` 加载,工具/设置/通道/阶段 e2e | -| 4 | cabi 删除 | `internal/plugin/cabi/` 与 bridge 模板不存在 | -| 5 | 崩溃隔离 | 插件 kill 只退出自身,homed 存活 | -| 6 | 热重载 | 同路径换 `.bin` 即生效,无需重启 | -| 7 | 并发改写 | 跨进程 stage 丢失率 0%(对照今天 35.8~36.8%) | -| 8 | 事件订阅 | 外部插件 `Events().Subscribe` 可用 | -| 9 | 多模态 | `SetToolBlocks` 非空实现 | -| 10 | 超时取消 | 工具超时可 `Process.Kill()`,零泄漏 | -| 11 | output_send | 真实结果返回(非假成功) | -| 12 | 权限梯度 | 内部专属 API 在 RPC 边界拒绝 | -| 13 | 内存/延迟 | 常驻 +≤29MB,RPC p50 ≤20µs 量级 | +| # | 检查点 | 通过标准 | 结果 | +|---|---|---|---| +| 1 | 公开 SDK 接口冻结 | `git diff third_party/homeagent-sdk/sdk/` **为空**(全程) | ✅ 每次审查均确认 | +| 2 | 外部插件业务代码零改动 | 17 个 `example/*/plugin.go` 与基线逐字节可比 | ✅ `git status example/` 无输出 | +| 3 | 17 插件 `.bin` 化 | 全部经 `plugin_install` 加载,工具/设置/通道/阶段 e2e | ✅ 17/17,`config_kept=true` | +| 4 | cabi 删除 | `internal/plugin/cabi/` 与 bridge 模板不存在 | ✅ -3198 行(`b20121f`) | +| 5 | 崩溃隔离 | 插件 kill 只退出自身,homed 存活 | ✅ `TestRealPlugin_CrashDoesNotKillKernel` | +| 6 | 热重载 | 同路径换 `.bin` 即生效,无需重启 | ✅ 生产实测(`unloaded (config kept)` → 重载) | +| 7 | 并发改写 | 跨进程 stage 丢失率 0%(对照今天 35.8~36.8%) | ✅ `TestPlugin_FiveProcessesConcurrentAppendNoLostUpdate` | +| 8 | 事件订阅 | 外部插件 `Events().Subscribe` 可用 | ✅ 事件环已接线(当前零用户) | +| 9 | 多模态 | `SetToolBlocks` 非空实现 | ⚠️ method 已定义并划入 core 能力,内核侧仍返回未实现 | +| 10 | 超时取消 | 工具超时可 `Process.Kill()`,零泄漏 | ✅ 整套新架构零 cgo | +| 11 | output_send | 真实结果返回(非假成功) | ✅ 生产实测 `map[status:sent]` | +| 12 | 权限梯度 | 内部专属 API 在 RPC 边界拒绝 | ✅ 12 项测试(`2ebdb9a`) | +| 13 | 内存/延迟 | 常驻 +≤29MB,RPC p50 ≤20µs 量级 | ⚠️ 延迟 24.1µs 达标;内存 88MB **未达标** | + +**两项未完全达标的说明**: + +- **#9 SetToolBlocks**:`io.setToolBlocks` 已在 protocol 定义并划入 `CapCore`, + 但内核侧 handler 仍返回未实现。C ABI 时代它也是空实现(§1.4), + 故**不是回归**,但也没兑现 §3.8 的承诺。当前无插件使用。 +- **#13 内存**:15 个进程 RSS=88.0MB,远超「基线 +29MB」。根因是每插件 + 静态链接整个 Go runtime,15 个不同二进制无共同物理页(PSS/RSS 99.9% + vs 基线 44%)。实验 5 的基线用的是 2.68MB 最小插件,而真实插件 3.1~14.8MB, + 绝对数字不可比。结构性指标(均摊线程 5.5 vs 4.9)同量级。 --- @@ -468,3 +486,147 @@ $ git diff third_party/homeagent-sdk/sdk/ --- *规划:2026-08-31,update 分支。Part 编号与其依赖的 plan.md/迁移评估阶段对应。* + +--- + +## Part 6.5 生产切换实录(2026-09-03) + +### 执行顺序(先换二进制,再装包) + +``` +1. systemctl stop homeagent +2. 换 /usr/local/bin/homed +3. 起服务 —— 15 个 .so 插件报可操作错误被跳过,homed 与 16 个内置正常 +4. 逐个 POST 装 17 个 hmap(overwrite=true) +5. 重启核对 +``` + +**为何不能反过来**:若先装包,旧 homed 的 `StopAndUnload` 会停掉 qq +消息通道,而它又无法加载 `.bin`,会卡在「插件全挂」的状态。 + +第 3 步顺带在真实二进制上验证了 Part 6.2 的可操作错误: + +``` +[plugin] dynamic weather: plugin weather: 检测到旧 C ABI 产物(plugin.so/.dll/.dylib)。 +外部插件已改为子进程模式,请用新版 plugindev 重编产出 plugin.bin(业务代码无需修改) +``` + +不崩溃,只跳过该插件。 + +### 走 hmap 正规通道,而非手工拷贝 + +第一版切换脚本是手工拷 `plugin.bin` + 手改 `plugin.json` 的 entry —— +那等于**重新实现了一遍 hmap 解包逻辑,且实现得更差**。漏掉的东西: + +| | 手工拷贝 | hmap 正规通道 | +|---|---|---| +| `platforms` 字段 | 漏了 | 包内 manifest 本来就写对 | +| 平台二进制选择 | 硬编码 `_linux_amd64` | `platformBinary()` 按 runtime 选 | +| `overwrite` 语义 | 无 | `StopAndUnload` **保留配置表** | +| 失败回滚 | 无 | `os.Rename` 备份,解包失败自动恢复 | +| 校验 | 只查文件存在 | `validatePackage` 查 manifest + 各平台二进制齐全 | + +配置保留那条尤其关键:生产 17 个插件都有配置(qq 账号、weather 默认城市、 +browser profile 路径)。手工脚本恰好没碰配置表所以侥幸不丢,但那是运气不是设计。 + +最终实现:POST 到 `127.0.0.1:9876/plugins`,传 `{path, overwrite:true}`。 +保留的一个设计是**先全部校验再动手**——任一插件缺 hmap 就整批中止, +因为新 homed 不认 `.so`,「一半装了一半没装」的中间态最难排查。 + +### 结果 + +``` +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)。 +**唯一回滚路径**是恢复该目录 + 回滚 homed 二进制。 + +### 生产端到端验证(真实 QQ 消息) + +``` +input from qq → response (83293ms, tools=[qq_get_message qq_get_history + output_send__qq output_send__qq qq_mark_read]) +``` + +逐环节: + +- **输入**:qq 子进程收 webhook → 经 RPC 报给内核 → agent 主循环 +- **工具调用**:5 次跨进程调用全部成功(内核反向调用进子进程执行) +- **stage 改写生效**(最关键的一条): + ``` + [sanitizer] cleanToolCallLeakage: 2 bytes removed + [sanitizer] cleaned 2 bytes (before=13590 after=13588) + [proc] sanitizer stage post_action 改写了 1 个字段 + ``` + sanitizer 在**另一个进程里**改了 StageContext,内核读到了改写结果。 + 13590 字节文本经共享段传递、被改写、写回,全程未拷贝整个上下文。 +- **输出真的送达**:`tool output_send__qq result: 已通过 [qq] 通道发送: map[status:sent]` + —— 直接验证 Part 0.1 修的 output_send 假成功缺陷(§9.4) +- **arena 生命周期正常**:每次 stage 结束都压实回收(单次最高 15802 字节),无泄漏累积 + +这一次对话触发约 20 次 stage、5 次工具调用、2 次输出发送,跨越 15 个插件子进程。 +旧架构下同样流程有三处会静默出问题:stage 并发写丢字段(§8.4 实测 35.8~36.8% +lost update)、output_send 假成功、cgo 超时泄漏 goroutine。现在这些在日志里可见且正确。 + +--- + +## Part 6.6 压测与延迟实测 + +基准与压测在代码里(`internal/plugin/proc/bench_test.go` + `streaming_test.go`), +非独立脚本——随代码演进自动跑,不会腐坏。 + +| 项目 | 实测 | 基线 | 判断 | +|---|---|---|---| +| 工具调用 RPC 往返 | 24.1 µs | 实验 11: 19.6 µs | 同量级 | +| 锁仲裁(内核侧) | 0.76 µs | — | 见下注 | +| 事件环写入 | 95 ns | — | 亚微秒 | +| 事件环并发写入 | 83 ns | — | 无锁竞争恶化 | +| 完整 stage 往返 | 132 µs | — | 含 3 次进程间往返 | +| 共享段编解码 | 3.7 µs | — | 占 stage 的 2.8% | + +**锁仲裁 0.76µs 不可与实验 3 的 19.40µs 对照**——测的不是同一个东西: +实验 3 测插件经 RPC 请求锁的完整跨进程往返,本基准只测内核侧 +`lockRegistry.acquire/release`。真实成本仍在 20µs 量级。基准原名 +`BenchmarkStageLockRoundTrip` 有误导性,已改为 `BenchmarkStageLockArbitration`。 + +**stage 往返 132µs 的成本构成**:共享段编解码只占 3.7µs,其余是 +**一次 stage 要走 3 次进程间往返**(`stage.invoke` + 插件侧反向的 +`stage.lock` / `stage.unlock`)。相对 LLM 往返 2-8 秒可忽略; +要优化的方向是把 lock/unlock 合入 `stage.invoke` 的请求/应答。 + +### 流式压测(§4.3 标记「风险高」的那一项) + +``` +5000 次 Publish + 每条睡 20µs 的慢消费者 + 实测 2.29ms,均摊 457 ns/token + 同步语义理论下限 100ms + +订阅者 1 个:1.547ms(515 ns/次) +订阅者 8 个:1.518ms(506 ns/次) ← 无线性恶化 + +环溢出(无消费者写 30000 次,cap=8192):均摊 35 ns/次 ← 仍 O(1) +``` + +2.29ms 与实验 4 的数字完全一致(那次也是 2.29ms / 0.46µs per token), +post-and-forget 在实现中成立。第三项的意义:消费者完全停摆时写端覆盖 +最旧 slot,这条路径仍是 O(1),故「插件卡住」不会连带拖慢内核主循环。 + +--- + +## 版本号 + +v1.0.0(tag 已打)。公开 SDK 接口零改动,但产物形态从 `plugin.so` 变为 +`plugin.bin`,0.9.x 内核不会识别——不可互操作的破坏性变化,故跃主版本号。 + +⚠️ **Makefile 陷阱**:`VERSION ?= $(shell git describe --tags --dirty)` +意味着实际注入值来自 git tag,`meta.go` 里的默认值只在不带 ldflags 时生效。 +打 tag 前 `make build` 注入的是 `v0.9.1-56-g2572688-dirty`。 + +同时删掉 C ABI 时代的死常量(`ABIVersion`/`CABINum`/51 个 `Core` +整数 ID)——随 Part 6.2 删 `internal/plugin/cabi/` 就已无使用者, +留着会让人以为 C 层协商还在生效,或以为加 method 要同步维护那张整数表。