mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 01:18:08 +00:00
配套 SDK 提交:homeagent-sdk ba49dfd(公开 API 纯追加,无签名变更)。
本仓第三方的库镜像同步至该版本,以保证全新 clone 能编译。
## 1. 注入标志位(内核侧)
- 7 条注入路径(排队/中断/同步 × 纯文本/带媒体 + 旧 NoMem 变体)解析并转发
no_memory / context_policy / cleaner_name;策略在入口**校验**,
非法值报错而不是静默降级成 none(降级会让调用方以为自己声明的裁剪在生效)。
- 新增 validateContextPolicy(与 tool.register 同一套规则)与 pubSdkInjectOpts。
- input.register 不再手写字段白名单重建 ChannelDef,改为整体传递 + 补 ContextPolicy。
- io 层:applyInjectOpts 把标志位写进事件 payload,仅非零时写
(零值与旧 payload 逐字节一致,事件订阅方与旧内核都不受影响)。
- ioAdapter / procCore / internal-sdk 别名补齐六个 *Opts 实现。
## 2. 修掉「输入无条件裁剪」这个真缺陷
eventloop 此前对**每条非中断输入**都调 `context.Prune(...)`:破坏性(低相关事件被
归档移出上下文)且无法从调用点看出是谁触发的。改为 pruneOnInput/pruneDeclared:
优先级:注入点声明(payload.context_policy)> 通道声明(ChannelDef.ContextPolicy)
> 默认**不裁剪**
查询向量仍取清洗后的内容;新增 cleanInputFor 解析清洗文本,优先级为
注入点声明的 cleaner(cleaner_name)> 按 source 查到的通道 cleaner > 原文,
名字查不到时**记日志再回退**(注入是 fire-and-forget,插件看不到错误,
至少要在内核日志留下「你声明的清洗没生效」的痕迹)。
## 3. jieba 词库内嵌(修「猜 GOMODCACHE → 静默失效」)
原 jiebaDictDir() 去猜 GOMODCACHE/GOPATH/~/go/pkg/mod,部署机上通常没有 Go 模块
缓存 → GetJieba() 返回 nil → 分词/关键词提取/NLP 依存解析(进而 doc→graph 三元组
抽取)/静态词向量 tokenizer **一律静默返回空列表**,只有一行日志。本机看起来正常
只因开发机与生产机重合、恰好有那份缓存。
现在词库随二进制分发:internal/memory/jiebadict/ 5 文件约 11.6MB + go:embed,
按**内容哈希**命名缓存目录落盘(词库升级不复用旧文件),已齐全则跳过写入。
模块缓存降为兜底。homed 体积 32MB。
顺带确认(并有测试佐证):gojieba 的 Tag() 不需要 pos_dict/ 目录——
cppjieba 的 PosTagger 从主词典每行的词性列取 tag。
## 4. homed 放弃 Windows 原生,改走 WSL2
插件体系依赖「继承的 fd」+「统一共享内存区的段内偏移解引用」,Windows 既无 fd
继承语义,其句柄模型也无法表达后者;强行适配等于再维护一套平台专属 ABI
(C ABI 时代三套 ABI 并存曾导致改写型插件在某平台静默失效)。
- cmd/homed/platform_{windows,other}.go:原生 Windows 启动即拒绝并打印 WSL2 指引。
- internal/plugin/proc/shmalloc_windows.go:allocShm 直接返回「请用 WSL2」,
**不返回半可用的段**(与 shmalloc_other.go 同风格:未支持平台显式报错);
procEnvForShm 返回 nil。顺手修掉两处长期编译错误
(cryptorand→rand、h.evData→h.unified.evtData),使 GOOS=windows 至少能编译。
注:homed 本就编不出 Windows——internal/memory 依赖 cgo-only 的 gojieba。
- deploy/packaging/installer.nsi:不再安装 homed.exe/initconfig.exe,改为携带
**linux payload** 并调用新的 install-via-wsl.ps1;退出码 20/21 表示
「需先装 WSL/发行版」,走指引而非报错。
- deploy/packaging/windows/install-via-wsl.ps1(新):检测 WSL → 引导安装 →
确保 WSL2 → 送包进发行版 → 在 WSL 内按 Linux 方式安装。**复用 Linux 包与
linux/setup.sh**,不另写一套安装逻辑;落点与 deb 布局统一
(/usr/bin/homed + /usr/lib/homeagent/setup.sh)。
- deploy/packaging/linux/setup.sh:API Key 允许 HOMEAGENT_API_KEY 覆盖
(否则安装器界面显示一份、config.db 里另一份 → 登录不上)。
- deploy/packaging/build.sh:windows 目标只构建 waiter + gui,并新增
stage_linux_payload 把 Linux 包暂存给安装器;homed/initconfig 在 windows
目标下明确拒绝。
## 5. 插件调用点统一写明意图
- webui 的 OpenAI 兼容端点(固定提示词模板)→ InjectTextSyncNoMemory。
- agentcli 的 5 处纯状态通知(已启动/超时/执行结束/进程退出/读取结束)→ NoMemory;
**带输出**的 2 处(定时反馈、有新输出)刻意保留记忆并注明理由。
- timer 的定时提醒 → NoMemory(中断本来也隐含 NoMemory,这里是写明意图)。
## 6. 版本
meta.Version 仍为 1.2.0(main 是下一个未发布中版本);
SDKCompatibleVersion 1.1.0 → **1.2.0**(本内核已实现 SDK 1.2.0 全部新增方法)。
## 测试
- core:默认不裁剪(无声明/none/空)、通道 opt-in、注入点双向覆盖通道、
nil context/io 安全、cleaner 优先级与未知名回退。
- io:零值 opts 与历史 payload 逐键相同;text/中断/媒体三类注入标志位都落到
payload;旧方法仍生效。
- proc:validateContextPolicy 只接受 ""/none/prune,报错含位置与实际值;
**跨进程** e2e——testdata 插件经 io.injectText 送出三个标志位,断言它们穿过 RPC
到达内核。
- memory:模块缓存不可见时内嵌词库仍可用(分词与 POS 内容词均非空)、
落盘幂等、内容哈希稳定。
验证:go build ./... / go vet ./... / go vet -tags onnxruntime ./...
go test -short ./internal/memory/... ./internal/nlp/... ./internal/plugin/...
./internal/agent/{core,io}/... ./pkg/...
224 lines
7.7 KiB
Go
224 lines
7.7 KiB
Go
package plugin
|
||
|
||
import (
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin/proc"
|
||
isdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||
)
|
||
|
||
// procCore 把内核的 *internal/sdk.PluginSDK 收窄成子进程插件可见的能力面。
|
||
//
|
||
// ❗ **必须用命名字段,不能嵌入** `*isdk.PluginSDK`:嵌入会让全部方法被提升,
|
||
// 外部插件通道就能经类型断言拿到 Supervisor()/Tracker()/Adapter()/Indexer()
|
||
// 这些内核内部机制——权限梯度退化成纸面约定。命名字段下只有下面显式写出的
|
||
// 方法存在,这才是 §3.8 说的「从 C ABI 表达能力的意外产物变成显式声明并强制的策略」。
|
||
//
|
||
// 另一个必要性:internal/sdk 的接口是公开 SDK 的**超集**(isdk.KnowledgeAPI
|
||
// 内嵌 pubsdk.KnowledgeAPI 再加 Stats()/Remove(),isdk.MemoryAPI 加 GraphData(),
|
||
// isdk.LLMAPI 加 Chat()/ReloadFromConfig()),Go 方法签名精确匹配下
|
||
// *isdk.PluginSDK 本就不满足 proc.CoreSDK。
|
||
//
|
||
// 内置插件走的仍是原路径(直接持 *isdk.PluginSDK,拿到全量接口),不受影响。
|
||
type procCore struct {
|
||
sdk *isdk.PluginSDK
|
||
}
|
||
|
||
// newProcCore 包装内核 SDK 供子进程插件使用。
|
||
func newProcCore(s *isdk.PluginSDK) procCore { return procCore{sdk: s} }
|
||
|
||
func (c procCore) PluginName() string { return c.sdk.PluginName() }
|
||
|
||
// ---- 能力访问器:内部超集接口 → 公开 SDK 接口 ----
|
||
//
|
||
// nil 保护是必要的:corehandler 用 `if xxx == nil` 判断能力不可用并返回
|
||
// errUnavailable,若把「类型化的 nil」透过去,判空会失效——插件收到的是
|
||
// panic 而不是"能力不可用"。
|
||
|
||
func (c procCore) Settings() pubsdk.SettingsAPI {
|
||
if s := c.sdk.Settings(); s != nil {
|
||
return s
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c procCore) Memory() pubsdk.MemoryAPI {
|
||
if m := c.sdk.Memory(); m != nil {
|
||
return m
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c procCore) TextMemory() pubsdk.TextMemoryAPI {
|
||
if m := c.sdk.TextMemory(); m != nil {
|
||
return m
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c procCore) DocMemory() pubsdk.DocMemoryAPI {
|
||
if m := c.sdk.DocMemory(); m != nil {
|
||
return m
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c procCore) Knowledge() pubsdk.KnowledgeAPI {
|
||
if k := c.sdk.Knowledge(); k != nil {
|
||
return k
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c procCore) LLM() pubsdk.LLMAPI {
|
||
if l := c.sdk.LLM(); l != nil {
|
||
return l
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c procCore) Social() pubsdk.SocialAPI {
|
||
if s := c.sdk.Social(); s != nil {
|
||
return s
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c procCore) PluginMgr() pubsdk.PluginMgrAPI {
|
||
if m := c.sdk.PluginMgr(); m != nil {
|
||
return m
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ---- 注册面 ----
|
||
|
||
func (c procCore) RegisterTool(name string, def pubsdk.ToolDef, handler pubsdk.ToolHandler) error {
|
||
return c.sdk.RegisterTool(name, def, handler)
|
||
}
|
||
|
||
func (c procCore) RegisterStage(stage pubsdk.Stage, handler pubsdk.StageHandler, scope ...pubsdk.StageScope) {
|
||
c.sdk.RegisterStage(stage, handler, scope...)
|
||
}
|
||
|
||
func (c procCore) RegisterPluginAPI(name string) error {
|
||
return c.sdk.RegisterPluginAPI(name)
|
||
}
|
||
|
||
func (c procCore) RegisterOutputChannel(name string, caps int, desc string, def pubsdk.ChannelDef, handler pubsdk.ToolHandler) error {
|
||
return c.sdk.RegisterOutputChannel(name, caps, desc, def, handler)
|
||
}
|
||
|
||
func (c procCore) RegisterInputChannel(name string, def pubsdk.ChannelDef) error {
|
||
return c.sdk.RegisterInputChannel(name, def)
|
||
}
|
||
|
||
// ---- IO 注入 ----
|
||
|
||
func (c procCore) InjectText(source, channel, text string) {
|
||
c.sdk.InjectText(source, channel, text)
|
||
}
|
||
|
||
func (c procCore) InjectInterruptText(source, channel, text string) {
|
||
c.sdk.InjectInterruptText(source, channel, text)
|
||
}
|
||
|
||
func (c procCore) InjectTextNoMemory(source, channel, text string) {
|
||
c.sdk.InjectTextNoMemory(source, channel, text)
|
||
}
|
||
|
||
// ---- 带注入标志位(记忆/裁剪行为由插件在调用点声明)----
|
||
|
||
func (c procCore) InjectTextOpts(source, channel, text string, opts pubsdk.InjectOptions) {
|
||
c.sdk.InjectTextOpts(source, channel, text, opts)
|
||
}
|
||
|
||
func (c procCore) InjectInterruptTextOpts(source, channel, text string, opts pubsdk.InjectOptions) {
|
||
c.sdk.InjectInterruptTextOpts(source, channel, text, opts)
|
||
}
|
||
|
||
func (c procCore) InjectInputSyncOpts(source, channel, text string, opts pubsdk.InjectOptions) string {
|
||
// 这里用公共 SDK 的三参数 + opts 形态(返回回复文本),
|
||
// 不用内核内部那个 (eventType, payload) → *OutputEvent 的全量签名:
|
||
// 它会把内核 IO 事件结构暴露给外部插件。
|
||
return c.sdk.InjectInputSyncOpts(source, channel, text, opts)
|
||
}
|
||
|
||
// InjectInputSync 收窄为公开 SDK 的三参数文本形态。
|
||
//
|
||
// internal/sdk.PluginSDK 的同名方法是 (source, channel, eventType, payload)
|
||
// → *agentIO.OutputEvent,暴露了内核 IO 事件结构;外部插件只该看到回复文本。
|
||
// 取值方式与 C ABI 路径一致(internal/plugin/cabi/loader.go 的 case 47)。
|
||
func (c procCore) InjectInputSync(source, channel, text string) string {
|
||
out := c.sdk.InjectInputSync(source, channel, "text", map[string]interface{}{
|
||
"content": text,
|
||
})
|
||
if out == nil {
|
||
return ""
|
||
}
|
||
reply, _ := out.Payload["content"].(string)
|
||
return reply
|
||
}
|
||
|
||
// ---- 带媒体的 IO 注入 ----
|
||
//
|
||
// 三个方法都直接转调 internal/sdk 的同名方法:那一层已经是三参数 + blocks
|
||
// 的公开形态,不像 InjectInputSync 需要收窄。
|
||
|
||
func (c procCore) InjectInputMedia(source, channel, text string, blocks []pubsdk.ContentBlock) {
|
||
c.sdk.InjectInputMedia(source, channel, text, blocks)
|
||
}
|
||
|
||
func (c procCore) InjectInputMediaSync(source, channel, text string, blocks []pubsdk.ContentBlock) string {
|
||
return c.sdk.InjectInputMediaSync(source, channel, text, blocks)
|
||
}
|
||
|
||
func (c procCore) InjectInterruptMedia(source, channel, text string, blocks []pubsdk.ContentBlock) {
|
||
c.sdk.InjectInterruptMedia(source, channel, text, blocks)
|
||
}
|
||
|
||
func (c procCore) InjectInputMediaOpts(source, channel, text string, blocks []pubsdk.ContentBlock, opts pubsdk.InjectOptions) {
|
||
c.sdk.InjectInputMediaOpts(source, channel, text, blocks, opts)
|
||
}
|
||
|
||
func (c procCore) InjectInputMediaSyncOpts(source, channel, text string, blocks []pubsdk.ContentBlock, opts pubsdk.InjectOptions) string {
|
||
return c.sdk.InjectInputMediaSyncOpts(source, channel, text, blocks, opts)
|
||
}
|
||
|
||
func (c procCore) InjectInterruptMediaOpts(source, channel, text string, blocks []pubsdk.ContentBlock, opts pubsdk.InjectOptions) {
|
||
c.sdk.InjectInterruptMediaOpts(source, channel, text, blocks, opts)
|
||
}
|
||
|
||
// SetToolBlocks 转调 internal/sdk:插件工具注入的媒体块,内核在下一轮
|
||
// tool message 携带。
|
||
func (c procCore) SetToolBlocks(blocks []pubsdk.ContentBlock) {
|
||
c.sdk.SetToolBlocks(blocks)
|
||
}
|
||
|
||
// ---- 生命周期 ----
|
||
|
||
func (c procCore) SetAutoRestart(enabled bool) { c.sdk.SetAutoRestart(enabled) }
|
||
|
||
// 编译期确认收窄面正好满足子进程插件的能力契约。
|
||
var _ proc.CoreSDK = procCore{}
|
||
|
||
// procPluginAdapter 把 *proc.Plugin 适配到 registry 的 sdk.Plugin 接口。
|
||
//
|
||
// 两者只差 Start 的参数类型:registry 传 *isdk.PluginSDK(全量能力),
|
||
// 而子进程插件只该拿到收窄后的 proc.CoreSDK。转接在此发生,
|
||
// 权限收窄就成了**类型系统强制**的事,而不是约定(§3.8)。
|
||
//
|
||
// Name/Stop/Close 经嵌入指针提升;Close 对 registry.closeDynamic 可见,
|
||
// 故重载时能真正 kill 子进程——对比 cabi 路径的 Close 只做 dlclose,
|
||
// 而 dlclose 对 Go c-shared 是 no-op(§1.1,热重载静默失效的根因)。
|
||
type procPluginAdapter struct {
|
||
*proc.Plugin
|
||
}
|
||
|
||
// Start 把内核全量 SDK 收窄成子进程可见的能力面后启动进程。
|
||
func (a procPluginAdapter) Start(s *isdk.PluginSDK) error {
|
||
return a.Plugin.Start(newProcCore(s))
|
||
}
|
||
|
||
// 编译期确认适配器满足 registry 的插件接口。
|
||
var _ isdk.Plugin = procPluginAdapter{}
|