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),并有「未登记⇒启动报错」的防回归断言
This commit is contained in:
AskaEth committed 2026-10-05 22:38:20 +08:00
1 parent 236b202fda
commit 69a3b73777
24 files changed
+3251 -171

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
package search
import (
"context"
"net/http"
)
// Provider 是一种搜索后端的**窄接口**(design/27 §4)。实现必须并发安全:
// 工具循环可能从多个 goroutine 调它。
type Provider interface {
// Name 返回该 provider 的配置名(即 WEB_SEARCH_PROVIDER / WEB_SEARCH_FALLBACK 里的取值)。
Name() string
// Search 执行一次检索,返回**已归一化**的结果(可能为空)。
//
// 返回空切片(len==0)**不是错误**:它表示「上游正常应答但什么都没查到」,
// 由调用方按「空结果 ≠ 成功」处置(见 ErrNoResults 的注释)。
Search(ctx context.Context, q Query) ([]Result, error)
}
// Query 是一次检索的输入。工具 schema 仍是 `query`/`count`(design/27 §4:不破坏契约)。
//
// 域名过滤**不在这里**:按 design/27 §4,域名过滤/去重/排序统一在归一化之后做一次
// (见 Finalize 的 Options),各家 provider 一律不许自己过滤。
type Query struct {
// Text 是检索词。
Text string
// Count 是调用方期望的结果条数(>0 才有意义)。provider 支持就透传,
// 不支持(如阿里云 IQS 默认 10 条不可调)就忽略 —— 本地截断仍然生效。
Count int
}
// Result 是所有 provider **统一归一化**后的结果(design/27 §4)。
type Result struct {
Link string
Title string
Snippet string
Content string
Published string
Score float64
}
// 本期内置的 provider 名(对外配置值 WEB_SEARCH_PROVIDER 的合法取值之一)。
const (
// NameSearXNG 是自建 SearXNG(A 档、零 key、本地兜底)。
NameSearXNG = "searxng"
// NameZhipu 是智谱 BigModel 联网搜索(design/27 §2.1 国内首选)。
NameZhipu = "zhipu"
// NameAliyunIQS 是阿里云信息查询服务 IQS(HTTP 免签名免 SDK)。
NameAliyunIQS = "aliyun_iqs"
// NameBocha 是博查 Bocha(中文优先)。
NameBocha = "bocha"
// NameVolcengine 是火山引擎·豆包搜索 SearchInfinity。
NameVolcengine = "volcengine"
// NameBaiduQianfan 是百度千帆 AI 搜索。
NameBaiduQianfan = "baidu_qianfan"
)
// Config 是构造一家 provider 所需的一切接线(密钥/地址/客户端/响应上限/URL 守卫)。
//
// 它由组合根(plugins/tools/io 的 searchWires)填好 —— provider 自己不读配置、
// 不读环境变量、不开新的 HTTP 客户端(单二进制、零新依赖:design/27 §1.5)。
type Config struct {
// APIKey 是上游凭据。**绝不进日志/响应/提示词**。
APIKey string
// BaseURL 是上游基地址(不含各家自己的路径),可被各自的环境变量覆盖。
BaseURL string
// Client 是这一套工具共用的 HTTP 客户端(已带 SSRF 重定向守卫与超时)。
Client *http.Client
// MaxBytes 是单次响应体读取上限(字节);≤0 用包内默认。
MaxBytes int64
// Guard 是**组合根注入的 SSRF 守卫**:发请求前对最终地址做一次判定
// (scheme / host / 解析结果)。它复用 plugins/tools/io 的既有守卫与本工具自己
// 那份白名单,**不改** http / web-fetch 共用的白名单。
//
// 为 nil 时不做判定(search 包的夹具测试直接用 httptest 客户端)。
Guard func(ctx context.Context, rawURL string) error
}