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

75 lines
3.2 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"
)
// zhipuProfile 是智谱 BigModel 联网搜索(design/27 §2.1 国内首选;分册 11)。
//
// 认证:`Authorization: Bearer <api_key>`;纯 REST(POST + JSON)⇒ 零新依赖。
// 官方**定价已确证**(分册 11 的官方定价页逐字):Search-Std 0.01 元/次、
// Search-Pro 0.03、Search-Pro-Sogou 0.05、Search-Pro-Quark 0.05。
//
// ⚠️ 诚实边界(分册 11「未确证」):**完整请求/响应 JSON schema 未取到**,只确证了
// 字段名(请求侧 search_engine/search_query/…,响应侧 title/link/content)。
// 因此这里:① 请求只发确证过的两个字段;② 响应用共享提取器按**多组候选键**容错,
// 不把任何猜测的字段当事实。拿到真实 key 后应按官方 API Reference 复核并收窄候选键。
type zhipu struct{ cfg Config }
// zhipuPath 是联网搜索端点路径(分册 11 官方原文:POST https://open.bigmodel.cn/api/paas/v4/web_search)。
const zhipuPath = "/api/paas/v4/web_search"
// zhipuEngineStd 是默认搜索型号。取 `search_std`(自研基础搜索,官方定价页确证
// 0.01 元/次,四档里最便宜)。⚠️ `search_engine` 是否可选官方未明说(分册 11),
// 显式写死一档以固定成本;要换档请改这里或后续开放为配置项。
const zhipuEngineStd = "search_std"
// newZhipu 造一家智谱 provider。
func newZhipu(cfg Config) Provider { return &zhipu{cfg: cfg} }
// Name 实现 Provider。
func (p *zhipu) Name() string { return NameZhipu }
// Search 实现 Provider。
func (p *zhipu) Search(ctx context.Context, q Query) ([]Result, error) {
// TODO(未确证):响应可能很大(自带网页正文,分册 11 提示注意本仓 1 MiB 上限)。
// 官方另有 `content_size` 可限正文长度,但其取值域未确证 ⇒ 不写死,留待拿到
// 真实 key 后按官方文档补上(当前超限会如实返回 ErrResponseTooLarge)。
payload := map[string]any{
"search_engine": zhipuEngineStd,
"search_query": q.Text,
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("search: zhipu: encode request: %w", err)
}
data, err := do(ctx, p.cfg, request{
Method: http.MethodPost,
URL: baseURLJoin(p.cfg.BaseURL, zhipuPath),
Headers: map[string]string{"Authorization": "Bearer " + p.cfg.APIKey},
Body: body,
})
if err != nil {
return nil, err
}
return extractResults(data, zhipuSpec())
}
// zhipuSpec 是智谱响应的候选形状。官方示例以「工具块」给出 WebBrowserOutput{title,link,content}
// (分册 11),故 title/link/content 是确证键;外层数组键名未确证 ⇒ 给多组候选。
func zhipuSpec() spec {
return spec{
Arrays: []string{"search_result", "results", "data", "data.results", "web_search_result"},
Title: []string{"title", "name"},
Link: []string{"link", "url"},
Snippet: []string{"snippet", "summary", "abstract"},
Content: []string{"content", "text"},
// 官方示例未见发布时间/评分字段 ⇒ 只给通用候选,取不到就是空。
Published: []string{"publish_date", "published", "date"},
Score: []string{"score", "rank"},
}
}