Commit Graph

15 Commits

Author SHA1 Message Date
ba49dfda44 feat(sdk): 注入行为的记忆/裁剪标志位(纯追加)+ plugindev 退出码修复 + 示例 hmap 随发版
## 1. 注入标志位(公开 API 纯追加,无签名变更)

给注入行为补上工具早已有的两类声明位,并让通道定义也带上:
- `InjectOptions{NoMemory, ContextPolicy, CleanerName}`
- `IOInjector` 新增六个 `*Opts` 变体(排队/中断/同步 × 纯文本/带媒体)
- `ChannelDef.ContextPolicy`,并**补上 JSON tag**(Cleaner 标 `json:"-"`)

零值 InjectOptions 与旧的三参数方法完全等价(记入记忆 + 不裁剪),
存量插件不需要改一行、也不需要重编;旧方法保留为转发到零值 opts 的语法糖。

三条设计要点:
- **默认不裁剪**:裁剪会归档丢弃低相关事件,必须显式声明(ContextPolicy=prune)。
- **中断也允许声明 prune**(已确认):中断同样携带内容进上下文。
- `CleanerName`:注入的 source 未必是注册过的输入通道名,而注入内容常带
  ANSI/JSON 包装;允许显式指定用哪个已注册 cleaner 清洗。

顺带修掉一个易静默丢字段的坑:`ChannelDef` 原来没有 JSON tag,只能手写字段
白名单跨进程传(`{"NoMemory": ...}`),新增字段会被丢掉。现在模板整体传 `def`。

## 2. 示例调用点统一写明意图
rss/memo/calendar/qq 的中断注入显式 `NoMemory: true`(行为等价,写清语义)。

## 3. plugindev 出错却 exit 0(真缺陷)
`buildBundle`/`buildTarget` 遇错只 Printf 后 return,`cmdBuild` 返回 void,
于是**构建失败也退 0**。实测中一个示例的 windows 目标编译失败,批量脚本却报
「17/17 全绿」,并因此少产出 16 个 .hmap。现在累计 `buildFailed` 并以非零退出。

## 4. 平台策略:插件目标去掉 windows
homed 已放弃 Windows 原生(见核心仓 cmd/homed/platform_windows.go:插件体系依赖
fd 继承 + 统一共享内存区的段内偏移解引用,Windows 句柄模型无法表达),
插件只运行在 homed 能跑的平台上,故 `allBundleTargets` 去掉 windows,
并对 windows 目标给出**可执行的报错**(指引 WSL2),而不是让它死在一句
`undefined: attachUnifiedShm` 上。

## 5. 发版附带各示例插件的 .hmap
新增 `package/build-examples.sh` 并接入 `package/build.sh`(组件 all|plugindev|examples):
- 用**刚构建出来的**那把工具链构建示例,保证与本次发版同源
- 逐平台 `--no-bundle --target <os/arch>`(bundle 会连 windows 一起编)
- 判成功同时看**退出码 + 产物存在**
- 全部产物齐了才 `sha256sum`(边打边算会漏掉后生成的包)
- 有任一失败即整体失败,不生成 SHA256SUMS

## 6. 版本
SDK 仍为 1.2.0(main 是下一个未发布中版本);1.2.0 条目补记本次新增接口,
并注明新标志位需核心 1.2.0+(旧核心会忽略这些字段,不报错但不生效)。
2026-09-11 20:29:30 +08:00
8c397ecf65 feat(plugindev): doc/knowledge 大正文走共享内存 + 协议版本 bump 到 2(§13.13)
- doc.insert / doc.insertWithMedia:doc_ref / attachments_ref
- knowledge.add:content_ref
- procProtocolVersion 1 → 2

bump 的理由:这两处改了内核→插件的 payload 承载方式,两种错配都静默失效
(v1 插件遇 v2 内核拿到空参数;v2 插件发 blocks_ref 给 v1 内核被静默忽略)。
双方都是等值校验,bump 后 v1 插件遇上 v2 内核会在建链时显式报错并带出
“请用配套 plugindev 重编”。

配套内核侧 50ba4a6。
2026-09-10 23:26:21 +08:00
632f6743d3 feat(plugindev): 媒体块经共享内存传递(§13.13)
SetToolBlocks / InjectInputMedia / InjectInputMediaSync /
InjectInterruptMedia 原先把 blocks 内联在 RPC 报文里。本地生成的图/音频是
base64 data URL(一张图可达数 MB),内联时整份要在报文里再编码再拷贝一遍;
更关键的是内容本体不在共享段里,插件回调无法就地改写。

