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:
1 parent
236b202fda
commit
69a3b73777
24 files changed
+3251
-171
No files matched your search
@@ -0,0 +1,89 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// bochaProfile 是博查 Bocha(design/27 §2 A 档「中文优先」;分册 02 官方原文确证)。
|
||||
//
|
||||
// POST `/v1/web-search` + `Authorization: Bearer`;响应 `webPages.value[]` 的
|
||||
// `name/url/snippet/summary/datePublished` —— **与 SearXNG 字段近乎同名**,归一化最省事。
|
||||
//
|
||||
// 两个诚实边界(分册 02):
|
||||
//
|
||||
// - 官方文档域名为 `api.bocha.cn`,而官网首页 curl 样例仍用 `api.bochaai.com`,
|
||||
// 两者关系官方未说明 ⇒ 默认取**文档现行口径** `api.bocha.cn`(见 config 的默认值),
|
||||
// 两域都可由 `WEB_SEARCH_BOCHA_BASE_URL` 覆盖。
|
||||
// - 中文检索质量**未实测**(无 key)。
|
||||
type bocha struct{ cfg Config }
|
||||
|
||||
// bochaPath 是 Web Search 端点路径(分册 02:POST https://api.bocha.cn/v1/web-search)。
|
||||
const bochaPath = "/v1/web-search"
|
||||
|
||||
// bochaDefaultCount / bochaMaxCount 是请求条数口径(分册 02 官方原文:
|
||||
// 「返回条数,可填范围 1-50,单次最大 50,默认 10」)。
|
||||
const (
|
||||
bochaMaxCount = 50
|
||||
bochaDefaultCount = 10
|
||||
)
|
||||
|
||||
// newBocha 造一家博查 provider。
|
||||
func newBocha(cfg Config) Provider { return &bocha{cfg: cfg} }
|
||||
|
||||
// Name 实现 Provider。
|
||||
func (p *bocha) Name() string { return NameBocha }
|
||||
|
||||
// Search 实现 Provider。
|
||||
func (p *bocha) Search(ctx context.Context, q Query) ([]Result, error) {
|
||||
// summary=true 换取中文摘要(分册 02:默认 false 不给 summary);
|
||||
// freshness 用官方推荐的 noLimit(官方原文:「推荐使用 noLimit。搜索算法会自动
|
||||
// 进行时间范围的改写,效果更佳」)。
|
||||
payload := map[string]any{
|
||||
"query": q.Text,
|
||||
"summary": true,
|
||||
"freshness": "noLimit",
|
||||
"count": bochaClampCount(q.Count),
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("search: bocha: encode request: %w", err)
|
||||
}
|
||||
data, err := do(ctx, p.cfg, request{
|
||||
Method: http.MethodPost,
|
||||
URL: baseURLJoin(p.cfg.BaseURL, bochaPath),
|
||||
Headers: map[string]string{"Authorization": "Bearer " + p.cfg.APIKey},
|
||||
Body: body,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return extractResults(data, bochaSpec())
|
||||
}
|
||||
|
||||
// bochaClampCount 把调用方要的条数收进官方允许区间(分册 02:1-50,默认 10)。
|
||||
func bochaClampCount(n int) int {
|
||||
if n <= 0 {
|
||||
return bochaDefaultCount
|
||||
}
|
||||
if n > bochaMaxCount {
|
||||
return bochaMaxCount
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// bochaSpec 是博查的响应形状(分册 02「响应字段」官方原文确证)。
|
||||
// `summary` 只在请求里 `summary=true` 时才有 —— 我们恒置 true。
|
||||
func bochaSpec() spec {
|
||||
return spec{
|
||||
Arrays: []string{"webPages.value"},
|
||||
Title: []string{"name"},
|
||||
Link: []string{"url"},
|
||||
Snippet: []string{"snippet"},
|
||||
Content: []string{"summary"},
|
||||
Published: []string{"datePublished"},
|
||||
// 官方字段表未列评分(`id` 是排序 ID、不是分数)⇒ 不给 Score 候选。
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user