Files
Cyrene_v2/plugins/tools/io/search/searxng.go
T
AskaEth 69a3b73777 feat(search): 原生 provider 层(searxng/智谱/阿里IQS/博查/火山/百度)+ 降级链(契约无改动)
主人定调「先把原生的做进插件,MCP 路线等后端 MCP 完善后再接」。
- 新包 plugins/tools/io/search/:一 provider 一文件 + 共享 http/提取器 + 归一化 Result;域名过滤/去重/排序**归一化后统一做一次**
- 配置 15 键:WEB_SEARCH_PROVIDER(默认 searxng)/_FALLBACK/_MAX_RESULTS + 五家 ×(API_KEY/BASE_URL);WEB_SEARCH_TIMEOUT 语义改为「**每次上游尝试**」(降级链才有意义)
- 硬语义:未注册/未配置的主 provider ⇒ **启动期报错**(不许静默回落);off ⇒ 不注册工具;缺 key 的家不进可用清单;降级链**如实标实际应答的 provider**(变异:拿主力冒充 ⇒ 测试红「provider = zhipu, want bocha」)
- 密钥绝不进日志/响应(有断言);错误分类(鉴权/限流/额度/网络)+ **空结果 ≠ 成功**(变异暴露过一个死分支⇒下移到 searchWith 修掉)
- searxng 真机 e2e 通过(provider=searxng 5 条);**其余五家无 key ⇒ 如实标未实测**;契约无改动(无新端点、工具 schema 不变)
- 注册表留好 B 档接入位(tavily/exa/firecrawl/serper/serpapi),并有「未登记⇒启动报错」的防回归断言
2026-10-05 22:38:20 +08:00

68 lines
2.3 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 search
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
)
// searxngProfile 是自建 SearXNG(A 档、零 key、**本地兜底**;design/27 §2 与分册 08)。
//
// 协议(WS1-A 已落地,这里逐字保留其语义):GET,`{query}` 占位符优先,否则追加
// `q=<q>&format=json`;响应是 `{"results":[{title,url,content,snippet,score}]}`。
// 它是「底线」不是「主力」:上游可达性决定覆盖面(分册 08 实测:mwmbl 稳定、
// bing 静默返空、sogou 会 CAPTCHA)。
type searxng struct{ cfg Config }
// newSearXNG 造一家 SearXNG provider。BaseURL 为空也能造出来 —— 工具层会以
// 「no search endpoint configured」如实报错(WS1-A 的既有行为,不在这里提前崩)。
func newSearXNG(cfg Config) Provider { return &searxng{cfg: cfg} }
// Name 实现 Provider。
func (p *searxng) Name() string { return NameSearXNG }
// Search 实现 Provider。
func (p *searxng) Search(ctx context.Context, q Query) ([]Result, error) {
target, err := buildSearxngURL(p.cfg.BaseURL, q.Text)
if err != nil {
return nil, err
}
data, err := do(ctx, p.cfg, request{Method: http.MethodGet, URL: target})
if err != nil {
return nil, err
}
return extractResults(data, searxngSpec())
}
// searxngSpec 是 SearXNG 的结果形状(分册 08:`results[].{url,title,content,score}`)。
// 只列确证过的键;没确证的(publishedDate 之类)不写死。
func searxngSpec() spec {
return spec{
Arrays: []string{"results"},
Title: []string{"title"},
Link: []string{"url"},
Snippet: []string{"snippet"},
Content: []string{"content"},
Score: []string{"score"},
}
}
// buildSearxngURL 用一个端点与查询词拼出请求地址(WS1-A 的原逻辑,逐字保留):
// 端点里含 `{query}` 占位符就替换,否则追加 `q=` 与 `format=json`。
func buildSearxngURL(endpoint, query string) (string, error) {
if strings.TrimSpace(endpoint) == "" {
return "", fmt.Errorf("search: searxng: no search endpoint configured")
}
q := url.QueryEscape(query)
if strings.Contains(endpoint, "{query}") {
return strings.ReplaceAll(endpoint, "{query}", q), nil
}
sep := "?"
if strings.Contains(endpoint, "?") {
sep = "&"
}
return endpoint + sep + "q=" + q + "&format=json", nil
}