Files
Enterprise-Knowledge-Base/internal/handler/web_search_provider.go
T
wizardchen a5ca9a2784 feat(web-search): refactor web search provider
- Updated relevant files to include provider registration, implementation, and metadata.
- Enhanced frontend components to support provider management and configuration.
- Added localization for new provider settings and messages.
- Implemented backend repository methods for CRUD operations on web search providers.
2026-03-31 20:22:34 +08:00

320 lines
10 KiB
Go

package handler
import (
"context"
"fmt"
"net/http"
"github.com/Tencent/WeKnora/internal/errors"
infra_web_search "github.com/Tencent/WeKnora/internal/infrastructure/web_search"
"github.com/Tencent/WeKnora/internal/logger"
"github.com/Tencent/WeKnora/internal/types"
"github.com/Tencent/WeKnora/internal/types/interfaces"
secutils "github.com/Tencent/WeKnora/internal/utils"
"github.com/gin-gonic/gin"
)
// WebSearchProviderHandler handles HTTP requests for web search provider CRUD
type WebSearchProviderHandler struct {
repo interfaces.WebSearchProviderRepository
service interfaces.WebSearchProviderService
registry *infra_web_search.Registry
}
// NewWebSearchProviderHandler creates a new handler
func NewWebSearchProviderHandler(
repo interfaces.WebSearchProviderRepository,
service interfaces.WebSearchProviderService,
registry *infra_web_search.Registry,
) *WebSearchProviderHandler {
return &WebSearchProviderHandler{repo: repo, service: service, registry: registry}
}
// --- request DTOs ---
// CreateProviderRequest defines the request body for creating a provider
type CreateProviderRequest struct {
Name string `json:"name" binding:"required"`
Provider types.WebSearchProviderType `json:"provider" binding:"required"`
Description string `json:"description"`
Parameters types.WebSearchProviderParameters `json:"parameters"`
IsDefault bool `json:"is_default"`
}
// UpdateProviderRequest defines the request body for updating a provider
type UpdateProviderRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters types.WebSearchProviderParameters `json:"parameters"`
IsDefault bool `json:"is_default"`
}
// --- helpers ---
// getTenantID extracts tenant ID from gin context (set by auth middleware).
func (h *WebSearchProviderHandler) getTenantID(c *gin.Context) uint64 {
return c.GetUint64(types.TenantIDContextKey.String())
}
// getOwnedProvider loads a provider and verifies it belongs to the given tenant.
// Returns (nil, status, msg) on failure so callers can respond immediately.
func (h *WebSearchProviderHandler) getOwnedProvider(
ctx context.Context, tenantID uint64, id string,
) (*types.WebSearchProviderEntity, int, string) {
provider, err := h.repo.GetByID(ctx, tenantID, id)
if err != nil {
return nil, http.StatusInternalServerError, "failed to query provider"
}
if provider == nil {
return nil, http.StatusNotFound, "web search provider not found"
}
return provider, http.StatusOK, ""
}
// --- endpoints ---
// CreateProvider creates a new web search provider
func (h *WebSearchProviderHandler) CreateProvider(c *gin.Context) {
ctx := c.Request.Context()
tenantID := h.getTenantID(c)
if tenantID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "unauthorized: tenant context missing"})
return
}
var req CreateProviderRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.Warnf(ctx, "Invalid create provider request: %v", err)
c.Error(errors.NewBadRequestError(err.Error()))
return
}
logger.Infof(ctx, "Creating web search provider: tenant=%d, name=%s, type=%s",
tenantID, secutils.SanitizeForLog(req.Name), secutils.SanitizeForLog(string(req.Provider)))
provider := &types.WebSearchProviderEntity{
TenantID: tenantID,
Name: secutils.SanitizeForLog(req.Name),
Provider: req.Provider,
Description: secutils.SanitizeForLog(req.Description),
Parameters: req.Parameters,
IsDefault: req.IsDefault,
}
if err := h.service.CreateProvider(ctx, provider); err != nil {
logger.Warnf(ctx, "Failed to create web search provider: %v", err)
c.Error(errors.NewInternalServerError(err.Error()))
return
}
c.JSON(http.StatusCreated, gin.H{
"success": true,
"data": provider,
})
}
// ListProviders lists all web search providers for the current tenant
func (h *WebSearchProviderHandler) ListProviders(c *gin.Context) {
ctx := c.Request.Context()
tenantID := h.getTenantID(c)
if tenantID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "unauthorized: tenant context missing"})
return
}
providers, err := h.repo.List(ctx, tenantID)
if err != nil {
logger.Warnf(ctx, "Failed to list web search providers: %v", err)
c.Error(errors.NewInternalServerError(err.Error()))
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": providers,
})
}
// GetProvider retrieves a single web search provider by ID
func (h *WebSearchProviderHandler) GetProvider(c *gin.Context) {
ctx := c.Request.Context()
tenantID := h.getTenantID(c)
if tenantID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "unauthorized: tenant context missing"})
return
}
id := c.Param("id")
provider, status, msg := h.getOwnedProvider(ctx, tenantID, id)
if status != http.StatusOK {
c.JSON(status, gin.H{"success": false, "error": msg})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": provider,
})
}
// UpdateProvider updates a web search provider
func (h *WebSearchProviderHandler) UpdateProvider(c *gin.Context) {
ctx := c.Request.Context()
tenantID := h.getTenantID(c)
if tenantID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "unauthorized: tenant context missing"})
return
}
id := c.Param("id")
// Ownership check
existing, status, msg := h.getOwnedProvider(ctx, tenantID, id)
if status != http.StatusOK {
c.JSON(status, gin.H{"success": false, "error": msg})
return
}
var req UpdateProviderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.Error(errors.NewBadRequestError(err.Error()))
return
}
// Build updated entity, keeping immutable fields from existing
provider := &types.WebSearchProviderEntity{
ID: id,
TenantID: tenantID,
Name: secutils.SanitizeForLog(req.Name),
Provider: existing.Provider, // Provider type is immutable after creation
Description: secutils.SanitizeForLog(req.Description),
Parameters: req.Parameters,
IsDefault: req.IsDefault,
}
if err := h.service.UpdateProvider(ctx, provider); err != nil {
logger.Warnf(ctx, "Failed to update web search provider %s: %v", id, err)
c.Error(errors.NewInternalServerError(err.Error()))
return
}
// Re-fetch to get the full stored state
updated, _ := h.repo.GetByID(ctx, tenantID, id)
if updated != nil {
c.JSON(http.StatusOK, gin.H{"success": true, "data": updated})
} else {
c.JSON(http.StatusOK, gin.H{"success": true})
}
}
// DeleteProvider deletes a web search provider
func (h *WebSearchProviderHandler) DeleteProvider(c *gin.Context) {
ctx := c.Request.Context()
tenantID := h.getTenantID(c)
if tenantID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "unauthorized: tenant context missing"})
return
}
id := c.Param("id")
// Ownership check
if _, status, msg := h.getOwnedProvider(ctx, tenantID, id); status != http.StatusOK {
c.JSON(status, gin.H{"success": false, "error": msg})
return
}
if err := h.service.DeleteProvider(ctx, tenantID, id); err != nil {
logger.Warnf(ctx, "Failed to delete web search provider %s: %v", id, err)
c.Error(errors.NewInternalServerError(err.Error()))
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
// ListProviderTypes returns available provider types and their parameter requirements
func (h *WebSearchProviderHandler) ListProviderTypes(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": types.GetWebSearchProviderTypes(),
})
}
// TestProviderByID tests an existing saved provider by performing a sample search
func (h *WebSearchProviderHandler) TestProviderByID(c *gin.Context) {
ctx := c.Request.Context()
tenantID := h.getTenantID(c)
if tenantID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "unauthorized: tenant context missing"})
return
}
id := c.Param("id")
provider, status, msg := h.getOwnedProvider(ctx, tenantID, id)
if status != http.StatusOK {
c.JSON(status, gin.H{"success": false, "error": msg})
return
}
if err := h.doTestSearch(ctx, string(provider.Provider), provider.Parameters); err != nil {
logger.Warnf(ctx, "Web search provider test failed: %v", err)
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
// TestProviderRequest defines the body for testing raw credentials
type TestProviderRequest struct {
Provider string `json:"provider" binding:"required"`
Parameters types.WebSearchProviderParameters `json:"parameters"`
}
// TestProviderRaw tests a provider with raw credentials (no persistence)
func (h *WebSearchProviderHandler) TestProviderRaw(c *gin.Context) {
ctx := c.Request.Context()
var req TestProviderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.Error(errors.NewBadRequestError(err.Error()))
return
}
if err := h.doTestSearch(ctx, req.Provider, req.Parameters); err != nil {
logger.Warnf(ctx, "Web search provider test failed: %v", err)
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
// doTestSearch creates a temporary provider and runs a simple test query
func (h *WebSearchProviderHandler) doTestSearch(ctx context.Context, providerType string, params types.WebSearchProviderParameters) error {
logger.Infof(ctx, "[WebSearch][Test] testing provider type=%s", providerType)
searchProvider, err := h.registry.CreateProvider(providerType, params)
if err != nil {
logger.Warnf(ctx, "[WebSearch][Test] failed to create provider: %v", err)
return fmt.Errorf("failed to create provider: %w", err)
}
results, err := searchProvider.Search(ctx, "test", 1, false)
if err != nil {
logger.Warnf(ctx, "[WebSearch][Test] search failed: %v", err)
return err
}
if len(results) == 0 {
logger.Warnf(ctx, "[WebSearch][Test] search returned 0 results — API key or configuration may be invalid")
return fmt.Errorf("search returned 0 results, please verify your API key and configuration")
}
logger.Infof(ctx, "[WebSearch][Test] succeeded: type=%s, results=%d", providerType, len(results))
return nil
}