- SDK: ToolDef.NoMemory field, PluginSDK.RegisterTextCleaner/TextCleaners - Registry: aggregate text cleaners from plugins, expose CleanText() - Memory: replace hardcoded QQ regex CleanTemplateText with dynamic CleanText/SetTextCleaner - StageHost: add ToolDef(name) lookup - eventloop: check ToolDef.NoMemory before emitMemoryCandidate - context/Prune: replace hardcoded agentcli/terminal source filter with ToolsUsed NoMemory check - agentcli/cmd: mark tools with NoMemory: true - main.go: wire memory.SetTextCleaner(pluginReg.CleanText)
7.6 KiB
插件化改造 — 实施计划
问题
当前 core 层两处硬编码耦合:
-
文本清洗:
internal/memory/clean_text.go硬编码 6 个 QQ 正则,context/indexer/cut/VertorizeClean 各环节直接调用CleanTemplateText -
工具输出不进记忆:
internal/agent/core/context.go:196硬编码 source 名过滤if s.event.Source == "agentcli" || s.event.Source == "terminal" { continue }意图是
cmd_run、terminal_*等工具的产出不归档到 document memory,但手段是查 source 而不是查工具定义,且只覆盖了 Prune 路径(document),没有覆盖emitMemoryCandidate路径(text + graph memory)。同时
eventloop.go:337的!stageCtx.NoMemory检查是命令式的(InjectTextNoMemory设 payload flag),不是声明式的工具级控制。
目标
| 问题 | 方案 |
|---|---|
| 文本清洗 | 插件通过 s.RegisterTextCleaner() 注册清洗函数,core 遍历执行 |
| 工具记忆控制 | ToolDef 增加 NoMemory bool 字段,插件声明式标记工具;core 在 emitMemoryCandidate 和 Prune 两处自动跳过 |
SDK 修改(外部包 homeagent-sdk)
位置: sdk/plugin.go
1.1 ToolDef 增加 NoMemory
type ToolDef struct {
Name string `json:"name"`
Plugin string `json:"plugin,omitempty"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
NoMemory bool `json:"no_memory,omitempty"` // ← 新增
}
1.2 PluginSDK 增加 TextCleaner
// PluginSDK 新增字段
textCleaners []func(text string) string
func (s *PluginSDK) RegisterTextCleaner(cleaner func(text string) string) {
s.textCleaners = append(s.textCleaners, cleaner)
}
// 框架用 — 提取所有 cleaner
func (s *PluginSDK) TextCleaners() []func(text string) string {
return s.textCleaners
}
SDK 管理策略
复制 SDK 到 _sdk_local/,go.mod 加 replace gitcode.com/JianFeeeee/homeagent-sdk => ./_sdk_local
内部变更
2.1 Registry:聚合 text cleaners
文件: internal/plugin/registry.go
- 新增字段
textCleaners []func(string) string buildSDK()中plg.Start(plgSDK)成功后提取plgSDK.TextCleaners()到textCleaners- 新增
CleanText(text string) string依次执行所有 cleaner,无 cleaner 时返回原文本 StopAll()/Reload()时重置
2.2 Memory:动态 cleaner 入口
文件: internal/memory/clean_text.go
替换硬编码 QQ 正则为动态 cleaner:
var globalTextCleaner func(string) string
func SetTextCleaner(fn func(string) string) { globalTextCleaner = fn }
func CleanText(text string) string {
if globalTextCleaner != nil {
text = globalTextCleaner(text)
}
// 保留通用 trim
text = strings.TrimSpace(text)
text = strings.TrimPrefix(text, ",")
text = strings.TrimPrefix(text, ",")
text = strings.TrimSpace(text)
return text
}
删除 reQQ* 正则变量。
2.3 StageHost:按名查 ToolDef
文件: internal/agent/core/stages.go
新增方法:
func (h *StageHost) ToolDef(name string) *sdk.ToolDef {
h.mu.RLock()
defer h.mu.RUnlock()
for _, def := range h.toolDefs {
if def.Name == name {
return &def
}
}
return nil
}
2.4 Agent:两处 NoMemory 检查
2.4a emitMemoryCandidate 路径
文件: internal/agent/core/eventloop.go
processTextInput() 和 processMediaInput() 中的 emitMemoryCandidate 调用前,除了检查 stageCtx.NoMemory,还检查本次调用的工具是否有 NoMemory:
hasNoMemoryTool := false
for _, name := range toolsUsed {
if def := a.stageHost.ToolDef(name); def != nil && def.NoMemory {
hasNoMemoryTool = true
break
}
}
if !stageCtx.NoMemory && !hasNoMemoryTool {
a.emitMemoryCandidate(...)
}
这样不论通过 InjectText 还是 InjectTextNoMemory,只要 LLM 调用了 NoMemory: true 的工具,整轮对话就不进 text memory 和 graph memory。
2.4b Prune 归档路径
文件: internal/agent/core/context.go
删除 hardcoded source 过滤,改为检查事件中 ToolsUsed 是否有 NoMemory 工具:
for _, s := range archive {
if hasNoMemoryTool(s.event.ToolsUsed) {
continue
}
filtered = append(filtered, s)
}
hasNoMemoryTool 通过 StageHost.ToolDef 查每个工具名是否有 NoMemory: true。
2.5 caller CleanTemplateText → CleanText
| 文件 | 替换 |
|---|---|
internal/agent/core/context.go |
memory.CleanTemplateText → memory.CleanText |
internal/memory/indexer.go |
同上 |
internal/memory/cut.go |
同上 |
internal/memory/clean_text.go |
删除函数 CleanTemplateText,新增 CleanText + SetTextCleaner |
2.6 main.go 串联
文件: cmd/homed/main.go
pluginReg.Load() 之后:
memory.SetTextCleaner(pluginReg.CleanText)
2.7 agentcli / cmd 插件:标记 NoMemory
文件: internal/plugins/agentcli/plugin.go 和 internal/plugins/cmd/plugin.go
在 RegisterTool 调用的 ToolDef 中加 NoMemory: true:
terminal_create,terminal_write,terminal_read,terminal_resize,terminal_close,terminal_listcmd_run
外部插件变更
QQ 插件
在 Start() 中:
s.RegisterTextCleaner(func(text string) string {
text = reQQGroupSuffix.ReplaceAllString(text, "")
text = reQQPrivateSuffix.ReplaceAllString(text, "")
text = reQQOldReply.ReplaceAllString(text, "")
text = reQQOldForbid.ReplaceAllString(text, "")
text = reQQGeneral.ReplaceAllString(text, "")
text = reTimestamp.ReplaceAllString(text, "")
text = reMultiSpace.ReplaceAllString(text, " ")
return text
})
s.RegisterTool("qq_get_message", sdk.ToolDef{
Description: "获取QQ消息正文",
Parameters: map[string]interface{}{...},
NoMemory: true,
}, handler)
文件变更清单
| 文件 | 操作 |
|---|---|
sdk/plugin.go(外部 SDK) |
修改:ToolDef 增 NoMemory、PluginSDK 增 RegisterTextCleaner/TextCleaners |
go.mod |
修改:增 replace 指令 |
internal/plugin/registry.go |
修改:增 cleaners 聚合 + CleanText |
internal/memory/clean_text.go |
修改:硬编码 → 动态 cleaner,删除 CleanTemplateText |
internal/agent/core/stages.go |
修改:增 ToolDef(name) 查找 |
internal/agent/core/eventloop.go |
修改:emitMemoryCandidate 前查 ToolDef.NoMemory |
internal/agent/core/context.go |
修改:source 过滤 → ToolsUsed NoMemory 检查 + CleanTemplateText → CleanText |
internal/agent/core/process.go |
修改(可能需要):传递 toolsUsed 到 Prune 或加辅助方法 |
internal/memory/indexer.go |
修改:CleanTemplateText → CleanText |
internal/memory/cut.go |
修改:同上 |
internal/plugins/agentcli/plugin.go |
修改:工具标记 NoMemory: true |
internal/plugins/cmd/plugin.go |
修改:cmd_run 标记 NoMemory: true |
cmd/homed/main.go |
修改:memory.SetTextCleaner(pluginReg.CleanText) |
实施顺序
- SDK 本地副本 + 加
NoMemory/RegisterTextCleaner - Registry 聚合 cleaners +
CleanText - Memory 动态 cleaner + 改名调用者
- StageHost
ToolDef(name) - eventloop + context Prune + process 加入 NoMemory 检查
- 标记 agentcli/cmd 工具的
NoMemory: true - main.go 串联
- 删除旧代码(QQ 正则、source 硬编码)
- 构建 + 测试