接入位表六家扩到八家,端点形状与错误码一字未改(契约 v0.87,仅两处名单)。
- tencent_wsa:POST {base}/SearchPro + Authorization Bearer,零签名。
两条官方口径决定实现:HTTP 状态码恒 200(判失败只能读 Response.Error.Code);
Pages 是「JSON 字符串数组」需二次解析。错误码按官方表分类
(UnauthorizedOperation/AuthFailure.* = 凭据、ResourceUnavailable = 欠费、
RequestLimitExceeded* = 限流),文案只带 code 不带上游 Message。
- metaso:POST {base}/api/v1/search,响应 schema 仅第三方来源且互相打架,
实现走宽松解析(数组候选与字段名回退),逐处标「未实测」。
- 零新 env 键:两家只走 $DATA_DIR/config/plugins/web-search.json,
.env / .env.example 未动;组合根 websearch_providers.go 零功能改动。
- 契约 v0.87:values.provider 合法闭集与 plugin.providers 候选各加两条,
计数不变(14 + 25 + 40 = 79)。
156 lines
8.5 KiB
Go
156 lines
8.5 KiB
Go
package config
|
||
|
||
import (
|
||
"net/url"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// WebSearchConfig 是 web-search 工具的搜索配置(design/27-网络搜索与检索提供商;切片 WS1-A/WS1-B)。
|
||
// 写法照既有 VoiceConfig 惯例:一组 provider 键 + 未配置则如实降级,绝不硬编码。
|
||
type WebSearchConfig struct {
|
||
// Provider 选谁当后端(design/27 §3):`searxng`(默认)/ `zhipu` / `aliyun_iqs` /
|
||
// `bocha` / `volcengine` / `baidu_qianfan` / `tencent_wsa` / `metaso`。
|
||
//
|
||
// ⚠️ 后两家(`tencent_wsa` / `metaso`)**没有对应的 env 字段**(`.env` 已冻结:不加键)
|
||
// —— 它们只在 `$DATA_DIR/config/plugins/web-search.json` 里配(组合根按插件侧的接入位表
|
||
// 逐家投影,见 cmd/cyrene/websearch_providers.go)。本结构里的逐家字段仍然**只**服务
|
||
// 已有的 6 家播种/覆盖。
|
||
//
|
||
// **语义硬约束**(design/27 §3,由组合根的 io 工具集在启动期执行):
|
||
// - 指向**未登记**(如本期未做的 tavily)或**未配置**(缺 key/端点)的名字
|
||
// ⇒ **启动期显式报错**,绝不静默回落到兜底;
|
||
// - 写空串 / `off` / `none`(大小写不敏感)⇒ **不注册 `web-search` 工具**
|
||
// (模型根本看不见它),见 ResolveProvider。
|
||
Provider string `env:"WEB_SEARCH_PROVIDER" default:"searxng"`
|
||
// Fallback 是降级链(design/27 §3):从左到右试,空 = 只用手选的 provider。
|
||
// 链上的家**只在「不可用」时**才被跳过(无 key / 网络失败 / 额度错误…);
|
||
// 结果里会如实标注**实际应答**的那一家,绝不拿兜底冒充主力。
|
||
Fallback []string `env:"WEB_SEARCH_FALLBACK"`
|
||
// MaxResults 是单次检索返回的条数上限(主人 2026-10-07 裁定:默认由 5 提到 10;
|
||
// design/27 §3 记的仍是 5,待同步设计文档)。它是**真上限**:模型给的 count 只能要得
|
||
// 更少,顶不掉它(`plugins/tools/io/web_search.go` 的 resultLimit)。
|
||
MaxResults int `env:"WEB_SEARCH_MAX_RESULTS" default:"10"`
|
||
|
||
// Endpoint 是 **searxng provider** 的端点(SearXNG 兼容的 `format=json` GET 接口)。
|
||
//
|
||
// 默认值指向**本机兜底**:`scripts/searxng/settings.yml` 配套的 SearXNG 实例
|
||
// (宿主机 http://127.0.0.1:38471)。默认指向本机而不是某个云搜索 API,是因为
|
||
// 兜底实例不需要任何第三方 key、开箱即用。
|
||
//
|
||
// **显式关闭**(值写成空串 / off / none,大小写与首尾空白不敏感)⇒ 组合根
|
||
// **不注册** web-search 工具:模型根本看不见它,而不是注册后每次调用都报错
|
||
// (见 ResolveEndpoint)。
|
||
Endpoint string `env:"WEB_SEARCH_ENDPOINT" default:"http://127.0.0.1:38471"`
|
||
// Timeout 是单次搜索请求的超时(Go duration 字符串,如 "20s")。
|
||
//
|
||
// **未配置(零值)语义 = 「沿用 io 包既有超时」**:这里刻意不写第二个默认值,
|
||
// 否则「文档承诺的超时」与「工具真正用的超时」会各有一份而悄悄漂移。
|
||
Timeout time.Duration `env:"WEB_SEARCH_TIMEOUT"`
|
||
|
||
// ---- 各家独立一组键(design/27 §3)----
|
||
//
|
||
// 口径:`WEB_SEARCH_<NAME>_API_KEY` 给凭据、`WEB_SEARCH_<NAME>_BASE_URL` 给基地址
|
||
// (各自的固定路径由 provider 代码拼上)。**未配置的家不出现在可用清单里**
|
||
// (fail-closed:绝不挂着报错)。密钥只从配置读,**绝不进日志/响应/提示词**。
|
||
|
||
// Zhipu* 是智谱 BigModel(design/27 §2.1 国内首选)。
|
||
ZhipuAPIKey string `env:"WEB_SEARCH_ZHIPU_API_KEY"`
|
||
ZhipuBaseURL string `env:"WEB_SEARCH_ZHIPU_BASE_URL" default:"https://open.bigmodel.cn"`
|
||
|
||
// AliyunIQS* 是阿里云信息查询服务(IQS;HTTP + X-API-Key,免签名免 SDK)。
|
||
AliyunIQSAPIKey string `env:"WEB_SEARCH_ALIYUN_IQS_API_KEY"`
|
||
AliyunIQSBaseURL string `env:"WEB_SEARCH_ALIYUN_IQS_BASE_URL" default:"https://cloud-iqs.aliyuncs.com"`
|
||
|
||
// Bocha* 是博查(中文优先)。默认基地址取**官方文档现行口径** `api.bocha.cn`
|
||
// (分册 02:官网首页 curl 样例仍用 api.bochaai.com,两域关系官方未说明)。
|
||
BochaAPIKey string `env:"WEB_SEARCH_BOCHA_API_KEY"`
|
||
BochaBaseURL string `env:"WEB_SEARCH_BOCHA_BASE_URL" default:"https://api.bocha.cn"`
|
||
|
||
// Volcengine* 是火山引擎·豆包搜索(SearchInfinity;Bearer + open.feedcoopapi.com)。
|
||
VolcengineAPIKey string `env:"WEB_SEARCH_VOLCENGINE_API_KEY"`
|
||
VolcengineBaseURL string `env:"WEB_SEARCH_VOLCENGINE_BASE_URL" default:"https://open.feedcoopapi.com"`
|
||
|
||
// BaiduQianfan* 是百度·千帆 AI 搜索。
|
||
BaiduQianfanAPIKey string `env:"WEB_SEARCH_BAIDU_QIANFAN_API_KEY"`
|
||
BaiduQianfanBaseURL string `env:"WEB_SEARCH_BAIDU_QIANFAN_BASE_URL" default:"https://qianfan.baidubce.com"`
|
||
}
|
||
|
||
// ProviderDecision 是 WEB_SEARCH_PROVIDER 的解析结论。
|
||
type ProviderDecision struct {
|
||
// Selected 是选定的 provider 名(已去空白;大小写不敏感,比较时请再归一化)。
|
||
Selected string
|
||
// Disabled 为 true ⇒ 显式关闭(空串 / off / none)⇒ 不注册 web-search 工具。
|
||
Disabled bool
|
||
}
|
||
|
||
// ResolveProvider 把 WEB_SEARCH_PROVIDER 解析成选择结论:显式关闭(空串 / off / none,
|
||
// 大小写与首尾空白不敏感)⇒ Disabled=true(组合根据此**不注册** web-search 工具);
|
||
// 其余 ⇒ Selected=去空白后的名字(是否登记/是否配置齐由 io 工具集在启动期校验并报错)。
|
||
func (w WebSearchConfig) ResolveProvider() ProviderDecision {
|
||
raw := strings.TrimSpace(w.Provider)
|
||
if isOffSentinel(raw) {
|
||
return ProviderDecision{Disabled: true}
|
||
}
|
||
return ProviderDecision{Selected: raw}
|
||
}
|
||
|
||
// IsWebSearchOffSentinel 是 isOffSentinel 的导出形式,供**组合根**在把 `.env` 的
|
||
// web-search 段播种成面板可变源时复用同一套判定(PLG-4):播种要判「端点是不是显式关闭」,
|
||
// 若在别处重写一遍判定,两处迟早会漂移成不同的关闭语义。
|
||
func IsWebSearchOffSentinel(raw string) bool { return isOffSentinel(raw) }
|
||
|
||
// isOffSentinel 报告取值是否属于「显式关闭」的哨兵词(空串 / off / none,大小写与
|
||
// 首尾空白不敏感)。它同时服务 WEB_SEARCH_PROVIDER 与 WEB_SEARCH_ENDPOINT 两个键。
|
||
func isOffSentinel(raw string) bool {
|
||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||
case "", "off", "none":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
// EndpointDecision 是 WEB_SEARCH_ENDPOINT 的解析结论:要不要注册 web-search,
|
||
// 以及注册时它该指向哪里。它把「关闭」「非法」「可用」三种去处做成**显式**答案,
|
||
// 使组合根不必自己猜语义,也堵死「未配置却挂着报错」那种半开状态。
|
||
type EndpointDecision struct {
|
||
// Enabled 为 true ⇒ 注册 web-search 并把端点指向 URL。
|
||
Enabled bool
|
||
// URL 是校验通过的 http(s) 绝对地址(Enabled 为 true 时才有值)。
|
||
URL string
|
||
// Reason 在 Enabled 为 false 时说明原因(关闭 / 不是 http(s) 地址),
|
||
// 供组合根记一条带 env 键名的 WARN;Enabled 为 true 时为空。
|
||
Reason string
|
||
}
|
||
|
||
// Reason 文案常量:组合根直接把它们写进日志,避免两处各写一份措辞。
|
||
const (
|
||
// WebSearchReasonOff 表示端点被显式关闭(空串 / off / none)。
|
||
WebSearchReasonOff = "显式关闭(空串 / off / none)"
|
||
// WebSearchReasonInvalid 表示端点既不是关闭也不是可用的 http(s) 绝对地址。
|
||
WebSearchReasonInvalid = "不是可用的 http(s) 地址"
|
||
)
|
||
|
||
// ResolveEndpoint 把 WEB_SEARCH_ENDPOINT(= searxng provider 的基地址)解析成端点裁决:
|
||
//
|
||
// - 显式关闭(空串 / off / none,大小写与首尾空白不敏感)⇒ Enabled=false、
|
||
// Reason=WebSearchReasonOff —— 组合根据此**不注册**该工具;
|
||
// - 不是 http/https 绝对地址(含缺主机、写错 scheme,如 `ftp://x`、`127.0.0.1:38471`)
|
||
// ⇒ Enabled=false、Reason=WebSearchReasonInvalid —— 同样不注册,并记一条 WARN;
|
||
// 宁可让模型看不见这把锤子,也不给它一把抡起来必报错的锤子;
|
||
// - 其余 ⇒ Enabled=true、URL=去空白后的原值。
|
||
//
|
||
// 注意「未配置」不落到这里:没写 WEB_SEARCH_ENDPOINT 时字段已被标签默认值填好。
|
||
func (w WebSearchConfig) ResolveEndpoint() EndpointDecision {
|
||
raw := strings.TrimSpace(w.Endpoint)
|
||
if isOffSentinel(raw) {
|
||
return EndpointDecision{Reason: WebSearchReasonOff}
|
||
}
|
||
u, err := url.Parse(raw)
|
||
if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") {
|
||
return EndpointDecision{Reason: WebSearchReasonInvalid}
|
||
}
|
||
return EndpointDecision{Enabled: true, URL: raw}
|
||
}
|