WeKnora 网络搜索引擎扩展开发指南:以 Brave Search 为例新增一个 WebSearch Provider
【免费下载链接】WeKnoraOpen-source LLM knowledge platform: turn raw documents into a queryable RAG, an autonomous reasoning agent, and a self-maintaining Wiki.项目地址: https://gitcode.com/GitHub_Trending/we/WeKnora
本文档是一份面向开发者的 WeKnora 搜索引擎扩展实战指南,讲解如何为 WeKnora 平台新增一个网络搜索引擎类型(Provider),并以Brave Search的完整接入为例,覆盖类型常量注册、元数据声明、Provider 实现、参数校验、依赖注入(DI)注册与 API 验证全流程。读完本文,你将掌握 WeKnora 网络搜索扩展的标准开发模式,能够独立接入新的商业搜索 API 或自托管搜索实例,并理解其背后的硬编码端点与 SSRF 防护设计。
一、架构概述:WebSearch Provider 在 WeKnora 中的位置
WeKnora 的网络搜索能力由一组可插拔的WebSearch Provider实现,负责把统一格式的搜索请求转发给不同的搜索引擎(商业 API、元搜索引擎、自托管实例等),并将异构响应归一化为统一的WebSearchResult结构。整个机制分布在四个层次:
internal/types/web_search_provider.go # 实体定义 + Provider 类型元数据 internal/infrastructure/web_search/ # Provider 实现(brave/bing/google/duckduckgo/tavily 等) internal/container/container.go # DI 注册(registerWebSearchProviders) internal/types/interfaces/web_search.go # WebSearchProvider 接口从源码结构看,当前仓库已内置 13 种 Provider:brave、bing、google、duckduckgo、tavily、ollama、baidu、searxng、keenable、zhipu、exa、metaso、bocha,常量定义在 internal/types/web_search_provider.go。新增一种引擎,本质就是在这四层中各补一段代码。
硬编码端点与 SSRF 防护
本扩展机制最核心的安全设计是:商业搜索引擎的 API 端点硬编码在 Provider 实现中,不向用户暴露 BaseURL。也就是说,用户在配置界面只能填写 API Key 等凭据,无法指定请求发往哪个地址,从源头杜绝了通过自定义 BaseURL 发起 SSRF 攻击的可能。这一点在 brave.go 中体现为包级常量:
const braveSearchURL = "https://api.search.brave.com/res/v1/web/search"该设计也是「硬编码端点 + SSRF 白名单校验」双层防护的一部分:对于允许用户填写 URL 的场景(如自托管 SearXNG 的BaseURL、可选ProxyURL),代码会统一调用utils.ValidateURLForSSRF做校验(见 internal/utils/security.go),私有地址需加入SSRF_WHITELIST环境变量才能放行。
类似的扩展开发模式可参考 集成向量数据库,两者遵循「接口实现 + 注册 + DI」的同一套范式。
二、核心接口与数据类型
在动手编码前,先理解三个关键类型。
1. Provider 接口
所有 Provider 必须实现 WebSearchProvider 接口:
type WebSearchProvider interface { // Name returns the name of the provider Name() string // Search performs a web search Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) }此外还存在一个可选接口FilteredWebSearchProvider(同文件 L19-L22),用于显式支持按调用传参的「地区 / 时效」过滤(SearchWithFilters)。不具备该能力的 Provider 不得静默丢弃调用方传入的过滤条件——这是接口注释里明确约定的行为准则。
2. Provider 类型常量
每个引擎对应一个WebSearchProviderType字符串常量,作为其在数据库、注册表、API 中的唯一标识,例如WebSearchProviderTypeBing WebSearchProviderType = "bing"。
3. 实体与参数
配置实例落库为WebSearchProviderEntity,对应表web_search_providers,按工作空间(TenantID)隔离,一个工作空间可创建多个同名类型的实例(如「生产 Bing」「测试 Google」),Agent 通过 ID 引用它们。其核心字段包括:
| 字段 | 说明 |
|---|---|
ID | UUID 主键,由BeforeCreateGORM 钩子自动生成 |
TenantID | 工作空间 ID,用于隔离 |
Name | 用户可读名称 |
Provider | Provider 类型标识(如brave) |
Parameters | Provider 特有参数(JSON 存储) |
IsDefault | 是否为该工作空间默认 Provider |
参数结构WebSearchProviderParameters(internal/types/web_search_provider.go)是扩展时最常打交道的类型:
type WebSearchProviderParameters struct { APIKey string `yaml:"api_key" json:"api_key,omitempty"` // 搜索引擎 API Key(加密存储) EngineID string `yaml:"engine_id" json:"engine_id,omitempty"` // Google CSE 专用 BaseURL string `yaml:"base_url" json:"base_url,omitempty"` // 自托管引擎地址(如 SearXNG) ProxyURL string `yaml:"proxy_url" json:"proxy_url,omitempty"` // 可选出站代理 ExtraConfig map[string]string `yaml:"extra_config" json:"extra_config,omitempty"` // 预留扩展配置 }注意两个安全细节:APIKey落库时经AES-GCM 加密(Value()/Scan()方法,密钥来自SYSTEM_AES_KEY),且凭据变更走独立的/credentials子资源,响应序列化时会按构造方式直接省略api_key字段,避免密钥泄露。
4. 类型元数据:驱动前端动态渲染
GetWebSearchProviderTypes()返回WebSearchProviderTypeInfo元数据数组,供GET /types接口下发,前端据此动态渲染配置表单。新增引擎时务必声明完整:
| 元数据字段 | 含义 |
|---|---|
ID | 类型标识 |
Name | 展示名称 |
RequiresAPIKey | 是否必须 API Key |
SupportsOptionalAPIKey | 是否可选 Key(如 Keenable,无 Key 也能用、有 Key 提限额),与前者互斥 |
RequiresEngineID | 是否需要引擎 ID(Google CSE) |
RequiresBaseURL | 是否需要用户提供地址(自托管 SearXNG) |
SupportsProxy | 是否支持proxy_url |
Description | 一句话描述 |
DocsURL | 官方凭据获取文档地址 |
ConfigFields | 非密钥的动态表单字段(存入ExtraConfig) |
三、分步实战:接入 Brave Search
以下以 Brave Search 为例,完整走一遍「注册类型 → 元数据 → 实现 → 校验 → DI → 验证」六步。
第 1 步:注册类型常量
在 internal/types/web_search_provider.go 的const块中追加:
WebSearchProviderTypeBrave WebSearchProviderType = "brave"第 2 步:添加类型元数据
在GetWebSearchProviderTypes()的返回数组中追加(Free字段在现有实现中未使用,注意遵循RequiresAPIKey等既有字段的取值约定):
{ ID: "brave", Name: "Brave Search", RequiresAPIKey: true, SupportsProxy: true, Description: "Brave Search API (supports country and freshness filters)", DocsURL: "填写 Brave 官方 API Key 获取页面地址", },仓库中的 Brave 元数据还特意在描述中标注了其支持country(国家)与freshness(时效)过滤的能力,这与其实现SearchWithFilters相呼应(见 internal/types/web_search_provider.go)。
第 3 步:创建 Provider 实现
新建internal/infrastructure/web_search/brave.go,遵循三点约定:
- 构造函数签名:
func(types.WebSearchProviderParameters) (interfaces.WebSearchProvider, error),构造时校验必要参数并返回错误; - API 端点硬编码为常量,不读取任何用户输入;
- 实现
Name()与Search()(以及可选的SearchWithFilters)。
仓库中 brave.go 的完整实现值得逐段研读,它是「商业 API」型 Provider 的参考范本:
func NewBraveProvider(params types.WebSearchProviderParameters) (interfaces.WebSearchProvider, error) { if strings.TrimSpace(params.APIKey) == "" { return nil, fmt.Errorf("API key is required for Brave provider") } client, err := NewSearchHTTPClient(30*time.Second, params.ProxyURL) if err != nil { return nil, err } // 官方端点不需要重定向:绝不把订阅令牌转发给重定向目标 client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } return &BraveProvider{client: client, apiKey: strings.TrimSpace(params.APIKey)}, nil }Search()的实现体现了统一的结果归一化逻辑(brave.go):
maxResults默认 5、上限 20(maxResults = min(maxResults, 20));- 支持
country、freshness参数透传(需先通过filters.Validate()); - 通过
X-Subscription-Token请求头携带 API Key,超时 30 秒; - 响应体限制为 4 MiB(
maxResponse = 4 << 20),防止超大响应拖垮服务; - 解析
web.results数组,把title/url/description/age映射为统一的WebSearchResult(Title/URL/Snippet/Source/Age)。
Name()返回"brave",与类型常量保持一致:
func (p *BraveProvider) Name() string { return "brave" }第 4 步:添加参数校验
在 internal/application/service/web_search_provider.go 的isValidProviderType()switch 中追加新类型,保证「保存配置」时类型合法:
case types.WebSearchProviderTypeBrave: return true同时,若新引擎有必填参数,还需在validateProviderParameters()(同文件 L152-L210)中补充校验分支。例如 Brave 的分支为:
case types.WebSearchProviderTypeBrave: if strings.TrimSpace(params.APIKey) == "" { return fmt.Errorf("API key is required for Brave provider") }函数末尾还会统一调用validateOptionalProxyURL校验可选的ProxyURL。这两处校验是「保存」与「使用」行为一致性的保证。
第 5 步:DI 注册
在 internal/container/container.go 的registerWebSearchProviders中追加一行工厂注册:
registry.Register("brave", infra_web_search.NewBraveProvider)注册表Registry(registry.go)内部以map[string]ProviderFactory保存「类型 ID → 工厂函数」,CreateProvider在每次搜索调用时按类型与租户参数动态创建实例——也就是说 Provider 是无状态的,凭据更新无需缓存失效。
第 6 步:构建与验证
go build ./... # 通过 API 验证类型已下发 curl http://localhost:8080/api/v1/web-search-providers/types该接口由 internal/handler/web_search_provider.go 的ListProviderTypes处理(@Router /web-search-providers/types [get]),具体前缀以项目实际路由挂载为准。同文件还提供了其余管理端点:
| 方法 | 路径 | 用途 |
|---|---|---|
| GET | /web-search-providers/types | 列出全部类型元数据 |
| POST | /web-search-providers | 创建 Provider 配置 |
| GET/PUT/DELETE | /web-search-providers/{id} | 读取 / 更新 / 删除 |
| POST | /web-search-providers/{id}/test | 按 ID 测试连通性 |
| POST | /web-search-providers/test | 原始参数测试连通性 |
仓库为 Brave 提供了完整的单元测试 brave_test.go,涵盖 Key 缺失、空查询、非 2xx 状态码等错误分支,新增引擎时建议仿照补齐。
四、现有 Provider 的参数要求速查
validateProviderParameters()完整映射了各引擎的必填参数约束,新引擎的参数设计可直接对照下表:
| Provider | 必填参数 | 备注 |
|---|---|---|
brave/bing/tavily/ollama/baidu/exa/metaso/bocha/zhipu | APIKey | 其中 metaso / bocha / zhipu 走各自的专用校验函数(含ExtraConfig字段校验) |
google | APIKey+EngineID | Google Custom Search 需要两者 |
duckduckgo | 无 | 免 Key |
keenable | 无 | 默认免 Key,可选 Key 提限额 |
searxng | BaseURL | 走ValidateSearxngBaseURL,必须通过 SSRF 校验 |
| 全部 | ProxyURL(可选) | 统一ValidateProxyURL校验 |
五、需要额外参数:ExtraConfig 与动态表单
若新引擎除 API Key 外还需要其他配置,有两种方式:
方式一(推荐):使用
WebSearchProviderParameters.ExtraConfig字段,并在类型元数据的ConfigFields中声明动态表单字段。前端会依据WebSearchProviderConfigField渲染出下拉框/输入框,值落库于ExtraConfig。仓库中已有成熟先例:zhipu:search_engine(标准/Pro/搜狗/夸克四档计价)、content_size(medium/high);metaso:scope(网页/文档/学术/播客/视频/图片);bocha:freshness(时效范围)、summary(是否请求摘要);exa:include_text(是否在统一结果中包含正文)。
字段结构定义见 internal/types/web_search_provider.go,支持
Key/Label/Type/Required/Default/Description/Options等属性。方式二:直接在
WebSearchProviderParameters中添加专用强类型字段(如EngineID之于 Google)。适合字段被 Provider 逻辑高频读取、需要类型安全的场景。
两种方式选型的判断标准:仅影响单个引擎的非敏感配置优先用ExtraConfig+ 动态表单,保持核心结构稳定;被多个组件读取的通用字段才考虑扩展专用字段。
六、特殊场景:自托管引擎与 BaseURL(以 SearXNG 为例)
与商业 API 的「硬编码端点」不同,自托管引擎(如 SearXNG)必须允许用户填写实例地址,因此走RequiresBaseURL+BaseURL校验路径,是理解 SSRF 防护落地的典型案例。其构造函数NewSearxngProvider会依次校验(searxng.go):
BaseURL非空,必须是绝对 http(s) URL;- 不允许携带 query 或 fragment;
- 通过
utils.ValidateURLForSSRF——私有 / 回环地址必须加入SSRF_WHITELIST环境变量才能放行。
请求侧还有三个值得注意的约定:
- 超时设为12 秒,略高于仓库内 SearXNG 镜像
docker/searxng/settings.yml默认的outgoing.max_request_timeout(10 秒),避免慢上游被客户端先行取消; - 搜索请求要求实例开启JSON 输出(
search.formats: [json]),否则响应解码失败并给出明确报错; language参数固定传all(SearXNG 文档中「不设语言过滤」的合法值),safesearch不传,尊重实例侧配置。
SearXNG 返回的publishedDate会按 6 种时间格式依次尝试解析(RFC3339、2006-01-02T15:04:05、2006-01-02、RFC1123 等),成功则回填PublishedAt;空结果时还会读取unresponsive_engines字段输出诊断信息,辅助排查上游引擎不可达问题。
七、文件变更清单
新增一个 Provider 需要改动的文件汇总如下(含本仓库为 Brave 实际引入的内容):
| 文件 | 操作 |
|---|---|
| internal/types/web_search_provider.go | 添加类型常量 +GetWebSearchProviderTypes元数据 |
internal/infrastructure/web_search/brave.go | 新建Provider 实现(构造函数 +Name()+Search()) |
| internal/application/service/web_search_provider.go | isValidProviderType加新类型、validateProviderParameters加参数校验 |
| internal/container/container.go | registerWebSearchProviders注册工厂 |
internal/infrastructure/web_search/brave_test.go | 新建(可选但强烈建议)单元测试 |
如果新引擎需要ExtraConfig动态表单,再补元数据中的ConfigFields声明即可,无需改动实体结构。
八、开发后的自检清单
收尾时逐项确认:
- 构建:
go build ./...通过; - 类型下发:
GET /web-search-providers/types能查到新类型及其元数据(RequiresAPIKey、ConfigFields等); - 参数校验:缺失必填参数时,创建/更新配置应返回明确错误;
- 搜索可用:配置真实凭据后调用测试接口返回正常结果;
- 安全基线:商业 API 端点必须硬编码;允许用户填 URL 的字段必须过 SSRF 校验;响应大小、超时、重定向策略有合理限制。
相关主题
- 集成向量数据库 — 同类扩展开发模式(接口实现 + 注册 + DI)
- MCP功能使用说明 — MCP 也可集成搜索工具,与网络搜索引擎互补
- 常见问题 — SSRF 白名单配置与排障
- 版本路线图 — 路线图中的社区组件扩展方向
- Home — Wiki 首页导航
【免费下载链接】WeKnoraOpen-source LLM knowledge platform: turn raw documents into a queryable RAG, an autonomous reasoning agent, and a self-maintaining Wiki.项目地址: https://gitcode.com/GitHub_Trending/we/WeKnora
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考