|
|
5175e7d6e0
|
refactor(plugindev): 模板适配 funccall 调用帧
工具调用/清洗由内核发起,内核标定一块内存帧交给插件(callee),
插件在帧内工作;只有结果超出内核预留预算时才向内核申请扩容块。
- 新增 frameInput/frameOutput/arenaPut:读写调用帧、按需扩容
- tool.invoke:从 frame[0,args_len) 读参数;结果优先写帧结果区,
放不下才 arena.alloc 扩容并打 sharedRefFlagExpand
- cleaner.invoke:同一帧模型(frame + input_len)
- SharedRef.Flags 语义位与内核对齐(JSON / EXPAND)
- 防漂移测试更新为 frame/args_len/result_ref
|
2026-09-10 18:54:40 +08:00 |
|
|
|
71e3325439
|
refactor(plugindev): 模板改用内核独占共享槽池 + ContextPolicy 字段
共享内存是内核内部实现,不对插件开发者暴露。模板不再维护任何分配
游标(历史上 bump 游标 / 模板内位图 CAS 两版都因把可变分配状态放在
共享内存里而出竞态),改为通过内核 RPC 申请/归还:
- 删除 arenaWrite 本地 bump 分配器与 arenaOff/arenaUsed 全局变量
- 新增 arenaAlloc/arenaFree:走 arena.alloc / arena.free RPC
- 新增 putInArena/callWithText:按 payload 大小自动选择共享槽或内联
JSON(inlinePayloadLimit=512),SDK 公开 API 仍是普通字符串/Map,
插件开发者无感
- cleaner.invoke 改为读 TextRef/内联 Text、写 RespRef/内联 Text;
插件不做任何分配(内核预分配请求槽 + 响应槽)
- 同步 handshake:不再解析 arena 槽池布局(偏移与大小由 arena.alloc
的应答下发)
测试:
- 新增 TestProcTemplate_UsesKernelArenaRPC:模板必须调用 arena.alloc/
arena.free,且不得再出现 arenaUsed/arenaWrite(回归保护)
- TestProcTemplate_RejectsVersionMismatch 更新为 §13.1 后的「统一区域
魔数不匹配」文案
- 修正模板与 sdk/plugin.go 的 gofmt 对齐(含补上 ContextPolicy 字段)
|
2026-09-10 17:58:06 +08:00 |
|
|
|
ce5bff9275
|
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 能在其上运行」的下限,媒体是可选能力。
|
2026-09-06 10:03:36 +08:00 |
|
|
|
9f844123fe
|
plugindev: entry 语义收敛 + 删 C ABI 工具链 + Windows 共享内存适配(Part 6.1)
## entry 不再是通道开关 —— 外部插件零改动的关键
17 个存量插件的 plg.json 都写着 "entry": "plugin.so"。若把 entry 当通道
开关,迁移就得改 17 个文件,而「外部插件零改动」是本次迁移的硬约束。
改法:Go 插件一律产出 plugin.bin,不看 entry 值。isProcEntry 删除,
resolveBuild 去掉 proc 参数。entry 现在只剩区分 Lua(main.lua)一个用途。
实测:weather 的 plg.json 一行不改(仍写 plugin.so),plugindev build
直接产出三平台 plugin.bin。
## Windows 不再是能力退化的第三套实现(§9.2 的正解)
C ABI 时代 Windows 是独立的第三套 ABI:dynamic_dll_windows.go 的 stage
只下发 3 个字段(raw_message/user_id/phase)且完全没有写回,sanitizer
这类改写型插件在 Windows 上静默失效,且无任何运行时警告。
现在 Windows 与 Unix 共用同一份 RPC 逻辑与同一份共享段布局。平台差异
收敛到三个挂载函数:
- Unix(linux/darwin/freebsd):内核经 ExtraFiles 传继承 fd(3=StageContext
段,4=事件环段,5=eventfd/pipe)
- Windows:没有 fd 继承语义(os/exec 的 ExtraFiles 在 Windows 不支持),
改用命名内核对象——父进程 CreateFileMapping/CreateEvent 建带名字的对象,
子进程 OpenFileMappingW/OpenEventW 按同名打开。名字经环境变量传入而非
硬编码:多个 homed 实例并存时不能撞名。
Windows 绑定用 syscall.NewLazyDLL 而非 golang.org/x/sys/windows:
OpenFileMappingW/OpenEventW 未被标准库 syscall 导出,而引入 x/sys 会给
**每个插件的 go.mod** 加一个新依赖,违反「插件仅依赖公开 SDK」。
LazyDLL 属标准库,零新增依赖。
新增 evtWaiter 接口抽象等待语义:eventfd 是计数器(多事件合并成一次
唤醒),Windows Event 是二元信号。不影响正确性——消费者被唤醒后按
readSeq 追 writeSeq 批量 drain,一次唤醒能处理累积的全部事件。
模板拆成三个文件:
proc_main.go.tmpl 平台无关(RPC + 共享段布局 + stage + 事件环消费)
proc_shm_unix.go.tmpl 继承 fd 挂载
proc_shm_windows.go.tmpl 命名对象挂载
## 删除 C ABI 工具链
templates.go 1296 → 516 行:
- tmplBridge(Windows DLL bridge) -265 行
- tmplLinuxBridge(Linux c-shared) -457 行
- tmplPluginInitC(C 入口) -57 行
另删 generateBridge / detectWindowsCC(MinGW 探测)/ tmplCABIHeader /
InitData.CABIVersion+CABIHeader。
交叉编译不再需要目标平台 C 工具链——这是 -buildmode=c-shared 退场的
连带收益(§3.1)。
## 测试
15 项全过,新增 4 项守护迁移不变量:
- AllPlatformsProduceBin:6 个 GOOS/GOARCH 组合统一产出 plugin.bin
- LuaIsSeparatePath:Lua 仍走解释器路径
- UnsupportedOSErrors:不支持平台明确报错,不静默产出错误产物
- NoCABIResiduals:代码中不得再出现 c-shared / CGO_ENABLED=1 /
detectWindowsCC / tmplLinuxBridge / tmplPluginInitC(注释除外)
- IgnoresEntryForGoPlugins:isProcEntry 必须已删除
验证:go build/vet/test 全通过;三平台交叉编译产出 plugin.bin;
git diff sdk/ 为空(接口冻结)。
Ref: docs/zh/架构迁移评估.md §3.1/§9.2、docs/zh/plugin-migration-plan.md Part 6
|
2026-09-02 18:39:02 +08:00 |
|
|
|
09b64dcb53
|
plugindev: 支持子进程插件构建(entry=plugin.bin,零 cgo)
Part 3 工具链改造。插件业务代码零改动,只需把 plg.json 的 entry
从 plugin.so 换成 plugin.bin。
新增 templates/proc_main.go.tmpl(1113 行)——子进程运行时:
- 51 个 core method 的插件侧 RPC 实现(procIO/procMemory/procSettings/
procSocial/procLLM/procKnowledge/procDocMemory/procTextMemory/procPluginMgr)
- 共享段访问(fd 3 = 内核经 ExtraFiles 传入的 memfd)+ 16 字段
StageContext 编解码,布局常量与 internal/plugin/proc/shm.go 逐一对齐
- handleStageInvoke:拿锁 → 读段 → handler → **只写脏字段** → 放锁。
只读插件脏字段集为空 → 零写入 → 不可能覆盖他人改写
(对照 C ABI 副本模型实测 35.8~36.8% lost update)
- 主循环每请求独立 goroutine:handler 内会反向调用内核并等应答,
在读循环里同步处理会死锁
- plugin.start 后显式上报 AutoRestart:公开 SDK 的 SetAutoRestart 是纯
setter 无 hook,隔着进程边界内核读不到(内核侧 corehandler.go:145 已就绪)
模板选择真实 .go 源文件 + //go:embed 而非 raw string:900+ 行代码塞在
字符串里写错只能等生成插件时才炸,作为源文件可被 parser/gofmt/vet 检查。
cmd_build.go:resolveBuild(target, proc) 分派;proc 走 go build -trimpath
+ CGO_ENABLED=0,交叉编译不再需要目标平台 C 工具链。bundle 模式各平台
产物同名故 zip 内加平台后缀(plugin.bin.linux.amd64)。
proc_runtime.go 生成时清理残留 z_bridge_gen.go/z_entry.c——同目录两套
main 会编译冲突,这让 .so → .bin 切换无需人工清理。
proc_runtime_test.go 16 项静态检查,防内核/插件两侧漂移:
method 名清单、7 个内核调用、共享段常量与字段枚举顺序、stage 加锁顺序、
快照必须存序列化字符串(切片共享底层数组的坑在 11.3 已踩过)、
arena 不足须报错、日志走 stderr、版本不匹配须拒绝、零 cgo。
验证:真实 plugindev 构建 example/weather,plugin.go 逐字节未改,
产出静态链接 ELF;git diff sdk/ 为空(接口冻结)。
Ref: docs/zh/架构迁移评估.md §3、docs/zh/plugin-migration-plan.md Part 3
|
2026-09-02 12:13:45 +08:00 |
|