mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-02 15:23:57 +00:00
feat(channel): N1a —— inputch 一等化(归属插件/归属 agent/容量/共享登记表)+ 单工具多视图总览
设计:docs/zh/resident-subagent-design.md §4.6(里程碑 N1a)。
## 用户要求
「父 agent 可以看到所有已注册的 inputch 以及 inputch 的划分情况,用**单工具多视图**方式构筑」
## 登记层(internal/agent/io/inputch.go)
inputch 是**最基本的输入路由单位**(由插件注册,一个插件可注册多个),
所以登记表以 inputch 为键,每条记录:
名字 / 归属插件 / 归属 agent(被划给谁) / 容量 / 默认回程 outputch / 记忆策略(ChannelDef)
- 新增 `ChannelRegistry`,设计成**可共享对象**(`*ChannelRegistry`):
根 agent 与驻留子共用同一份,"划入/授权"才有意义;`SetChannelRegistry` 注入。
- **插件重载不得抹掉划分**:重复登记只更新「归属插件 + 策略」,
保留已有 Owner/Capacity/Output(否则一次 reload 就把父做的划分清空)。
- `Assign` 语义按设计 R6 默认:**读写授权,不转移所有权**(Plugin 与 Owner 分别记录)。
- 原 `inputChannels map[string]ChannelDef` 被登记表取代;`GetInputChannelDef` 保持兼容。
- `plugin.Registry` 注册时带上**归属插件名**(此前完全无归属信息)。
## 总览工具(internal/agent/core/inputch.go)
单工具 **`input_channels`** + `view` 参数(不是一堆小工具):
all(默认)= 全部已注册(带归属插件)
mine = 划给本 agent 的
unassigned = 尚未划出的
by_agent = 划分情况总览(按归属分组)
detail = 单个 inputch 全字段(需 name)
未知 view **报错并列出可用值**(拼错不得被静默当成默认视图);登记表为空时明确说明。
## 验收
- `internal/agent/io/inputch_test.go`:归属记录、重载保划分、Assign/视图数据面、
**跨 manager 共享登记表**、策略查询向后兼容 —— 5 项
- `internal/agent/core/inputch_test.go`:单工具多视图逐视图断言(含未知 view 与空表)—— 2 项
- 全仓 `go test ./...` 37 包 ok / 0 FAIL;`-race ./internal/agent/... ./internal/plugin/...` 干净
This commit is contained in:
118
internal/plugins/deepsearch_e2e_test.go
Normal file
118
internal/plugins/deepsearch_e2e_test.go
Normal file
@ -0,0 +1,118 @@
|
||||
//go:build linux || darwin
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// deepsearch 插件的真实调用往返:内核 ExecuteTool → RPC → 插件子进程 → 本地 SearXNG → 结果回传。
|
||||
//
|
||||
// 与 TestRealPlugin_ToolInvokeRoundTrip 的区别:那条只断言「链路通(拿到结果或拿到插件侧的错误)」,
|
||||
// 这条断言**内容形状**——返回里必须有「摘要」与「引擎覆盖度」。这正是旧实现(抓 Bing HTML)
|
||||
// 拿不到的东西,也是「搜索能力不行」的根因,所以它必须成为回归判据。
|
||||
//
|
||||
// 前置:本机 127.0.0.1:8888 上跑着 SearXNG(部署见 /root/searxng-agent)。
|
||||
// 未启动时 fail 并给出可操作提示,而不是 skip —— 否则这条判据会在环境退化时静默失效。
|
||||
func TestRealPlugin_DeepSearchInvoke(t *testing.T) {
|
||||
env := setupIntegration(t)
|
||||
defer env.cleanup()
|
||||
|
||||
plgDir := filepath.Join(env.tmpDir, "plugins")
|
||||
installRealPlugin(t, plgDir, "deepsearch")
|
||||
|
||||
if err := env.pluginReg.Load(plgDir); err != nil {
|
||||
t.Fatalf("加载插件: %v", err)
|
||||
}
|
||||
if env.pluginReg.Get("deepsearch") == nil {
|
||||
t.Fatal("deepsearch 未经 proc 通道加载")
|
||||
}
|
||||
|
||||
var toolName string
|
||||
for _, def := range env.stageHost.GetToolDefs() {
|
||||
if strings.HasPrefix(def.Name, "deepsearch") && strings.HasSuffix(def.Name, "_search") {
|
||||
toolName = def.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
if toolName == "" {
|
||||
t.Fatal("deepsearch 未注册检索工具")
|
||||
}
|
||||
t.Logf("调用工具 %s", toolName)
|
||||
|
||||
// 用当初失败的那条查询做判据
|
||||
res, err := env.stageHost.ExecuteTool(toolName, map[string]interface{}{
|
||||
"query": "深度科技 deepin 开发者 被开除",
|
||||
"count": float64(3),
|
||||
})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found in any plugin") {
|
||||
t.Fatalf("工具未注册到 stageHost: %v", err)
|
||||
}
|
||||
t.Fatalf("工具调用失败(检查本机 SearXNG 是否在 127.0.0.1:8888 运行): %v", err)
|
||||
}
|
||||
if res == nil {
|
||||
t.Fatal("工具返回 nil 且无错误")
|
||||
}
|
||||
|
||||
text := fmt.Sprintf("%v", res)
|
||||
t.Logf("工具返回前 500 字:\n%s", truncRunes(text, 500))
|
||||
|
||||
if !strings.Contains(text, "摘要:") {
|
||||
t.Errorf("返回内容缺少摘要——这正是旧实现拿不到的部分:\n%s", truncRunes(text, 800))
|
||||
}
|
||||
if !strings.Contains(text, "覆盖:") {
|
||||
t.Errorf("返回内容缺少引擎覆盖度(模型据此判断可信度):\n%s", truncRunes(text, 800))
|
||||
}
|
||||
if !strings.Contains(text, "http") {
|
||||
t.Errorf("返回内容缺少结果链接:\n%s", truncRunes(text, 800))
|
||||
}
|
||||
}
|
||||
|
||||
// deepsearch_status 也走一遍真实调用:它把「后端是否可用、哪些引擎在出结果」暴露成工具,
|
||||
// 出问题时 agent 可以先自检,而不是盲目换词重搜。
|
||||
func TestRealPlugin_DeepSearchStatusInvoke(t *testing.T) {
|
||||
env := setupIntegration(t)
|
||||
defer env.cleanup()
|
||||
|
||||
plgDir := filepath.Join(env.tmpDir, "plugins")
|
||||
installRealPlugin(t, plgDir, "deepsearch")
|
||||
if err := env.pluginReg.Load(plgDir); err != nil {
|
||||
t.Fatalf("加载插件: %v", err)
|
||||
}
|
||||
|
||||
var toolName string
|
||||
for _, def := range env.stageHost.GetToolDefs() {
|
||||
if strings.HasPrefix(def.Name, "deepsearch") && strings.HasSuffix(def.Name, "_status") {
|
||||
toolName = def.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
if toolName == "" {
|
||||
t.Fatal("deepsearch 未注册自检工具")
|
||||
}
|
||||
|
||||
res, err := env.stageHost.ExecuteTool(toolName, map[string]interface{}{"probe": "test"})
|
||||
if err != nil {
|
||||
t.Fatalf("自检调用失败(检查本机 SearXNG 是否运行): %v", err)
|
||||
}
|
||||
text := fmt.Sprintf("%v", res)
|
||||
t.Logf("自检返回:%s", truncRunes(text, 400))
|
||||
|
||||
for _, want := range []string{"healthz", "search_ok", "engines_returning_results"} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Errorf("自检结果缺少字段 %q:%s", want, truncRunes(text, 500))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func truncRunes(s string, n int) string {
|
||||
r := []rune(s)
|
||||
if len(r) <= n {
|
||||
return s
|
||||
}
|
||||
return string(r[:n]) + "…"
|
||||
}
|
||||
Reference in New Issue
Block a user