WeKnora 网络搜索引擎扩展开发指南:以 Brave Search 为例新增一个 WebSearch Provider
2026/9/13 11:33:06 网站建设 项目流程

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:bravebinggoogleduckduckgotavilyollamabaidusearxngkeenablezhipuexametasobocha,常量定义在 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 引用它们。其核心字段包括:

字段说明
IDUUID 主键,由BeforeCreateGORM 钩子自动生成
TenantID工作空间 ID,用于隔离
Name用户可读名称
ProviderProvider 类型标识(如brave
ParametersProvider 特有参数(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));
  • 支持countryfreshness参数透传(需先通过filters.Validate());
  • 通过X-Subscription-Token请求头携带 API Key,超时 30 秒;
  • 响应体限制为 4 MiB(maxResponse = 4 << 20),防止超大响应拖垮服务;
  • 解析web.results数组,把title/url/description/age映射为统一的WebSearchResultTitle/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/zhipuAPIKey其中 metaso / bocha / zhipu 走各自的专用校验函数(含ExtraConfig字段校验)
googleAPIKey+EngineIDGoogle Custom Search 需要两者
duckduckgo免 Key
keenable默认免 Key,可选 Key 提限额
searxngBaseURLValidateSearxngBaseURL,必须通过 SSRF 校验
全部ProxyURL(可选)统一ValidateProxyURL校验

五、需要额外参数:ExtraConfig 与动态表单

若新引擎除 API Key 外还需要其他配置,有两种方式:

  • 方式一(推荐):使用WebSearchProviderParameters.ExtraConfig字段,并在类型元数据的ConfigFields中声明动态表单字段。前端会依据WebSearchProviderConfigField渲染出下拉框/输入框,值落库于ExtraConfig。仓库中已有成熟先例:

    • zhipusearch_engine(标准/Pro/搜狗/夸克四档计价)、content_size(medium/high);
    • metasoscope(网页/文档/学术/播客/视频/图片);
    • bochafreshness(时效范围)、summary(是否请求摘要);
    • exainclude_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):

  1. BaseURL非空,必须是绝对 http(s) URL
  2. 不允许携带 query 或 fragment;
  3. 通过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:052006-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.goisValidProviderType加新类型、validateProviderParameters加参数校验
internal/container/container.goregisterWebSearchProviders注册工厂
internal/infrastructure/web_search/brave_test.go新建(可选但强烈建议)单元测试

如果新引擎需要ExtraConfig动态表单,再补元数据中的ConfigFields声明即可,无需改动实体结构。

八、开发后的自检清单

收尾时逐项确认:

  1. 构建go build ./...通过;
  2. 类型下发GET /web-search-providers/types能查到新类型及其元数据(RequiresAPIKeyConfigFields等);
  3. 参数校验:缺失必填参数时,创建/更新配置应返回明确错误;
  4. 搜索可用:配置真实凭据后调用测试接口返回正常结果;
  5. 安全基线:商业 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询