mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 01:18:08 +00:00
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:
@ -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<Method>` 整数表**均已删除**。
|
||||
>
|
||||
> 两个遗留点:`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 项可行性实验(跨进程并发改写零丢失等数字来源)
|
||||
- `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/` 迁移执行期工具
|
||||
@ -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<Method>`
|
||||
整数 ID)——随 Part 6.2 删 `internal/plugin/cabi/` 就已无使用者,
|
||||
留着会让人以为 C 层协商还在生效,或以为加 method 要同步维护那张整数表。
|
||||
|
||||
Reference in New Issue
Block a user