mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-25 19:38:08 +00:00
- 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)
247 lines
7.6 KiB
Markdown
247 lines
7.6 KiB
Markdown
# 插件化改造 — 实施计划
|
||
|
||
## 问题
|
||
|
||
当前 core 层两处硬编码耦合:
|
||
|
||
1. **文本清洗**:`internal/memory/clean_text.go` 硬编码 6 个 QQ 正则,context/indexer/cut/VertorizeClean 各环节直接调用 `CleanTemplateText`
|
||
|
||
2. **工具输出不进记忆**:`internal/agent/core/context.go:196` 硬编码 source 名过滤
|
||
```go
|
||
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
|
||
|
||
```go
|
||
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
|
||
|
||
```go
|
||
// 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:
|
||
|
||
```go
|
||
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`
|
||
|
||
新增方法:
|
||
|
||
```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`:
|
||
|
||
```go
|
||
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` 工具:
|
||
|
||
```go
|
||
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()` 之后:
|
||
|
||
```go
|
||
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_list`
|
||
- `cmd_run`
|
||
|
||
---
|
||
|
||
## 外部插件变更
|
||
|
||
### QQ 插件
|
||
|
||
在 `Start()` 中:
|
||
|
||
```go
|
||
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)` |
|
||
|
||
---
|
||
|
||
## 实施顺序
|
||
|
||
1. SDK 本地副本 + 加 `NoMemory`/`RegisterTextCleaner`
|
||
2. Registry 聚合 cleaners + `CleanText`
|
||
3. Memory 动态 cleaner + 改名调用者
|
||
4. StageHost `ToolDef(name)`
|
||
5. eventloop + context Prune + process 加入 NoMemory 检查
|
||
6. 标记 agentcli/cmd 工具的 `NoMemory: true`
|
||
7. main.go 串联
|
||
8. 删除旧代码(QQ 正则、source 硬编码)
|
||
9. 构建 + 测试
|