主人定调「先把原生的做进插件,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),并有「未登记⇒启动报错」的防回归断言
59 lines
2.1 KiB
Go
59 lines
2.1 KiB
Go
package search
|
||
|
||
import (
|
||
"context"
|
||
"net/http"
|
||
"net/url"
|
||
)
|
||
|
||
// aliyunIQSProfile 是阿里云信息查询服务(AI 联网搜索,IQS;design/27 §2.1,分册 15)。
|
||
//
|
||
// 接入形态对本仓最友好:**GET `/search/genericSearch` + `X-API-Key`(免签名、免 SDK)**
|
||
// ⇒ 标准库即可,恒不破「零新依赖」。
|
||
//
|
||
// 分册 15 确证的两个硬约束:
|
||
//
|
||
// - 官方原文:**默认返回网页 10 条,暂不支持调整** ⇒ 本 provider 忽略 Query.Count
|
||
// (本地截断仍生效,见 io 层的 Finalize)。
|
||
// - `mainText` 默认只给**前 500 字符**(要 3000 字需联系客户经理开通)⇒ 正文偏短是
|
||
// 上游口径,不是本仓截断。
|
||
type aliyunIQS struct{ cfg Config }
|
||
|
||
// aliyunIQSPath 是联网搜索端点路径(分册 15 官方原文:GET /search/genericSearch?query=<q>)。
|
||
const aliyunIQSPath = "/search/genericSearch"
|
||
|
||
// newAliyunIQS 造一家阿里云 IQS provider。
|
||
func newAliyunIQS(cfg Config) Provider { return &aliyunIQS{cfg: cfg} }
|
||
|
||
// Name 实现 Provider。
|
||
func (p *aliyunIQS) Name() string { return NameAliyunIQS }
|
||
|
||
// Search 实现 Provider。
|
||
func (p *aliyunIQS) Search(ctx context.Context, q Query) ([]Result, error) {
|
||
target := baseURLJoin(p.cfg.BaseURL, aliyunIQSPath) + "?query=" + url.QueryEscape(q.Text)
|
||
data, err := do(ctx, p.cfg, request{
|
||
Method: http.MethodGet,
|
||
URL: target,
|
||
Headers: map[string]string{"X-API-Key": p.cfg.APIKey},
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return extractResults(data, aliyunIQSSpec())
|
||
}
|
||
|
||
// aliyunIQSSpec 是 IQS 的响应形状(分册 15「响应字段」官方原文确证):
|
||
// `pageItems[].{title,link,snippet,mainText,markdownText,publishTime,score}`。
|
||
// 正文优先取 `mainText`(纯文本,更适合进提示词),其次 `markdownText`。
|
||
func aliyunIQSSpec() spec {
|
||
return spec{
|
||
Arrays: []string{"pageItems"},
|
||
Title: []string{"title"},
|
||
Link: []string{"link"},
|
||
Snippet: []string{"snippet"},
|
||
Content: []string{"mainText", "markdownText"},
|
||
Published: []string{"publishTime"},
|
||
Score: []string{"score"},
|
||
}
|
||
}
|