改为经 putValueInArena 传 blocks_ref,内核 resolveBlocks 读回;小 payload
仍内联。

同步调用用 mediaArgsOwned 返回的释放函数延迟释放:injectMediaSync 要等
应答,槽不能在应答到达前回收,否则内核读到已释放内存。

配套内核侧 15d912e(实现 setToolBlocks 桩 + resolveBlocks)。
2026-09-10 22:46:41 +08:00
9d930db4ea feat(plugindev): output.invoke 模板从共享帧读参数(§13.6)
内核侧 OutputInvokeParams 新增 Frame/ArgsLen,payload 不再内联在 RPC 报文里。
模板必须跟着读帧,否则 frameInput 拿不到参数、输出通道收到的 payload 为空——
生产插件(如 qq)走的正是模板,模板不改这个改动就等于没做。

无帧时回退内联 args,保持对直连 RPC 调用方的兼容。
2026-09-10 21:28:56 +08:00
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
18fec9b003 feat(shm): Cleaner SharedRef + ContextPolicy (§13.4/§13.8)
- cleaner.invoke 协议: TextRef SharedRef 替代内联 text
- 插件侧 arenaWrite/SharedRef 读写辅助
- ToolDef.ContextPolicy 字段(默认 none / 可设 prune)
- 握手解析统一区域 SuperBlock + arena 元数据
2026-09-10 16:04:55 +08:00
fc236120e3 feat(shm): 统一共享内存区域(§13.1) — 子进程侧模板适配
- proc_shm_unix.go.tmpl: fd 3 = 统一区域,fd 4 = eventfd
- proc_main.go.tmpl: 握手解析 SuperBlock,从 ctxOff/evtOff 定位两段
- 新增 unified region 常量(magic/version/offset) + StageContext 内部布局常量
2026-09-10 11:46:12 +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
ef0e58ee23 plugindev: 模板支持事件环消费(Part 5 子进程侧)
handleHandshake 额外挂载 fd 4(事件环段)+ fd 5(eventfd),启动
evtConsumerLoop goroutine 消费事件。

HandshakeParams 新增 evt_ring_size 字段(0 = 不支持事件环)。

events.subscribe:按类型列表在本地注册 handler,evtConsumerLoop 从
共享段读 slot 后按位索引分发。events.unsubscribe 清空全部 handler。

事件环布局常量与内核 internal/plugin/proc/evtring.go 一一对应。

验证:weather.bin 零改动编译通过;E2E 测试全通过。
Ref: docs/zh/架构迁移评估.md §3.6
2026-09-02 17:05:01 +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
f3d87ec35f docs: 生命周期文档补全 onRemove(删除清理)说明
- README.md/README_EN.md:新增删除清理(onRemove)小节——语义(仅卸载
  触发、重载/禁用不触发,Stop 之后执行)、内核配套清理(工具注册/disabled/
  配置项定义 plugin.<name>.*/配置表 config_<name>)、示例清单与代码片段
- plugindev 模板 README.md.tmpl:新增 Lifecycle 段(RegisterStopHandler 每次
  停止、RegisterOnRemoveHandler 仅卸载)
2026-08-02 15:23:22 +08:00
fb07081929 插件删除回调(onRemove)与模板/示例同步
- sdk: RegisterOnRemoveHandler/RunOnRemoveHandlers(仅卸载时触发,重载不触发;
  后注册先执行、幂等),与 RegisterStopHandler/RunStopHandlers 并存
- plugindev: 模板 main.go.tmpl 新增 onRemove 演示(删配置键),templates.go 同步
- example/calendar: RegisterOnRemoveHandler(p.cleanupData) 卸载时清理 events.json
- README/README_EN: 生命周期文档补充 onRemove
- example/calendar/plg.json: 版本对齐
2026-08-02 13:33:25 +08:00
1762f0c34b docs: 修正全部文档使其与源码实现一致
- Plugin.Start(sdk *PluginSDK) 接口签名改为指针
- 方法表重写: 移除 CallLLM/QueryKnowledge/SetMemory 等不存在方法
- IOInjector 参数顺序修正为 (source, channel, text)
- 删除虚构 SDKConfig, 替换为实际 New() 构造函数签名
- .hmap 内容描述一致化 (plugin.so + plugin.dll + main.lua)
- 添加 meta/ 包元数据文件
2026-07-18 20:47:17 +08:00