package websearch import ( "context" "strings" libbrave "haixun-backend/internal/library/brave" libexa "haixun-backend/internal/library/exa" ) type Provider string const ( ProviderBrave Provider = "brave" ProviderExa Provider = "exa" ) func ParseProvider(raw string) Provider { switch Provider(strings.ToLower(strings.TrimSpace(raw))) { case ProviderExa: return ProviderExa default: return ProviderBrave } } type Mode string const ( ModeKnowledgeExpand Mode = "knowledge_expand" ModeThreadsDiscover Mode = "threads_discover" ) type SearchResult struct { Title string Snippet string URL string } type SearchResponse struct { Results []SearchResult Query string Status string Provider Provider } type SearchOptions struct { Query string Limit int Mode Mode Country string SearchLang string UserLocation string StartPublishedDate string } type Client interface { Search(ctx context.Context, opts SearchOptions) (SearchResponse, error) Enabled() bool Provider() Provider } type Config struct { Provider Provider BraveKey string ExaKey string Country string SearchLang string UserLocation string } func New(cfg Config) Client { switch ParseProvider(string(cfg.Provider)) { case ProviderExa: return &exaAdapter{ client: libexa.NewClient(cfg.ExaKey), provider: ProviderExa, userLocation: cfg.UserLocation, } default: return &braveAdapter{ client: libbrave.NewClient(cfg.BraveKey), provider: ProviderBrave, country: cfg.Country, searchLang: cfg.SearchLang, } } } func ConfigFromMember(braveKey, exaKey, provider, country, searchLang, userLocation string) Config { return Config{ Provider: ParseProvider(provider), BraveKey: braveKey, ExaKey: exaKey, Country: country, SearchLang: searchLang, UserLocation: userLocation, } } type braveAdapter struct { client *libbrave.Client provider Provider country string searchLang string } func (a *braveAdapter) Provider() Provider { return a.provider } func (a *braveAdapter) Enabled() bool { return a != nil && a.client != nil && a.client.Enabled() } func (a *braveAdapter) Search(ctx context.Context, opts SearchOptions) (SearchResponse, error) { mode := libbrave.ModeKnowledgeExpand if opts.Mode == ModeThreadsDiscover { mode = libbrave.ModeThreadsDiscover } res, err := a.client.Search(ctx, libbrave.SearchOptions{ Query: opts.Query, Limit: opts.Limit, Mode: mode, Country: firstNonEmpty(opts.Country, a.country), SearchLang: firstNonEmpty(opts.SearchLang, a.searchLang), }) return toBraveResponse(res.Query, res.Status, a.provider, res.Results, err) } type exaAdapter struct { client *libexa.Client provider Provider userLocation string } func (a *exaAdapter) Provider() Provider { return a.provider } func (a *exaAdapter) Enabled() bool { return a != nil && a.client != nil && a.client.Enabled() } func (a *exaAdapter) Search(ctx context.Context, opts SearchOptions) (SearchResponse, error) { mode := libexa.ModeKnowledgeExpand if opts.Mode == ModeThreadsDiscover { mode = libexa.ModeThreadsDiscover } res, err := a.client.Search(ctx, libexa.SearchOptions{ Query: opts.Query, Limit: opts.Limit, Mode: mode, UserLocation: firstNonEmpty(opts.UserLocation, a.userLocation), StartPublishedDate: opts.StartPublishedDate, }) out := SearchResponse{Query: res.Query, Status: res.Status, Provider: a.provider} if err != nil { return out, err } for _, item := range res.Results { out.Results = append(out.Results, SearchResult{ Title: item.Title, Snippet: item.Snippet, URL: item.URL, }) } return out, nil } func toBraveResponse(query, status string, provider Provider, items []libbrave.SearchResult, err error) (SearchResponse, error) { out := SearchResponse{Query: query, Status: status, Provider: provider} if err != nil { return out, err } for _, item := range items { out.Results = append(out.Results, SearchResult{ Title: item.Title, Snippet: item.Snippet, URL: item.URL, }) } return out, nil } func firstNonEmpty(values ...string) string { for _, value := range values { if trimmed := strings.TrimSpace(value); trimmed != "" { return trimmed } } return "" } func ActiveAPIKey(cfg Config) string { if ParseProvider(string(cfg.Provider)) == ProviderExa { return strings.TrimSpace(cfg.ExaKey) } return strings.TrimSpace(cfg.BraveKey) } func ProviderLabel(provider Provider) string { switch provider { case ProviderExa: return "Exa" default: return "Brave" } }