Files
Cyrene_v2/plugins/tools/io/search/baidu.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

99 lines
3.6 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"
"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"},
}
}