mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-21 17:38:03 +00:00
feat(sdk): 多模态贯通插件边界——媒体字段、媒体注入接口与并发修复
记忆系统在核心 1.1.0 支持了二进制多媒体节点,但那条链路只对**内核自己**开放: 插件把 Triple / Doc 交进来,媒体一律无处安放,且**不报错**。本版补上公开接口 侧缺失的表达能力。 ## 一、类型与接口(全部新增,无签名变更) - `Triple` += `SentenceText`、`MediaDigests` - `Doc` += `MediaDigests`、`Attachments`;新增 `MediaAttachment` - `TextEvent` += `Attachments` - `DocMemoryAPI` += `InsertWithMedia` - `IOInjector` += `InjectInputMedia` / `InjectInputMediaSync` / `InjectInterruptMedia` - `PluginSDK` 补上一直缺失的 `SetToolBlocks` 包装(接口里有、便捷方法里没有, 插件只能自己去拿 injector) `MediaAttachment` 一个类型服务两个方向:给 `Data`+`MIME` 是新内容(内核按字节 去重),只给 `Digest` 是引用已有内容。读路径**只回元数据不回字节**——一次检索 可能命中几十份媒体,把字节全塞回来会撑爆跨进程消息。 媒体注入为什么不能搭 `SetToolBlocks` 的车:那个方法只在工具处理函数内部可用, 且媒体要等**下一条** tool message 才到模型手上。插件主动发起一轮带媒体的对话、 以及中断注入,需要各自的签名,且媒体在**本轮**就随消息发出。 `Triple.MediaDigests` 非空而 `SentenceText` 为空时,内核会用媒体标记本身充当句子 ——媒体引用挂在句子上,没有句子就无处挂起。插件只需填 digest,标记由内核拼: 要求调用方知道格式,等于让一个拼写错误静默切断引用绑定而全链路无人报错。 ## 二、修掉两处并发竞态 `sdk/stress_test.go` 的 `-race` 实测报 11 处 DATA RACE,收敛到两个字段: 1. **`PluginSDK` 的 API 字段无锁**。写方是内核(加载/重载插件时依次注入 injector、memory、doc、llm…),读方是插件在 `Start()` 里起的后台 goroutine ——轮询、监听、定时器都要拿 injector 往管道注消息。生产表现是插件重载瞬间 偶发崩溃:读到半个接口值就 nil 解引用。 2. **`autoRestart` 标志无锁**。`SetAutoRestart` 的文档用法本身就是「外部连接建好 后再决定能否自动重启」,而连接建立通常在后台 goroutine;内核 registry 在另一个 goroutine 读 `AutoRestart()` 决定崩溃后重启策略。这对读写天然跨 goroutine。 加 `apiMu sync.RWMutex`。关键约定写进注释:**只在持锁期间取字段值,取完立刻 释放再调用**。持锁调用会把 `InjectInputSync`(阻塞到 agent 回复,可达数分钟) 与 `SetIOInjector` 串到一起,让插件重载卡死。 ## 三、压测(sdk/stress_test.go,13 例) SDK 是被多个 goroutine 同时使用的共享对象,单线程单测全绿不代表并发路径成立。 断言的是不变量而非吞吐: - 媒体注入高并发不丢不串——每次调用带唯一 tag,逐条校验文本与图片 URL 配对。 「不串」是重点:若实现里出现任何共享中间状态(把 blocks 暂存到字段再读出), 高并发下会出现 A 的文本配 B 的图,而两者单独看都「成功」了; - injector 热替换(含替换成 nil,即内核卸载 API 的真实状态); - stop / onRemove handler 恰好一次——契约是「执行后清空,幂等」,执行两次的后果 从重复写文件到 close 已关闭 channel 直接 panic; - `StageContext` 并发读改写无 lost update(媒体链路让 Extra 成为新热点, 而 map 并发写在 Go 里是直接 fatal,recover 接不住); - `OwnTools` scope 不跨插件泄漏; - 媒体类型 JSON 往返字节级一致(9 种长度,含 0/1/2/3 与 base64 分组边界) ——`[]byte` 在 JSON 里是 base64,往返不一致意味着图片静默损坏, 要到 CAS 校验 digest 时才发现,那时已无从追查; - `omitempty` 真的生效(读路径不能出现 `"data"` 键); - nil 依赖全部静默降级不 panic。 ## 四、工具链同步 - `proc_main.go.tmpl`:`procIO` 三个媒体方法、`procDocMemory.InsertWithMedia`。 模板不跟上的后果是**每个外部插件都编不过**(接口未实现),是硬失败; - `proc_runtime_test.go`:方法清单补 `io.injectMedia*` 与 `doc.insertWithMedia`。 漏接线时插件调 `InjectInputMedia` 会静默无效果——模板不发这个 RPC,内核也就 收不到,两边都不报错; - `yaegi/mocksdk`:与公开 SDK 对齐。它此前漂移严重且**没有任何代码对着它编译**, 所以漂移不会被编译器抓到:`Triple` 用的是 `Predicate`,而公开 SDK 一直叫 `Relation` —— 插件在 yaegi 调试期写 `Relation:` 报未知字段,写 `Predicate:` 则 编成 plugin.bin 时报错,两边都不对。 - README 中英双语补媒体接口文档与用法示例。 ## 兼容性 存量插件不需要改一行也不需要重编:新增方法由**插件调用、内核实现**,不调就不 受影响。17 个 example 插件源码零改动通过类型检查;用 SDK 0.9.2 编的旧 plugin.bin 在新内核上直接建链通过(握手校验的是 ProtocolVersion=1,不是 SDK 版本)。 媒体接口需要核心 1.1.1+(更早的核心没有对应 RPC,调用返回 unknown method)。 `CoreVersion` 保持 1.0.0:它是「SDK 能在其上运行」的下限,媒体是可选能力。
This commit is contained in:
30
meta/meta.go
30
meta/meta.go
@ -6,9 +6,30 @@ var (
|
||||
// Version 是 HomeAgent SDK 版本号。
|
||||
// 通过 `-ldflags="-X gitcode.com/JianFeeeee/homeagent-sdk/meta.Version=vX.Y.Z"` 注入。
|
||||
//
|
||||
// 版本号语义:**SDK 版本跟随核心的中版本,patch 位恒为 .0**。
|
||||
// 整条核心 1.1.x 线(1.1.0、1.1.1、1.1.7…)共用 SDK 1.1.0;
|
||||
// 只有核心进入 1.2.0 这种中版本跃迁时 SDK 才升到 1.2.0。
|
||||
// 这样插件开发者只需关心「我在为哪个中版本写插件」,
|
||||
// 不必跟着核心的每个 bugfix 换 SDK 依赖(见 核心仓 docs/git-branching.md §七)。
|
||||
//
|
||||
// 1.0.0:插件运行模型从 C ABI 动态库改为子进程 + 共享内存。
|
||||
// 公开 SDK 接口(sdk/ 目录)**零改动**——插件业务代码不需要改一行,
|
||||
// 但产物形态变了(plugin.so → plugin.bin),必须用新版 plugindev 重编。
|
||||
// 公开 SDK 接口零改动,但产物形态变了(plugin.so → plugin.bin)。
|
||||
// 1.1.0:多模态贯通插件边界。**全部是新增,无签名变更**:
|
||||
// - Triple.SentenceText / Triple.MediaDigests
|
||||
// - Doc.MediaDigests / Doc.Attachments、MediaAttachment
|
||||
// - TextEvent.Attachments
|
||||
// - DocMemoryAPI.InsertWithMedia
|
||||
// - IOInjector 的 InjectInputMedia / InjectInputMediaSync /
|
||||
// InjectInterruptMedia;PluginSDK 补上缺失的 SetToolBlocks 包装
|
||||
// 同版修掉两处并发竞态(sdk/stress_test.go 的 -race 实证,不是理论风险):
|
||||
// PluginSDK 的 API 字段与 autoRestart 标志此前无锁,而写方
|
||||
// (内核注入 API、插件 SetAutoRestart)与读方(插件后台 goroutine
|
||||
// 注入、内核 registry 读 AutoRestart)天然跨 goroutine。
|
||||
// 存量插件不需要改一行也不需要重编:新增方法由**插件调用、内核实现**,
|
||||
// 不调就不受影响。想用新字段的插件重编即可。
|
||||
//
|
||||
// ❗main 分支上此值是**下一个未发布中版本**;已发布的值看对应的
|
||||
// release/vX.Y.x 分支与 tag(见 核心仓 docs/git-branching.md §2.1 与 §七.1)。
|
||||
Version = "1.0.0"
|
||||
|
||||
// Commit 是构建时的 Git commit hash。
|
||||
@ -27,6 +48,11 @@ var (
|
||||
//
|
||||
// 1.0.0 是硬下限而非建议值:0.9.x 内核只会 dlopen `.so`,
|
||||
// 本版工具链产出的 `plugin.bin` 在旧内核上根本不会被识别。
|
||||
//
|
||||
// ⚠️ 1.1.0 新增的媒体接口需要核心 **1.1.1+**(更早的核心没有
|
||||
// doc.insertWithMedia / io.injectMedia* 这些 RPC,调用会返回 unknown method)。
|
||||
// 这里仍写 1.0.0,因为它是「SDK 能在其上运行」的下限;
|
||||
// 媒体接口是可选能力,不用就不受影响。
|
||||
CoreVersion = "1.0.0"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user