Files
Cyrene_v2/internal/config/websearch.go
T
AskaEth 8e6c77ad10
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(search): web-search 接线本机 SearXNG 兜底 + 显式开关(她不再拿一把用不了的锤子)
主人报的故障:她调 web-search 必失败(no search endpoint configured)。根因两条:组合根从未注入 WithSearchEndpoint + 配置层根本没有搜索配置项。
- internal/config/websearch.go:WEB_SEARCH_ENDPOINT(默认本机兜底 http://127.0.0.1:38471)+ WEB_SEARCH_TIMEOUT;ResolveEndpoint() 三态裁决(可用/显式关闭/非法),非法值启动即报错
- cmd/cyrene/wire_io_tools.go:组合根唯一的 io 选项装配点;端点不可用 ⇒ WithoutSearch() + 带 source 的 WARN(只记键名与原因,不打印 URL 值)
- plugins/tools/io:新增 WithoutSearch()(**根本不进注册面** ⇒ 模型看不见)+ WithSearchTimeout();协议解析逻辑一字未动
- 隐蔽坑(额外查出并修):SearXNG 对 /?q= 回 308 → /search,重定向那一跳走**共享 SSRF 白名单** ⇒ 只注入端点照样被拦。改为**只给 web-search 一件**加宽自己的客户端与白名单,共享白名单与 http/web-fetch 客户端一字未动(有测试钉住它仍拒绝本机网关地址)
- .env.example 新增 2 键 + 注释说明「空串/off/none = 不注册」
- 真机 e2e 两次绿(SearXNG 返回 9 条);变异:不注入端点 / 关闭态仍注册 / 默认值改错 / 不并入端点主机 / 重定向用共享白名单 ⇒ 5 条全红
2026-10-05 20:29:21 +08:00

73 lines
3.7 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 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}
}