package config import ( "net/url" "strings" "time" ) // WebSearchConfig 是 web-search 工具的搜索端点配置(design/09-插件生态;切片 WS1-A)。 // 写法照既有 VoiceConfig 惯例:一组 provider 键 + 未配置则如实降级,绝不硬编码。 type WebSearchConfig struct { // Endpoint 是 SearXNG 兼容搜索端点(支持 `format=json` 的 GET 接口)。 // // 默认值指向**本机兜底**:`scripts/searxng/settings.yml` 配套的 SearXNG 实例 // (宿主机 http://127.0.0.1:38471)。默认指向本机而不是某个云搜索 API,是因为 // 兜底实例不需要任何第三方 key、开箱即用;要换成云 provider 请在本键里写新地址。 // // **显式关闭**(值写成空串 / 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"` } // 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 把配置值解析成端点裁决: // // - 显式关闭(空串 / 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) // 显式关闭的三个哨兵词(大小写不敏感):空串、off、none。 switch strings.ToLower(raw) { case "", "off", "none": 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} }