package search import ( "context" "encoding/json" "fmt" "net/http" ) // volcengineProfile 是火山引擎·豆包搜索(SearchInfinity,design/27 §2.1;分册 13 官方原文确证)。 // // 接口形态:**POST `/search_api/web_search` + `Authorization: Bearer `** // ⇒ 标准库即可,无需签名/SDK(不必走 IAM AK/SK 的 TOP 网关那条路)。 // 请求字段是**大写驼峰**(Query/SearchType/Count/Filter),与其它家不同 —— 这是上游口径。 // // 分册 13 确证的口径:`Query` 1~100 字符且**不支持多词搜索**;账号维度默认 10 QPS; // 每账号每月 500 次免费(自 2026-03-17 起对个人账号生效);按量 0.020 元/次。 // // ⚠️ 诚实边界(分册 13「未确证」):**响应字段未逐字取全**(含「如意」结构化卡) // ⇒ 用共享提取器按多组候选键容错,不把猜测的字段当事实;拿到真实 key 后按官方文档收窄。 type volcengine struct{ cfg Config } // volcenginePath 是 API Key 接入的端点路径 // (分册 13 官方原文:POST https://open.feedcoopapi.com/search_api/web_search)。 const volcenginePath = "/search_api/web_search" // volcengineSearchTypeWeb 是搜索类型枚举(分册 13 官方原文:目前支持 `web`,需开通 web搜索)。 const volcengineSearchTypeWeb = "web" // volcengineDefaultCount / volcengineMaxCount 是条数口径 // (分册 13 官方原文:Count 最多 50 条,默认 10 条)。 const ( volcengineDefaultCount = 10 volcengineMaxCount = 50 ) // newVolcengine 造一家火山·豆包搜索 provider。 func newVolcengine(cfg Config) Provider { return &volcengine{cfg: cfg} } // Name 实现 Provider。 func (p *volcengine) Name() string { return NameVolcengine } // Search 实现 Provider。 func (p *volcengine) Search(ctx context.Context, q Query) ([]Result, error) { payload := map[string]any{ "Query": q.Text, "SearchType": volcengineSearchTypeWeb, "Count": volcengineCount(q.Count), } body, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("search: volcengine: encode request: %w", err) } data, err := do(ctx, p.cfg, request{ Method: http.MethodPost, URL: baseURLJoin(p.cfg.BaseURL, volcenginePath), Headers: map[string]string{"Authorization": "Bearer " + p.cfg.APIKey}, Body: body, }) if err != nil { return nil, err } return extractResults(data, volcengineSpec()) } // volcengineCount 把调用方要的条数收进官方允许区间(分册 13:最多 50,默认 10)。 func volcengineCount(n int) int { if n <= 0 { return volcengineDefaultCount } if n > volcengineMaxCount { return volcengineMaxCount } return n } // volcengineSpec 是火山·豆包搜索响应的**候选**形状。 // // TODO(未确证):分册 13 只逐字取到了请求字段;响应结构(外层 Result.WebResults 与 // 每条结果的 Title/Url/Snippet/Summary/PublishTime/RankScore 等)**未确证**。 // 这里按可信度从高到低列候选键做容错解析 —— 它是「尽量认得出来」,不是「已确证」; // 拿到真实 key 后必须按官方文档复核并删掉不成立的候选。 func volcengineSpec() spec { return spec{ Arrays: []string{"Result.WebResults", "WebResults", "results", "Result.Results"}, Title: []string{"Title", "title"}, Link: []string{"Url", "url", "Link"}, Snippet: []string{ "Snippet", "snippet", "Summary", "summary", }, Content: []string{"Content", "content", "Summary", "summary"}, Published: []string{"PublishTime", "publishTime", "PublishTimeMs"}, Score: []string{"RankScore", "rankScore", "Score", "score"}, } }