第一段正文,包含关键事实。
第二段正文。
package main import ( "encoding/json" "net/http" "net/http/httptest" "net/url" "strings" "testing" ) func newTestPlugin(t *testing.T, h http.HandlerFunc) (*Plugin, *httptest.Server) { t.Helper() srv := httptest.NewServer(h) t.Cleanup(srv.Close) p := &Plugin{ name: "deepsearch", searxURL: srv.URL, maxItems: 5, language: "zh-CN", fetchMax: 1000, userAgent: "test-agent", http: srv.Client(), } return p, srv } // 一份贴近真实 SearXNG 的响应:含重复 URL、缺摘要、多引擎、无响应引擎 const sampleResponse = `{ "query": "deepin 被开除", "results": [ {"url":"https://www.zhihu.com/question/1?utm_source=x","title":"网传统信内核开发工程师因没穿西服被开除","content":"截止1月9日最新情况…","engines":["duckduckgo","brave"],"score":9.5,"publishedDate":"2026-09-10T00:00:00"}, {"url":"https://www.zhihu.com/question/1","title":"网传统信内核开发工程师因没穿西服被开除(重复项)","content":"重复条目","engines":["brave"],"score":1.0}, {"url":"https://www.163.com/dy/article/KIQURODQ.html","title":"离谱!传某信创操作系统大厂因西装开除核心开发者","content":"一位负责Linux内核开发的核心工程师…","engines":["brave","quark"],"score":7.2}, {"url":"https://bbs.deepin.org.cn/zh","title":"deepin官方论坛","content":"","engines":["duckduckgo"],"score":2.0} ], "answers": [], "suggestions": ["deepin 王勇 离职"], "unresponsive_engines": [["baidu","CAPTCHA"],["sogou","unexpected crash"]], "timings": {"search": 1.2} }` // 1) 检索:去重 + 按分数排序 + 摘要/覆盖度输出 func TestSearchDedupAndFormat(t *testing.T) { var gotQuery url.Values p, _ := newTestPlugin(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/search" { gotQuery = r.URL.Query() w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(sampleResponse)) return } http.NotFound(w, r) }) res, err := p.handleSearch(map[string]interface{}{"query": "deepin 被开除", "count": float64(5)}) if err != nil { t.Fatalf("err: %v", err) } if gotQuery.Get("format") != "json" { t.Errorf("必须要求 json 输出,实际 %q", gotQuery.Get("format")) } // SearXNG 的 /search **不认** count/limit(实测两者都返回同样的条数), // 所以「要几条」必须由插件侧截断 —— 也不要再发这种无意义参数(曾以为它生效过)。 if gotQuery.Get("limit") != "" || gotQuery.Get("count") != "" { t.Errorf("不应依赖 SearXNG 的条数参数(它不认): %q", gotQuery.Encode()) } txt := res.(map[string]interface{})["content"].(string) // utm_source 应被规范化掉,重复项只剩一条 if n := strings.Count(txt, "zhihu.com/question/1"); n != 1 { t.Errorf("URL 未正确去重(出现 %d 次):\n%s", n, txt) } if !strings.Contains(txt, "网传统信内核开发工程师") { t.Errorf("缺少标题: %s", txt) } if !strings.Contains(txt, "摘要:") { t.Errorf("应输出摘要: %s", txt) } if !strings.Contains(txt, "baidu(CAPTCHA)") { t.Errorf("应回报无响应引擎(让模型知道覆盖度): %s", txt) } if !strings.Contains(txt, "duckduckgo") || !strings.Contains(txt, "quark") { t.Errorf("应回报引擎覆盖: %s", txt) } // 高分条目应排在前面 if strings.Index(txt, "统信内核开发工程师") > strings.Index(txt, "离谱!") { t.Errorf("未按分数排序:\n%s", txt) } } // 2) 403(未开 json)必须给出可操作提示,而不是裸错误 func TestSearchForbiddenHint(t *testing.T) { p, _ := newTestPlugin(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusForbidden) _, _ = w.Write([]byte("Forbidden")) }) _, err := p.handleSearch(map[string]interface{}{"query": "x"}) if err == nil { t.Fatal("应返回错误") } msg := err.Error() if !strings.Contains(msg, "403") || !strings.Contains(msg, "formats") { t.Errorf("403 提示应指向 json/limiter 配置,实际: %s", msg) } } // 3) 空结果:要给出原因与下一步建议 func TestSearchEmptyHint(t *testing.T) { p, _ := newTestPlugin(t, func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"query":"x","results":[],"suggestions":["换个词"],"unresponsive_engines":[["google","CAPTCHA"]]}`)) }) res, err := p.handleSearch(map[string]interface{}{"query": "x"}) if err != nil { t.Fatalf("err: %v", err) } txt := res.(map[string]interface{})["content"].(string) for _, want := range []string{"未返回结果", "google(CAPTCHA)", "换个词", "deepsearch_news"} { if !strings.Contains(txt, want) { t.Errorf("空结果提示缺少 %q: %s", want, txt) } } } // 4) 新闻:应带 categories=news 与 time_range=week;新闻为空时回退 general func TestNewsParamsAndFallback(t *testing.T) { var calls []url.Values p, _ := newTestPlugin(t, func(w http.ResponseWriter, r *http.Request) { calls = append(calls, r.URL.Query()) if r.URL.Query().Get("categories") == "news" { _, _ = w.Write([]byte(`{"query":"n","results":[]}`)) return } _, _ = w.Write([]byte(`{"query":"n","results":[{"url":"https://a.com/1","title":"回退结果","content":"内容","engines":["brave"],"score":1}]}`)) }) res, err := p.handleNews(map[string]interface{}{"query": "某事"}) if err != nil { t.Fatalf("err: %v", err) } if len(calls) != 2 { t.Fatalf("新闻为空时应回退 general,实际调用 %d 次", len(calls)) } if calls[0].Get("categories") != "news" || calls[0].Get("time_range") != "week" { t.Errorf("首次应为 news + week,实际 categories=%q time_range=%q", calls[0].Get("categories"), calls[0].Get("time_range")) } if tmp := res.(map[string]interface{})["content"].(string); !strings.Contains(tmp, "回退结果") { t.Errorf("回退结果未被采用: %s", tmp) } } // 5) 正文抽取:去脚本/样式/导航,保留 article func TestFetchExtractsArticle(t *testing.T) { page := `
第一段正文,包含关键事实。
第二段正文。
正文内容 " + r.URL.Path + "
"+long+"