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,98 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// baiduQianfanProfile 是百度·千帆 AI 搜索(design/27 §2.1;分册 16 官方原文确证)。
|
||||
//
|
||||
// 接口形态:**POST `/v2/ai_search/web_search` + `Authorization: Bearer <API_KEY>`**
|
||||
// ⇒ 标准库即可。
|
||||
//
|
||||
// 分册 16 确证的口径:
|
||||
//
|
||||
// - 请求必须走 `messages` 数组,且**百度搜索仅支持单轮输入**(多轮则以最后一条
|
||||
// content 为查询)⇒ 本 provider 只放一条 user 消息。
|
||||
// - `search_source` 固定值 `baidu_search_v2`。
|
||||
// - `resource_type_filter[].top_k` 上限:**网页 50**(默认 20)。
|
||||
// - 计费:标准版 0.036 元/次、增强版 0.072 元/次;每月免费 1500 次(约每天 50 次)。
|
||||
//
|
||||
// ⚠️ 诚实边界(分册 16「未确证」):**响应结构未逐字取全** ⇒ 用共享提取器按多组候选键
|
||||
// 容错,不把猜测的字段当事实;拿到真实 key 后按官方文档收窄。
|
||||
type baiduQianfan struct{ cfg Config }
|
||||
|
||||
// baiduQianfanPath 是百度搜索端点路径
|
||||
// (分册 16 官方原文:POST /v2/ai_search/web_search,HOST qianfan.baidubce.com)。
|
||||
const baiduQianfanPath = "/v2/ai_search/web_search"
|
||||
|
||||
// baiduSearchSourceV2 是搜索引擎版本固定值(分册 16 官方原文:固定值 `baidu_search_v2`)。
|
||||
const baiduSearchSourceV2 = "baidu_search_v2"
|
||||
|
||||
// baiduDefaultTopK / baiduMaxTopK 是网页模态的 top_k 口径
|
||||
// (分册 16 官方原文:默认 `[{"type":"web","top_k":20}]`,网页 top_k 上限 50)。
|
||||
const (
|
||||
baiduDefaultTopK = 20
|
||||
baiduMaxTopK = 50
|
||||
)
|
||||
|
||||
// newBaiduQianfan 造一家百度千帆 provider。
|
||||
func newBaiduQianfan(cfg Config) Provider { return &baiduQianfan{cfg: cfg} }
|
||||
|
||||
// Name 实现 Provider。
|
||||
func (p *baiduQianfan) Name() string { return NameBaiduQianfan }
|
||||
|
||||
// Search 实现 Provider。
|
||||
func (p *baiduQianfan) Search(ctx context.Context, q Query) ([]Result, error) {
|
||||
payload := map[string]any{
|
||||
"messages": []map[string]string{{"role": "user", "content": q.Text}},
|
||||
// 单轮输入:百度搜索只认最后一条 content,这里就只放一条。
|
||||
"search_source": baiduSearchSourceV2,
|
||||
"resource_type_filter": []map[string]any{
|
||||
{"type": "web", "top_k": baiduTopK(q.Count)},
|
||||
},
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("search: baidu_qianfan: encode request: %w", err)
|
||||
}
|
||||
data, err := do(ctx, p.cfg, request{
|
||||
Method: http.MethodPost,
|
||||
URL: baseURLJoin(p.cfg.BaseURL, baiduQianfanPath),
|
||||
Headers: map[string]string{"Authorization": "Bearer " + p.cfg.APIKey},
|
||||
Body: body,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return extractResults(data, baiduQianfanSpec())
|
||||
}
|
||||
|
||||
// baiduTopK 把调用方要的条数收进官方允许区间(分册 16:网页 top_k 上限 50,默认 20)。
|
||||
func baiduTopK(n int) int {
|
||||
if n <= 0 {
|
||||
return baiduDefaultTopK
|
||||
}
|
||||
if n > baiduMaxTopK {
|
||||
return baiduMaxTopK
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// baiduQianfanSpec 是百度千帆响应的**候选**形状。
|
||||
//
|
||||
// TODO(未确证):分册 16 只逐字取到了请求参数;响应结构**未确证**。
|
||||
// 这里的候选键是容错解析(尽量认得出来),拿到真实 key 后必须按官方文档复核并收窄。
|
||||
func baiduQianfanSpec() spec {
|
||||
return spec{
|
||||
Arrays: []string{"references", "search_results", "data.references", "results"},
|
||||
Title: []string{"title", "Title"},
|
||||
Link: []string{"url", "Url", "link"},
|
||||
Snippet: []string{"snippet", "summary", "abstract"},
|
||||
Content: []string{"content", "Content"},
|
||||
Published: []string{"date", "publish_time", "publishTime"},
|
||||
Score: []string{"score", "rank"},
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user