package search import ( "context" "net/http" "sort" "strings" ) // Wire 是组合根给**一家** provider 的接线值(来自 env 配置)。 // // Configured 由组合根**显式**判定后给出(判定口径见 Configured 函数):false ⇒ // 这家不进注册表、不出现在可用清单里(design/27 §1.2 的 fail-closed)。刻意不用 // 「APIKey 是否为空」在这里自动推断,是为了让 searxng 这种「零 key、看 base URL」 // 的家也走同一条判定,且判定口径只有一处。 type Wire struct { // Name 是 provider 名(必须是本期内置的接入位之一,否则被忽略)。 Name string // APIKey 是上游凭据(searxng 不用)。 APIKey string // BaseURL 是上游基地址。 BaseURL string // Configured 报告这家在当前配置下是否已配置齐。 Configured bool } // Registry 是**已配置且已登记**的 provider 集合(值语义,由 Build 构造)。 // // 为什么用注入的 Registry 而不是包级 `Register(provider)` 全局注册表 // (design/27 §4 提到的写法):AGENTS §10 禁包级全局变量,且包级可变注册表会让 // 并行测试(t.Parallel)互相污染。这里把它做成一个由组合根构造、注入工具的值, // 能力等价(同样的「按名字选一家」+「可用清单」),但没有全局可变状态。 type Registry struct { providers map[string]Provider order []string } // Lookup 按名字取一家已登记的 provider(名字大小写与首尾空白不敏感)。 // 未配置/未登记的家返回 ok=false。 func (r *Registry) Lookup(name string) (Provider, bool) { if r == nil { return nil, false } p, ok := r.providers[strings.ToLower(strings.TrimSpace(name))] return p, ok } // Names 返回**已配置且已登记**的 provider 名(稳定顺序 = 接线表给出的顺序)。 // 它就是「可用清单」,未配置的家不会出现在这里。 func (r *Registry) Names() []string { if r == nil { return nil } out := make([]string, len(r.order)) copy(out, r.order) return out } // definition 是一家 provider 的**接入位**:名字 + 是否需要 key + 构造函数。 // 新增一家 = 新建一个文件(design/27 §4 一 provider 一文件)并在此表加一行。 type definition struct { name string needKey bool new func(Config) Provider } // definitions 返回本期内置的全部接入位(**含未配置的家**)。顺序即「可用清单」的展示顺序。 // // 它每次返回新建的切片(不是包级变量),保持包内无全局状态。 func definitions() []definition { return []definition{ {name: NameSearXNG, needKey: false, new: newSearXNG}, {name: NameZhipu, needKey: true, new: newZhipu}, {name: NameAliyunIQS, needKey: true, new: newAliyunIQS}, {name: NameBocha, needKey: true, new: newBocha}, {name: NameVolcengine, needKey: true, new: newVolcengine}, {name: NameBaiduQianfan, needKey: true, new: newBaiduQianfan}, } } // lookupDef 找一家接入位(名字去空白、大小写不敏感 —— 配置里写 `Zhipu` 也认)。 func lookupDef(name string) (definition, bool) { want := strings.ToLower(strings.TrimSpace(name)) for _, d := range definitions() { if d.name == want { return d, true } } return definition{}, false } // Known 报告 name 是否是本期**已登记的接入位**(含未配置的家)。 // // 它的用处是把两种失败分开报错:名字不认识(写错了 / 本期没做,如 tavily)⇒ // ErrUnknownProvider;名字认识但没配齐(缺 key)⇒ ErrNotConfigured。 // 两者都不许静默回落到兜底(design/27 §3 的语义硬约束)。 func Known(name string) bool { _, ok := lookupDef(name) return ok } // KnownNames 返回本期全部接入位名(含未配置的家),稳定顺序。 func KnownNames() []string { defs := definitions() out := make([]string, 0, len(defs)) for _, d := range defs { out = append(out, d.name) } sort.Strings(out) return out } // Configured 报告一家 provider 在给到的接线值下是否**配置齐**: // searxng 无 key、看 base URL;云家看 API key(design/27 §3:未配置 ⇒ 该家不出现)。 // 名字不是接入位时返回 false。 func Configured(name, apiKey, baseURL string) bool { def, ok := lookupDef(name) if !ok { return false } if def.needKey { return strings.TrimSpace(apiKey) != "" } return strings.TrimSpace(baseURL) != "" } // Build 按接线表构造注册表:**只登记 Configured 的家**,名字不是接入位的直接忽略。 // // guard 是组合根注入的 SSRF 守卫(可为 nil,夹具测试不判定);client / maxBytes 由 // 各家共用(同一份连接池与读取上限)。 // // 调用方要区分「未注册」与「未配置」时用 Known / Configured 自行判定后报错 // (注册表本身只承载「能用哪些」这一个事实)。 func Build(wires []Wire, client *http.Client, maxBytes int64, guard Guard) *Registry { reg := &Registry{providers: make(map[string]Provider, len(wires))} for _, w := range wires { if !w.Configured { continue } def, ok := lookupDef(w.Name) if !ok { continue } cfg := Config{APIKey: w.APIKey, BaseURL: w.BaseURL, Client: client, MaxBytes: maxBytes, Guard: guard} p := def.new(cfg) if p == nil { continue } if _, dup := reg.providers[def.name]; dup { continue } reg.providers[def.name] = p reg.order = append(reg.order, def.name) } return reg } // Guard 是发请求前的地址判定(组合根注入的 SSRF 守卫;见 Config.Guard)。 type Guard func(ctx context.Context, rawURL string) error