# 添加新的网络搜索引擎 本文档说明如何在 WeKnora 中添加一个新的网络搜索引擎类型(如 Brave Search、Searx 等)。 ## 架构概述 ``` internal/ ├── types/ │ └── web_search_provider.go # 实体定义 + Provider 类型元数据 ├── infrastructure/ │ └── web_search/ │ ├── registry.go # Provider 工厂注册表 │ ├── bing.go # Bing 实现 │ ├── google.go # Google 实现 │ ├── duckduckgo.go # DuckDuckGo 实现 │ └── tavily.go # Tavily 实现 ├── container/ │ └── container.go # DI 注册(registerWebSearchProviders) └── types/interfaces/ └── web_search.go # WebSearchProvider 接口 ``` 搜索引擎的 API 端点**硬编码**在代码中,不向用户暴露 BaseURL,从源头消除 SSRF 风险。 ## 步骤 以添加 **Brave Search** 为例。 ### 1. 在 `types/web_search_provider.go` 中注册类型常量 ```go const ( WebSearchProviderTypeBing WebSearchProviderType = "bing" WebSearchProviderTypeGoogle WebSearchProviderType = "google" WebSearchProviderTypeDuckDuckGo WebSearchProviderType = "duckduckgo" WebSearchProviderTypeTavily WebSearchProviderType = "tavily" WebSearchProviderTypeBrave WebSearchProviderType = "brave" // ← 新增 ) ``` ### 2. 在 `GetWebSearchProviderTypes()` 中添加类型元数据 ```go { ID: "brave", Name: "Brave Search", Free: false, RequiresAPIKey: true, Description: "Brave Search API", DocsURL: "https://brave.com/search/api/", }, ``` 字段说明: | 字段 | 说明 | | ---------------- | ---------------------------------------------- | | `ID` | 唯一标识,存入数据库,不可更改 | | `Name` | 前端展示名称 | | `Free` | 是否免费 | | `RequiresAPIKey` | 是否需要 API Key | | `RequiresEngineID` | 是否需要额外 ID(如 Google CSE) | | `Description` | 简短描述 | | `DocsURL` | 官方文档链接,前端在添加对话框中显示 | ### 3. 创建 Provider 实现 新建 `internal/infrastructure/web_search/brave.go`: ```go package web_search import ( "context" "encoding/json" "fmt" "io" "net/http" "time" "github.com/Tencent/WeKnora/internal/types" "github.com/Tencent/WeKnora/internal/types/interfaces" ) const defaultBraveSearchURL = "https://api.search.brave.com/res/v1/web/search" type BraveProvider struct { client *http.Client apiKey string } // NewBraveProvider 从参数创建实例(不读环境变量) func NewBraveProvider(params types.WebSearchProviderParameters) (interfaces.WebSearchProvider, error) { if params.APIKey == "" { return nil, fmt.Errorf("API key is required for Brave provider") } return &BraveProvider{ client: &http.Client{Timeout: 10 * time.Second}, apiKey: params.APIKey, }, nil } func BraveProviderTypeInfo() types.WebSearchProviderTypeInfo { return types.WebSearchProviderTypeInfo{ ID: "brave", Name: "Brave Search", Free: false, RequiresAPIKey: true, Description: "Brave Search API", DocsURL: "https://brave.com/search/api/", } } func (p *BraveProvider) Name() string { return "brave" } func (p *BraveProvider) Search( ctx context.Context, query string, maxResults int, includeDate bool, ) ([]*types.WebSearchResult, error) { // 构造请求 — BaseURL 硬编码 req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s?q=%s&count=%d", defaultBraveSearchURL, query, maxResults), nil) if err != nil { return nil, err } req.Header.Set("X-Subscription-Token", p.apiKey) req.Header.Set("Accept", "application/json") resp, err := p.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() // 解析响应 body, _ := io.ReadAll(resp.Body) var data braveResponse if err := json.Unmarshal(body, &data); err != nil { return nil, err } results := make([]*types.WebSearchResult, 0, len(data.Web.Results)) for _, r := range data.Web.Results { results = append(results, &types.WebSearchResult{ Title: r.Title, URL: r.URL, Snippet: r.Description, Source: "brave", }) } return results, nil } type braveResponse struct { Web struct { Results []struct { Title string `json:"title"` URL string `json:"url"` Description string `json:"description"` } `json:"results"` } `json:"web"` } ``` **关键要求**: 1. **构造函数签名**必须为 `func(types.WebSearchProviderParameters) (interfaces.WebSearchProvider, error)` 2. **API 端点硬编码**为常量,不从参数中读取 3. **实现 `interfaces.WebSearchProvider` 接口**:`Name()` 和 `Search()` ### 4. 在 Service 的参数校验中添加新类型 编辑 `internal/application/service/web_search_provider.go`: ```go func isValidProviderType(provider types.WebSearchProviderType) bool { switch provider { case types.WebSearchProviderTypeBing, types.WebSearchProviderTypeGoogle, types.WebSearchProviderTypeDuckDuckGo, types.WebSearchProviderTypeTavily, types.WebSearchProviderTypeBrave: // ← 新增 return true default: return false } } ``` ### 5. 在 DI 容器中注册 编辑 `internal/container/container.go` 的 `registerWebSearchProviders` 函数: ```go func registerWebSearchProviders(registry *infra_web_search.Registry) { // ... 已有注册 ... // Register Brave provider type registry.Register(infra_web_search.BraveProviderTypeInfo(), infra_web_search.NewBraveProvider) } ``` ### 6. 验证 ```bash # 编译 go build ./... # 启动后调用 API 验证类型列表 curl http://localhost:8080/api/v1/web-search-providers/types \ -H 'X-API-Key: your_key' # 创建 Brave 搜索引擎实例 curl -X POST http://localhost:8080/api/v1/web-search-providers \ -H 'X-API-Key: your_key' \ -H 'Content-Type: application/json' \ -d '{ "name": "Brave Search", "provider": "brave", "parameters": { "api_key": "BSA..." }, "is_default": true }' ``` ## 需要额外参数的情况 如果新引擎需要 API Key 以外的参数(类似 Google 的 `engine_id`),有两种方式: ### 方式一:使用 `ExtraConfig` 利用 `WebSearchProviderParameters.ExtraConfig` 字段,不需要改类型定义: ```go func NewFooProvider(params types.WebSearchProviderParameters) (interfaces.WebSearchProvider, error) { region := params.ExtraConfig["region"] if region == "" { region = "us" } // ... } ``` 前端在 `GetWebSearchProviderTypes()` 中可以标注需要哪些 extra 字段(后续支持动态表单渲染)。 ### 方式二:添加专用字段 如果参数非常通用(比如多个引擎都需要),可以在 `WebSearchProviderParameters` 中添加新字段: ```go type WebSearchProviderParameters struct { APIKey string `json:"api_key,omitempty"` EngineID string `json:"engine_id,omitempty"` Region string `json:"region,omitempty"` // ← 新增 ExtraConfig map[string]string `json:"extra_config,omitempty"` } ``` 同时在 `WebSearchProviderTypeInfo` 中添加 `RequiresRegion bool` 等字段,前端根据此动态显示输入框。 ## 文件变更清单 | 文件 | 操作 | | ---- | ---- | | `internal/types/web_search_provider.go` | 添加常量 + 类型元数据 | | `internal/infrastructure/web_search/brave.go` | **新建** Provider 实现 | | `internal/application/service/web_search_provider.go` | `isValidProviderType` 加新类型 | | `internal/container/container.go` | `registerWebSearchProviders` 注册 |