docs/添加新的网络搜索引擎.md
本文档说明如何在 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 为例。
types/web_search_provider.go 中注册类型常量const (
WebSearchProviderTypeBing WebSearchProviderType = "bing"
WebSearchProviderTypeGoogle WebSearchProviderType = "google"
WebSearchProviderTypeDuckDuckGo WebSearchProviderType = "duckduckgo"
WebSearchProviderTypeTavily WebSearchProviderType = "tavily"
WebSearchProviderTypeBrave WebSearchProviderType = "brave" // ← 新增
)
GetWebSearchProviderTypes() 中添加类型元数据{
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 | 官方文档链接,前端在添加对话框中显示 |
新建 internal/infrastructure/web_search/brave.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"`
}
关键要求:
func(types.WebSearchProviderParameters) (interfaces.WebSearchProvider, error)interfaces.WebSearchProvider 接口:Name() 和 Search()编辑 internal/application/service/web_search_provider.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
}
}
编辑 internal/container/container.go 的 registerWebSearchProviders 函数:
func registerWebSearchProviders(registry *infra_web_search.Registry) {
// ... 已有注册 ...
// Register Brave provider type
registry.Register(infra_web_search.BraveProviderTypeInfo(), infra_web_search.NewBraveProvider)
}
# 编译
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 字段,不需要改类型定义:
func NewFooProvider(params types.WebSearchProviderParameters) (interfaces.WebSearchProvider, error) {
region := params.ExtraConfig["region"]
if region == "" {
region = "us"
}
// ...
}
前端在 GetWebSearchProviderTypes() 中可以标注需要哪些 extra 字段(后续支持动态表单渲染)。
如果参数非常通用(比如多个引擎都需要),可以在 WebSearchProviderParameters 中添加新字段:
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 注册 |