Files
homeagent-sdk/example/deepsearch/live_test.go
JianFeeeee 7c0b7a1fb0 feat(deepsearch): 联网检索插件 + SearXNG 生命周期托管
把原 websearch 示例改名为 deepsearch(目录/go.mod/plg.json/工具前缀/README 全量对齐,
工具名 websearch_* → deepsearch_*)。

新增 searxng.go:插件自己托管搜索后端
- 启动探 healthz:已在跑则直接接管(不重启),没跑就 docker compose up -d 并等就绪
- 关闭动作注册为 stop handler(幂等、限时 4s < 内核 5s 宽限期)
- 配置 manage_searxng / searxng_dir / stop_searxng_on_exit
- 崩溃/被 kill 时不关后端(下次启动接管):安全失败方向
- 可注入 cmdRunner + 时间预算,8 项单测离线覆盖接管/拉起/失败/幂等/保留

契约依据(internal/plugin/proc):停止插件 = plugin.stop → RunStopHandlers(LIFO、
幂等)→ Stop() → exit(0),宽限期 5s;stdin 关闭同路径。

验证:19 项单测(18 通过 + 1 联调跳过)、-race 干净、vet/gofmt 干净;内核 E2E 真调用
返回 58 条/37 条、58/58 带摘要;线上 daemon 实测 stop handler 与冷启动(约 3s)。
2026-09-12 23:16:23 +08:00

82 lines
2.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"os"
"strings"
"testing"
)
// 真实后端联调(默认跳过,需显式指定地址):
//
// DEEPSEARCH_LIVE_SEARXNG=http://127.0.0.1:8888 go test -run TestLiveSearxng -v ./...
//
// 它跑的就是当初失败的场景(日志里那条「你的搜索能力好像不太行啊」对应的查询),
// 用来回答一个具体问题:换了后端之后,模型拿到的是不是「带摘要的相关结果」。
func TestLiveSearxng(t *testing.T) {
base := os.Getenv("DEEPSEARCH_LIVE_SEARXNG")
if base == "" {
t.Skip("未设置 DEEPSEARCH_LIVE_SEARXNG跳过真实后端联调")
}
p := &Plugin{
name: "deepsearch",
searxURL: strings.TrimRight(base, "/"),
maxItems: 6,
language: "zh-CN",
fetchMax: 1200,
userAgent: defaultUA,
}
p.ensure()
// 1) 自检
st, err := p.handleStatus(map[string]interface{}{})
if err != nil {
t.Fatalf("status: %v", err)
}
t.Logf("status: %v", st)
// 2) 当初失败的那条查询
res, err := p.handleSearch(map[string]interface{}{"query": "深度科技 deepin 开发者 被开除"})
if err != nil {
t.Fatalf("search: %v", err)
}
txt := res.(map[string]interface{})["content"].(string)
t.Logf("检索结果:\n%s", txt)
if !strings.Contains(txt, "摘要:") {
t.Errorf("结果里应当有摘要(这正是原实现缺失的东西)")
}
if !strings.Contains(txt, "覆盖:") {
t.Errorf("应报告引擎覆盖度")
}
// 3) 正文抓取(取第一条结果的 URL
var firstURL string
for _, line := range strings.Split(txt, "\n") {
l := strings.TrimSpace(line)
if strings.HasPrefix(l, "http") {
firstURL = l
break
}
}
if firstURL == "" {
t.Fatal("未从结果中解析出 URL")
}
page, err := p.handleFetch(map[string]interface{}{"url": firstURL, "max_chars": float64(600)})
if err != nil {
t.Logf("抓取 %s 失败(真实站点有反爬/需 JS 属正常):%v", firstURL, err)
} else {
body := page.(map[string]interface{})["content"].(string)
t.Logf("抓取 %s 正文前 400 字:%s", firstURL, oneLine(body, 400))
}
// 4) 深检索
deep, err := p.handleDeep(map[string]interface{}{"query": "统信 UOS 内核工程师 西装 事件", "top_k": float64(2)})
if err != nil {
t.Fatalf("deep: %v", err)
}
dTxt := deep.(map[string]interface{})["content"].(string)
if !strings.Contains(dTxt, "候选清单") || !strings.Contains(dTxt, "正文证据") {
t.Errorf("深检索输出结构不对")
}
t.Logf("深检索输出前 800 字:\n%s", oneLine(dTxt, 800))
}