Back to Weknora

添加新的网络搜索引擎

docs/添加新的网络搜索引擎.md

0.5.18.2 KB
Original Source

添加新的网络搜索引擎

本文档说明如何在 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.goregisterWebSearchProviders 函数:

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.goisValidProviderType 加新类型
internal/container/container.goregisterWebSearchProviders 注册