docs: Part 6.6 文档收尾 —— 迁移计划/接口矩阵/PLUGIN_DEV 全量更新

## 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 处),其余全部清理。
This commit is contained in:
JianFeeeee
2026-09-03 08:12:30 +08:00
parent 670efcd426
commit 2a03a83ce0
4 changed files with 348 additions and 98 deletions

View File

@ -1,11 +1,14 @@
# 外部插件接口不变矩阵(多进程化整改基线)
> 状态:**基线 v1**2026-08-31update 分支)
> 状态:**完成 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 {
## 三、合同面 Bbridge 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<Method>` 整数表**均已删除**
>
> 两个遗留点:`case 25``CoreFreeString`)无对应 method内存管理是 C 层特有问题);
> `io.setToolBlocks` 已定义但内核侧仍返回未实现C ABI 时代也是空实现,非回归)。
>
> 下表保留作为历史对照。
| # | method id今天 C ABI | SDK 背的方法 | 迁移后 RPC method 名(建议) |
|---|---|---|---|
@ -182,7 +190,11 @@ type Plugin interface {
## 四、合同面 CStageContext 跨 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` 多模态注入 | ❌ 空实现 | ✅ 二进制落 arenaSlice 描述符回传 |
| `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` 多模态注入 | ❌ 空实现 | ✅ 二进制落 arenaSlice 描述符回传 | ⚠️ 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 项可行性实验(跨进程并发改写零丢失等数字来源
- `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/` 迁移执行期工具

View File

@ -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 runtime15 个不同二进制无共同物理页可映射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 | 内存/延迟 | 常驻 +≤29MBRPC 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 | 内存/延迟 | 常驻 +≤29MBRPC 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 runtime15 个不同二进制无共同物理页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-31update 分支。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 个 hmapoverwrite=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 个残留 .so17 个 plugin.bin 均有执行位
17 个 manifest 的 entry 均为 plugin.bin无 .bak 残留
bundle 包正确挑了当前平台weather 目录只留 8.7MB 的 linux/amd64 那份)
```
备份:`/home/newqqagent-migration-backup-20260902-214812`
plugins 全目录 + homed.old + homeagent.service162MB
**唯一回滚路径**是恢复该目录 + 回滚 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.547ms515 ns/次)
订阅者 8 个1.518ms506 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.0tag 已打)。公开 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<Method>`
整数 ID——随 Part 6.2 删 `internal/plugin/cabi/` 就已无使用者,
留着会让人以为 C 层协商还在生效,或以为加 method 要同步维护那张整数表。