mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat(search): add 8 new search providers (#135)
* feat(search): add Sogou search provider * fix(search): use new endpoint and API version for sogou * feat(search): add Serper, SearXNG, Jina, Exa, Bocha, DuckDuckGo search providers Add six new search provider integrations: - Serper: Google search via Serper API - SearXNG: Self-hosted meta search engine - Jina: Jina AI search API - Exa: Exa neural search API - Bocha: Bocha AI web search - DuckDuckGo: DuckDuckGo HTML search (no API key required) Each provider includes backend implementation, config schema, i18n entries, and Vue settings component. * feat(search): add Yandex search provider Add Yandex search provider with XML response parsing and configurable search type (RU/TR/COM). --------- Co-authored-by: Menci <mencici@msn.com>
This commit is contained in:
@@ -3,12 +3,20 @@ package web
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -111,6 +119,22 @@ func (p *Executor) callWebSearch(ctx context.Context, providerName string, confi
|
|||||||
return p.callGoogleSearch(ctx, configJSON, query, count)
|
return p.callGoogleSearch(ctx, configJSON, query, count)
|
||||||
case string(searchproviders.ProviderTavily):
|
case string(searchproviders.ProviderTavily):
|
||||||
return p.callTavilySearch(ctx, configJSON, query, count)
|
return p.callTavilySearch(ctx, configJSON, query, count)
|
||||||
|
case string(searchproviders.ProviderSogou):
|
||||||
|
return p.callSogouSearch(ctx, configJSON, query, count)
|
||||||
|
case string(searchproviders.ProviderSerper):
|
||||||
|
return p.callSerperSearch(ctx, configJSON, query, count)
|
||||||
|
case string(searchproviders.ProviderSearXNG):
|
||||||
|
return p.callSearXNGSearch(ctx, configJSON, query, count)
|
||||||
|
case string(searchproviders.ProviderJina):
|
||||||
|
return p.callJinaSearch(ctx, configJSON, query, count)
|
||||||
|
case string(searchproviders.ProviderExa):
|
||||||
|
return p.callExaSearch(ctx, configJSON, query, count)
|
||||||
|
case string(searchproviders.ProviderBocha):
|
||||||
|
return p.callBochaSearch(ctx, configJSON, query, count)
|
||||||
|
case string(searchproviders.ProviderDuckDuckGo):
|
||||||
|
return p.callDuckDuckGoSearch(ctx, configJSON, query, count)
|
||||||
|
case string(searchproviders.ProviderYandex):
|
||||||
|
return p.callYandexSearch(ctx, configJSON, query, count)
|
||||||
default:
|
default:
|
||||||
return mcpgw.BuildToolErrorResult("unsupported search provider"), nil
|
return mcpgw.BuildToolErrorResult("unsupported search provider"), nil
|
||||||
}
|
}
|
||||||
@@ -361,6 +385,679 @@ func (p *Executor) callTavilySearch(ctx context.Context, configJSON []byte, quer
|
|||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Executor) callSogouSearch(ctx context.Context, configJSON []byte, query string, count int) (map[string]any, error) {
|
||||||
|
cfg := parseConfig(configJSON)
|
||||||
|
host := firstNonEmpty(stringValue(cfg["base_url"]), "wsa.tencentcloudapi.com")
|
||||||
|
secretID := stringValue(cfg["secret_id"])
|
||||||
|
secretKey := stringValue(cfg["secret_key"])
|
||||||
|
if secretID == "" || secretKey == "" {
|
||||||
|
return mcpgw.BuildToolErrorResult("Sogou search requires Tencent Cloud SecretId and SecretKey"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
action := "SearchPro"
|
||||||
|
version := "2025-05-08"
|
||||||
|
service := "wsa"
|
||||||
|
payload, _ := json.Marshal(map[string]any{
|
||||||
|
"Query": query,
|
||||||
|
"Mode": 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
timestamp := fmt.Sprintf("%d", now.Unix())
|
||||||
|
date := now.Format("2006-01-02")
|
||||||
|
|
||||||
|
hashedPayload := sha256Hex(payload)
|
||||||
|
canonicalHeaders := fmt.Sprintf("content-type:%s\nhost:%s\n",
|
||||||
|
"application/json", host)
|
||||||
|
signedHeaders := "content-type;host"
|
||||||
|
canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
|
||||||
|
"POST", "/", "", canonicalHeaders, signedHeaders, hashedPayload)
|
||||||
|
|
||||||
|
credentialScope := fmt.Sprintf("%s/%s/tc3_request", date, service)
|
||||||
|
stringToSign := fmt.Sprintf("TC3-HMAC-SHA256\n%s\n%s\n%s",
|
||||||
|
timestamp, credentialScope, sha256Hex([]byte(canonicalRequest)))
|
||||||
|
|
||||||
|
secretDate := hmacSHA256([]byte("TC3"+secretKey), []byte(date))
|
||||||
|
secretService := hmacSHA256(secretDate, []byte(service))
|
||||||
|
secretSigning := hmacSHA256(secretService, []byte("tc3_request"))
|
||||||
|
signature := hex.EncodeToString(hmacSHA256(secretSigning, []byte(stringToSign)))
|
||||||
|
|
||||||
|
authorization := fmt.Sprintf("TC3-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s",
|
||||||
|
secretID, credentialScope, signedHeaders, signature)
|
||||||
|
|
||||||
|
timeout := parseTimeout(configJSON, 15*time.Second)
|
||||||
|
client := &http.Client{Timeout: timeout}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://"+host+"/", bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", authorization)
|
||||||
|
req.Header.Set("Host", host)
|
||||||
|
req.Header.Set("X-TC-Action", action)
|
||||||
|
req.Header.Set("X-TC-Version", version)
|
||||||
|
req.Header.Set("X-TC-Timestamp", timestamp)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return buildSearchHTTPError(resp.StatusCode, body), nil
|
||||||
|
}
|
||||||
|
var rawResp struct {
|
||||||
|
Response struct {
|
||||||
|
Error *struct {
|
||||||
|
Code string `json:"Code"`
|
||||||
|
Message string `json:"Message"`
|
||||||
|
} `json:"Error,omitempty"`
|
||||||
|
Pages []json.RawMessage `json:"Pages"`
|
||||||
|
} `json:"Response"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &rawResp); err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult("invalid search response"), nil
|
||||||
|
}
|
||||||
|
if rawResp.Response.Error != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult("Sogou search failed: " + rawResp.Response.Error.Message), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type sogouPage struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Passage string `json:"passage"`
|
||||||
|
Score float64 `json:"scour"`
|
||||||
|
}
|
||||||
|
var pages []sogouPage
|
||||||
|
for _, raw := range rawResp.Response.Pages {
|
||||||
|
var rawStr string
|
||||||
|
if err := json.Unmarshal(raw, &rawStr); err == nil {
|
||||||
|
var page sogouPage
|
||||||
|
if err := json.Unmarshal([]byte(rawStr), &page); err == nil {
|
||||||
|
pages = append(pages, page)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var page sogouPage
|
||||||
|
if err := json.Unmarshal(raw, &page); err == nil {
|
||||||
|
pages = append(pages, page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(pages, func(i, j int) bool {
|
||||||
|
return pages[i].Score > pages[j].Score
|
||||||
|
})
|
||||||
|
results := make([]map[string]any, 0, len(pages))
|
||||||
|
for i, page := range pages {
|
||||||
|
if i >= count {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
results = append(results, map[string]any{
|
||||||
|
"title": page.Title,
|
||||||
|
"url": page.URL,
|
||||||
|
"description": page.Passage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return mcpgw.BuildToolSuccessResult(map[string]any{
|
||||||
|
"query": query,
|
||||||
|
"results": results,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sha256Hex(data []byte) string {
|
||||||
|
h := sha256.Sum256(data)
|
||||||
|
return hex.EncodeToString(h[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func hmacSHA256(key, data []byte) []byte {
|
||||||
|
h := hmac.New(sha256.New, key)
|
||||||
|
h.Write(data)
|
||||||
|
return h.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Executor) callSerperSearch(ctx context.Context, configJSON []byte, query string, count int) (map[string]any, error) {
|
||||||
|
cfg := parseConfig(configJSON)
|
||||||
|
endpoint := firstNonEmpty(stringValue(cfg["base_url"]), "https://google.serper.dev/search")
|
||||||
|
apiKey := stringValue(cfg["api_key"])
|
||||||
|
if apiKey == "" {
|
||||||
|
return mcpgw.BuildToolErrorResult("Serper API key is required"), nil
|
||||||
|
}
|
||||||
|
payload, _ := json.Marshal(map[string]any{
|
||||||
|
"q": query,
|
||||||
|
})
|
||||||
|
timeout := parseTimeout(configJSON, 15*time.Second)
|
||||||
|
client := &http.Client{Timeout: timeout}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("X-API-KEY", apiKey)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return buildSearchHTTPError(resp.StatusCode, body), nil
|
||||||
|
}
|
||||||
|
var raw struct {
|
||||||
|
Organic []struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Link string `json:"link"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Position int `json:"position"`
|
||||||
|
} `json:"organic"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &raw); err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult("invalid search response"), nil
|
||||||
|
}
|
||||||
|
sort.Slice(raw.Organic, func(i, j int) bool {
|
||||||
|
return raw.Organic[i].Position < raw.Organic[j].Position
|
||||||
|
})
|
||||||
|
results := make([]map[string]any, 0, len(raw.Organic))
|
||||||
|
for i, item := range raw.Organic {
|
||||||
|
if i >= count {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
results = append(results, map[string]any{
|
||||||
|
"title": item.Title,
|
||||||
|
"url": item.Link,
|
||||||
|
"description": item.Description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return mcpgw.BuildToolSuccessResult(map[string]any{
|
||||||
|
"query": query,
|
||||||
|
"results": results,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Executor) callSearXNGSearch(ctx context.Context, configJSON []byte, query string, count int) (map[string]any, error) {
|
||||||
|
cfg := parseConfig(configJSON)
|
||||||
|
baseURL := stringValue(cfg["base_url"])
|
||||||
|
if baseURL == "" {
|
||||||
|
return mcpgw.BuildToolErrorResult("SearXNG base URL is required"), nil
|
||||||
|
}
|
||||||
|
reqURL, err := url.Parse(strings.TrimRight(baseURL, "/"))
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult("invalid SearXNG base_url"), nil
|
||||||
|
}
|
||||||
|
params := reqURL.Query()
|
||||||
|
params.Set("q", query)
|
||||||
|
params.Set("format", "json")
|
||||||
|
params.Set("pageno", "1")
|
||||||
|
if lang := stringValue(cfg["language"]); lang != "" {
|
||||||
|
params.Set("language", lang)
|
||||||
|
}
|
||||||
|
if ss := stringValue(cfg["safesearch"]); ss != "" {
|
||||||
|
params.Set("safesearch", ss)
|
||||||
|
}
|
||||||
|
if cats := stringValue(cfg["categories"]); cats != "" {
|
||||||
|
params.Set("categories", cats)
|
||||||
|
}
|
||||||
|
reqURL.RawQuery = params.Encode()
|
||||||
|
|
||||||
|
timeout := parseTimeout(configJSON, 15*time.Second)
|
||||||
|
client := &http.Client{Timeout: timeout}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return buildSearchHTTPError(resp.StatusCode, body), nil
|
||||||
|
}
|
||||||
|
var raw struct {
|
||||||
|
Results []struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Score float64 `json:"score"`
|
||||||
|
} `json:"results"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &raw); err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult("invalid search response"), nil
|
||||||
|
}
|
||||||
|
sort.Slice(raw.Results, func(i, j int) bool {
|
||||||
|
return raw.Results[i].Score > raw.Results[j].Score
|
||||||
|
})
|
||||||
|
results := make([]map[string]any, 0, len(raw.Results))
|
||||||
|
for i, item := range raw.Results {
|
||||||
|
if i >= count {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
results = append(results, map[string]any{
|
||||||
|
"title": item.Title,
|
||||||
|
"url": item.URL,
|
||||||
|
"description": item.Content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return mcpgw.BuildToolSuccessResult(map[string]any{
|
||||||
|
"query": query,
|
||||||
|
"results": results,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Executor) callJinaSearch(ctx context.Context, configJSON []byte, query string, count int) (map[string]any, error) {
|
||||||
|
cfg := parseConfig(configJSON)
|
||||||
|
endpoint := firstNonEmpty(stringValue(cfg["base_url"]), "https://s.jina.ai/")
|
||||||
|
apiKey := stringValue(cfg["api_key"])
|
||||||
|
if apiKey == "" {
|
||||||
|
return mcpgw.BuildToolErrorResult("Jina API key is required"), nil
|
||||||
|
}
|
||||||
|
if count > 10 {
|
||||||
|
count = 10
|
||||||
|
}
|
||||||
|
payload, _ := json.Marshal(map[string]any{
|
||||||
|
"q": query,
|
||||||
|
"count": count,
|
||||||
|
})
|
||||||
|
timeout := parseTimeout(configJSON, 15*time.Second)
|
||||||
|
client := &http.Client{Timeout: timeout}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("X-Retain-Images", "none")
|
||||||
|
req.Header.Set("Authorization", apiKey)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return buildSearchHTTPError(resp.StatusCode, body), nil
|
||||||
|
}
|
||||||
|
var raw struct {
|
||||||
|
Data []struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &raw); err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult("invalid search response"), nil
|
||||||
|
}
|
||||||
|
results := make([]map[string]any, 0, len(raw.Data))
|
||||||
|
for _, item := range raw.Data {
|
||||||
|
results = append(results, map[string]any{
|
||||||
|
"title": item.Title,
|
||||||
|
"url": item.URL,
|
||||||
|
"description": item.Content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return mcpgw.BuildToolSuccessResult(map[string]any{
|
||||||
|
"query": query,
|
||||||
|
"results": results,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Executor) callExaSearch(ctx context.Context, configJSON []byte, query string, count int) (map[string]any, error) {
|
||||||
|
cfg := parseConfig(configJSON)
|
||||||
|
endpoint := firstNonEmpty(stringValue(cfg["base_url"]), "https://api.exa.ai/search")
|
||||||
|
apiKey := stringValue(cfg["api_key"])
|
||||||
|
if apiKey == "" {
|
||||||
|
return mcpgw.BuildToolErrorResult("Exa API key is required"), nil
|
||||||
|
}
|
||||||
|
payload, _ := json.Marshal(map[string]any{
|
||||||
|
"query": query,
|
||||||
|
"numResults": count,
|
||||||
|
"contents": map[string]any{
|
||||||
|
"text": true,
|
||||||
|
"highlights": true,
|
||||||
|
},
|
||||||
|
"type": "auto",
|
||||||
|
})
|
||||||
|
timeout := parseTimeout(configJSON, 15*time.Second)
|
||||||
|
client := &http.Client{Timeout: timeout}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return buildSearchHTTPError(resp.StatusCode, body), nil
|
||||||
|
}
|
||||||
|
var raw struct {
|
||||||
|
Results []struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
} `json:"results"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &raw); err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult("invalid search response"), nil
|
||||||
|
}
|
||||||
|
results := make([]map[string]any, 0, len(raw.Results))
|
||||||
|
for _, item := range raw.Results {
|
||||||
|
results = append(results, map[string]any{
|
||||||
|
"title": item.Title,
|
||||||
|
"url": item.URL,
|
||||||
|
"description": item.Text,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return mcpgw.BuildToolSuccessResult(map[string]any{
|
||||||
|
"query": query,
|
||||||
|
"results": results,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Executor) callBochaSearch(ctx context.Context, configJSON []byte, query string, count int) (map[string]any, error) {
|
||||||
|
cfg := parseConfig(configJSON)
|
||||||
|
endpoint := firstNonEmpty(stringValue(cfg["base_url"]), "https://api.bochaai.com/v1/web-search")
|
||||||
|
apiKey := stringValue(cfg["api_key"])
|
||||||
|
if apiKey == "" {
|
||||||
|
return mcpgw.BuildToolErrorResult("Bocha API key is required"), nil
|
||||||
|
}
|
||||||
|
payload, _ := json.Marshal(map[string]any{
|
||||||
|
"query": query,
|
||||||
|
"summary": true,
|
||||||
|
"freshness": "noLimit",
|
||||||
|
"count": count,
|
||||||
|
})
|
||||||
|
timeout := parseTimeout(configJSON, 15*time.Second)
|
||||||
|
client := &http.Client{Timeout: timeout}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return buildSearchHTTPError(resp.StatusCode, body), nil
|
||||||
|
}
|
||||||
|
var raw struct {
|
||||||
|
Data struct {
|
||||||
|
WebPages struct {
|
||||||
|
Value []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
} `json:"value"`
|
||||||
|
} `json:"webPages"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &raw); err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult("invalid search response"), nil
|
||||||
|
}
|
||||||
|
results := make([]map[string]any, 0, len(raw.Data.WebPages.Value))
|
||||||
|
for _, item := range raw.Data.WebPages.Value {
|
||||||
|
results = append(results, map[string]any{
|
||||||
|
"title": item.Name,
|
||||||
|
"url": item.URL,
|
||||||
|
"description": item.Summary,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return mcpgw.BuildToolSuccessResult(map[string]any{
|
||||||
|
"query": query,
|
||||||
|
"results": results,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Executor) callDuckDuckGoSearch(ctx context.Context, configJSON []byte, query string, count int) (map[string]any, error) {
|
||||||
|
cfg := parseConfig(configJSON)
|
||||||
|
endpoint := firstNonEmpty(stringValue(cfg["base_url"]), "https://html.duckduckgo.com/html/")
|
||||||
|
|
||||||
|
timeout := parseTimeout(configJSON, 15*time.Second)
|
||||||
|
client := &http.Client{Timeout: timeout}
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("q", query)
|
||||||
|
form.Set("b", "")
|
||||||
|
form.Set("kl", "")
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(form.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return buildSearchHTTPError(resp.StatusCode, body), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlStr := string(body)
|
||||||
|
links := ddgResultLinkRe.FindAllStringSubmatch(htmlStr, -1)
|
||||||
|
titles := ddgResultTitleRe.FindAllStringSubmatch(htmlStr, -1)
|
||||||
|
snippets := ddgResultSnippetRe.FindAllStringSubmatch(htmlStr, -1)
|
||||||
|
|
||||||
|
n := len(links)
|
||||||
|
if len(titles) < n {
|
||||||
|
n = len(titles)
|
||||||
|
}
|
||||||
|
if count < n {
|
||||||
|
n = count
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]map[string]any, 0, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
rawURL := html.UnescapeString(links[i][1])
|
||||||
|
realURL := extractDDGURL(rawURL)
|
||||||
|
title := html.UnescapeString(strings.TrimSpace(titles[i][1]))
|
||||||
|
snippet := ""
|
||||||
|
if i < len(snippets) {
|
||||||
|
snippet = html.UnescapeString(strings.TrimSpace(ddgHTMLTagRe.ReplaceAllString(snippets[i][1], "")))
|
||||||
|
}
|
||||||
|
if realURL == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
results = append(results, map[string]any{
|
||||||
|
"title": title,
|
||||||
|
"url": realURL,
|
||||||
|
"description": snippet,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return mcpgw.BuildToolSuccessResult(map[string]any{
|
||||||
|
"query": query,
|
||||||
|
"results": results,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ddgResultLinkRe = regexp.MustCompile(`class="result__a"[^>]*href="([^"]+)"`)
|
||||||
|
ddgResultTitleRe = regexp.MustCompile(`class="result__a"[^>]*>([^<]+)<`)
|
||||||
|
ddgResultSnippetRe = regexp.MustCompile(`class="result__snippet"[^>]*>([\s\S]*?)</a>`)
|
||||||
|
ddgHTMLTagRe = regexp.MustCompile(`<[^>]*>`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func extractDDGURL(rawURL string) string {
|
||||||
|
if strings.Contains(rawURL, "uddg=") {
|
||||||
|
parsed, err := url.Parse(rawURL)
|
||||||
|
if err == nil {
|
||||||
|
if uddg := parsed.Query().Get("uddg"); uddg != "" {
|
||||||
|
return uddg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(rawURL, "//") {
|
||||||
|
return "https:" + rawURL
|
||||||
|
}
|
||||||
|
return rawURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Executor) callYandexSearch(ctx context.Context, configJSON []byte, query string, count int) (map[string]any, error) {
|
||||||
|
cfg := parseConfig(configJSON)
|
||||||
|
endpoint := firstNonEmpty(stringValue(cfg["base_url"]), "https://searchapi.api.cloud.yandex.net/v2/web/search")
|
||||||
|
apiKey := stringValue(cfg["api_key"])
|
||||||
|
if apiKey == "" {
|
||||||
|
return mcpgw.BuildToolErrorResult("Yandex API key is required"), nil
|
||||||
|
}
|
||||||
|
searchType := firstNonEmpty(stringValue(cfg["search_type"]), "SEARCH_TYPE_RU")
|
||||||
|
payload, _ := json.Marshal(map[string]any{
|
||||||
|
"query": map[string]any{
|
||||||
|
"queryText": query,
|
||||||
|
"searchType": searchType,
|
||||||
|
},
|
||||||
|
"groupSpec": map[string]any{
|
||||||
|
"groupMode": "GROUP_MODE_DEEP",
|
||||||
|
"groupsOnPage": count,
|
||||||
|
"docsInGroup": 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
timeout := parseTimeout(configJSON, 15*time.Second)
|
||||||
|
client := &http.Client{Timeout: timeout}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Api-Key "+apiKey)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return buildSearchHTTPError(resp.StatusCode, body), nil
|
||||||
|
}
|
||||||
|
var rawResp struct {
|
||||||
|
RawData string `json:"rawData"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &rawResp); err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult("invalid search response"), nil
|
||||||
|
}
|
||||||
|
xmlData, err := base64.StdEncoding.DecodeString(rawResp.RawData)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult("failed to decode Yandex response"), nil
|
||||||
|
}
|
||||||
|
results, err := parseYandexXML(xmlData)
|
||||||
|
if err != nil {
|
||||||
|
return mcpgw.BuildToolErrorResult("failed to parse Yandex XML response"), nil
|
||||||
|
}
|
||||||
|
return mcpgw.BuildToolSuccessResult(map[string]any{
|
||||||
|
"query": query,
|
||||||
|
"results": results,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlInnerText string
|
||||||
|
|
||||||
|
func (t *xmlInnerText) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
var buf strings.Builder
|
||||||
|
for {
|
||||||
|
tok, err := d.Token()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch v := tok.(type) {
|
||||||
|
case xml.CharData:
|
||||||
|
buf.Write(v)
|
||||||
|
case xml.StartElement:
|
||||||
|
var inner xmlInnerText
|
||||||
|
if err := d.DecodeElement(&inner, &v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buf.WriteString(string(inner))
|
||||||
|
case xml.EndElement:
|
||||||
|
*t = xmlInnerText(buf.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*t = xmlInnerText(buf.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type yandexResponse struct {
|
||||||
|
XMLName xml.Name `xml:"response"`
|
||||||
|
Results yandexResults `xml:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type yandexResults struct {
|
||||||
|
Grouping yandexGrouping `xml:"grouping"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type yandexGrouping struct {
|
||||||
|
Groups []yandexGroup `xml:"group"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type yandexGroup struct {
|
||||||
|
Doc yandexDoc `xml:"doc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type yandexDoc struct {
|
||||||
|
URL xmlInnerText `xml:"url"`
|
||||||
|
Title xmlInnerText `xml:"title"`
|
||||||
|
Passages yandexPassages `xml:"passages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type yandexPassages struct {
|
||||||
|
Passage []xmlInnerText `xml:"passage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseYandexXML(data []byte) ([]map[string]any, error) {
|
||||||
|
var resp yandexResponse
|
||||||
|
if err := xml.Unmarshal(data, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results := make([]map[string]any, 0, len(resp.Results.Grouping.Groups))
|
||||||
|
for _, group := range resp.Results.Grouping.Groups {
|
||||||
|
snippet := ""
|
||||||
|
if len(group.Doc.Passages.Passage) > 0 {
|
||||||
|
snippet = string(group.Doc.Passages.Passage[0])
|
||||||
|
}
|
||||||
|
results = append(results, map[string]any{
|
||||||
|
"title": string(group.Doc.Title),
|
||||||
|
"url": string(group.Doc.URL),
|
||||||
|
"description": snippet,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
// buildSearchHTTPError builds an error result for non-2xx search API responses.
|
// buildSearchHTTPError builds an error result for non-2xx search API responses.
|
||||||
// It includes the HTTP status code and attempts to extract a brief error detail
|
// It includes the HTTP status code and attempts to extract a brief error detail
|
||||||
// from the response body (capped at 200 characters to avoid context blowout).
|
// from the response body (capped at 200 characters to avoid context blowout).
|
||||||
|
|||||||
@@ -143,6 +143,253 @@ func (s *Service) ListMeta(_ context.Context) []ProviderMeta {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Provider: string(ProviderSogou),
|
||||||
|
DisplayName: "Sogou",
|
||||||
|
ConfigSchema: ProviderConfigSchema{
|
||||||
|
Fields: map[string]ProviderFieldSchema{
|
||||||
|
"secret_id": {
|
||||||
|
Type: "secret",
|
||||||
|
Title: "Secret ID",
|
||||||
|
Description: "Tencent Cloud SecretId for Sogou search",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"secret_key": {
|
||||||
|
Type: "secret",
|
||||||
|
Title: "Secret Key",
|
||||||
|
Description: "Tencent Cloud SecretKey for Sogou search",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"base_url": {
|
||||||
|
Type: "string",
|
||||||
|
Title: "Base URL",
|
||||||
|
Description: "Tencent Cloud TMS API host",
|
||||||
|
Required: false,
|
||||||
|
Example: "wsa.tencentcloudapi.com",
|
||||||
|
},
|
||||||
|
"timeout_seconds": {
|
||||||
|
Type: "number",
|
||||||
|
Title: "Timeout (seconds)",
|
||||||
|
Description: "HTTP timeout in seconds",
|
||||||
|
Required: false,
|
||||||
|
Example: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Provider: string(ProviderSerper),
|
||||||
|
DisplayName: "Serper",
|
||||||
|
ConfigSchema: ProviderConfigSchema{
|
||||||
|
Fields: map[string]ProviderFieldSchema{
|
||||||
|
"api_key": {
|
||||||
|
Type: "secret",
|
||||||
|
Title: "API Key",
|
||||||
|
Description: "Serper API key",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"base_url": {
|
||||||
|
Type: "string",
|
||||||
|
Title: "Base URL",
|
||||||
|
Description: "Serper API base URL",
|
||||||
|
Required: false,
|
||||||
|
Example: "https://google.serper.dev/search",
|
||||||
|
},
|
||||||
|
"timeout_seconds": {
|
||||||
|
Type: "number",
|
||||||
|
Title: "Timeout (seconds)",
|
||||||
|
Description: "HTTP timeout in seconds",
|
||||||
|
Required: false,
|
||||||
|
Example: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Provider: string(ProviderSearXNG),
|
||||||
|
DisplayName: "SearXNG",
|
||||||
|
ConfigSchema: ProviderConfigSchema{
|
||||||
|
Fields: map[string]ProviderFieldSchema{
|
||||||
|
"base_url": {
|
||||||
|
Type: "string",
|
||||||
|
Title: "Base URL",
|
||||||
|
Description: "SearXNG instance URL (self-hosted)",
|
||||||
|
Required: true,
|
||||||
|
Example: "http://localhost:8080/search",
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
Type: "string",
|
||||||
|
Title: "Language",
|
||||||
|
Description: "Search language (e.g. all, en, zh)",
|
||||||
|
Required: false,
|
||||||
|
Example: "all",
|
||||||
|
},
|
||||||
|
"safesearch": {
|
||||||
|
Type: "string",
|
||||||
|
Title: "Safe Search",
|
||||||
|
Description: "Safe search level: 0 (off), 1 (moderate), 2 (strict)",
|
||||||
|
Required: false,
|
||||||
|
Enum: []string{"0", "1", "2"},
|
||||||
|
Example: "1",
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
Type: "string",
|
||||||
|
Title: "Categories",
|
||||||
|
Description: "Search categories (comma-separated, e.g. general,news)",
|
||||||
|
Required: false,
|
||||||
|
Example: "general",
|
||||||
|
},
|
||||||
|
"timeout_seconds": {
|
||||||
|
Type: "number",
|
||||||
|
Title: "Timeout (seconds)",
|
||||||
|
Description: "HTTP timeout in seconds",
|
||||||
|
Required: false,
|
||||||
|
Example: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Provider: string(ProviderJina),
|
||||||
|
DisplayName: "Jina",
|
||||||
|
ConfigSchema: ProviderConfigSchema{
|
||||||
|
Fields: map[string]ProviderFieldSchema{
|
||||||
|
"api_key": {
|
||||||
|
Type: "secret",
|
||||||
|
Title: "API Key",
|
||||||
|
Description: "Jina Search API key",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"base_url": {
|
||||||
|
Type: "string",
|
||||||
|
Title: "Base URL",
|
||||||
|
Description: "Jina Search API base URL",
|
||||||
|
Required: false,
|
||||||
|
Example: "https://s.jina.ai/",
|
||||||
|
},
|
||||||
|
"timeout_seconds": {
|
||||||
|
Type: "number",
|
||||||
|
Title: "Timeout (seconds)",
|
||||||
|
Description: "HTTP timeout in seconds",
|
||||||
|
Required: false,
|
||||||
|
Example: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Provider: string(ProviderExa),
|
||||||
|
DisplayName: "Exa",
|
||||||
|
ConfigSchema: ProviderConfigSchema{
|
||||||
|
Fields: map[string]ProviderFieldSchema{
|
||||||
|
"api_key": {
|
||||||
|
Type: "secret",
|
||||||
|
Title: "API Key",
|
||||||
|
Description: "Exa Search API key",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"base_url": {
|
||||||
|
Type: "string",
|
||||||
|
Title: "Base URL",
|
||||||
|
Description: "Exa API base URL",
|
||||||
|
Required: false,
|
||||||
|
Example: "https://api.exa.ai/search",
|
||||||
|
},
|
||||||
|
"timeout_seconds": {
|
||||||
|
Type: "number",
|
||||||
|
Title: "Timeout (seconds)",
|
||||||
|
Description: "HTTP timeout in seconds",
|
||||||
|
Required: false,
|
||||||
|
Example: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Provider: string(ProviderBocha),
|
||||||
|
DisplayName: "Bocha",
|
||||||
|
ConfigSchema: ProviderConfigSchema{
|
||||||
|
Fields: map[string]ProviderFieldSchema{
|
||||||
|
"api_key": {
|
||||||
|
Type: "secret",
|
||||||
|
Title: "API Key",
|
||||||
|
Description: "Bocha Search API key",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"base_url": {
|
||||||
|
Type: "string",
|
||||||
|
Title: "Base URL",
|
||||||
|
Description: "Bocha API base URL",
|
||||||
|
Required: false,
|
||||||
|
Example: "https://api.bochaai.com/v1/web-search",
|
||||||
|
},
|
||||||
|
"timeout_seconds": {
|
||||||
|
Type: "number",
|
||||||
|
Title: "Timeout (seconds)",
|
||||||
|
Description: "HTTP timeout in seconds",
|
||||||
|
Required: false,
|
||||||
|
Example: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Provider: string(ProviderDuckDuckGo),
|
||||||
|
DisplayName: "DuckDuckGo",
|
||||||
|
ConfigSchema: ProviderConfigSchema{
|
||||||
|
Fields: map[string]ProviderFieldSchema{
|
||||||
|
"base_url": {
|
||||||
|
Type: "string",
|
||||||
|
Title: "Base URL",
|
||||||
|
Description: "DuckDuckGo HTML search URL",
|
||||||
|
Required: false,
|
||||||
|
Example: "https://html.duckduckgo.com/html/",
|
||||||
|
},
|
||||||
|
"timeout_seconds": {
|
||||||
|
Type: "number",
|
||||||
|
Title: "Timeout (seconds)",
|
||||||
|
Description: "HTTP timeout in seconds",
|
||||||
|
Required: false,
|
||||||
|
Example: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Provider: string(ProviderYandex),
|
||||||
|
DisplayName: "Yandex",
|
||||||
|
ConfigSchema: ProviderConfigSchema{
|
||||||
|
Fields: map[string]ProviderFieldSchema{
|
||||||
|
"api_key": {
|
||||||
|
Type: "secret",
|
||||||
|
Title: "API Key",
|
||||||
|
Description: "Yandex Search API key",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"search_type": {
|
||||||
|
Type: "string",
|
||||||
|
Title: "Search Type",
|
||||||
|
Description: "Yandex search type (e.g. SEARCH_TYPE_RU, SEARCH_TYPE_TR, SEARCH_TYPE_COM)",
|
||||||
|
Required: false,
|
||||||
|
Example: "SEARCH_TYPE_RU",
|
||||||
|
},
|
||||||
|
"base_url": {
|
||||||
|
Type: "string",
|
||||||
|
Title: "Base URL",
|
||||||
|
Description: "Yandex Search API base URL",
|
||||||
|
Required: false,
|
||||||
|
Example: "https://searchapi.api.cloud.yandex.net/v2/web/search",
|
||||||
|
},
|
||||||
|
"timeout_seconds": {
|
||||||
|
Type: "number",
|
||||||
|
Title: "Timeout (seconds)",
|
||||||
|
Description: "HTTP timeout in seconds",
|
||||||
|
Required: false,
|
||||||
|
Example: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +521,15 @@ func (s *Service) toGetResponse(row sqlc.SearchProvider) GetResponse {
|
|||||||
func isValidProviderName(name ProviderName) bool {
|
func isValidProviderName(name ProviderName) bool {
|
||||||
switch name {
|
switch name {
|
||||||
case ProviderBrave, ProviderBing, ProviderGoogle,
|
case ProviderBrave, ProviderBing, ProviderGoogle,
|
||||||
ProviderTavily:
|
ProviderTavily,
|
||||||
|
ProviderSogou,
|
||||||
|
ProviderSerper,
|
||||||
|
ProviderSearXNG,
|
||||||
|
ProviderJina,
|
||||||
|
ProviderExa,
|
||||||
|
ProviderBocha,
|
||||||
|
ProviderDuckDuckGo,
|
||||||
|
ProviderYandex:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ const (
|
|||||||
ProviderBing ProviderName = "bing"
|
ProviderBing ProviderName = "bing"
|
||||||
ProviderGoogle ProviderName = "google"
|
ProviderGoogle ProviderName = "google"
|
||||||
ProviderTavily ProviderName = "tavily"
|
ProviderTavily ProviderName = "tavily"
|
||||||
|
ProviderSogou ProviderName = "sogou"
|
||||||
|
ProviderSerper ProviderName = "serper"
|
||||||
|
ProviderSearXNG ProviderName = "searxng"
|
||||||
|
ProviderJina ProviderName = "jina"
|
||||||
|
ProviderExa ProviderName = "exa"
|
||||||
|
ProviderBocha ProviderName = "bocha"
|
||||||
|
ProviderDuckDuckGo ProviderName = "duckduckgo"
|
||||||
|
ProviderYandex ProviderName = "yandex"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProviderConfigSchema struct {
|
type ProviderConfigSchema struct {
|
||||||
|
|||||||
@@ -201,7 +201,15 @@
|
|||||||
"brave": "Brave",
|
"brave": "Brave",
|
||||||
"bing": "Bing",
|
"bing": "Bing",
|
||||||
"google": "Google",
|
"google": "Google",
|
||||||
"tavily": "Tavily"
|
"tavily": "Tavily",
|
||||||
|
"sogou": "Sogou",
|
||||||
|
"serper": "Serper",
|
||||||
|
"searxng": "SearXNG",
|
||||||
|
"jina": "Jina",
|
||||||
|
"exa": "Exa",
|
||||||
|
"bocha": "Bocha",
|
||||||
|
"duckduckgo": "DuckDuckGo",
|
||||||
|
"yandex": "Yandex"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mcp": {
|
"mcp": {
|
||||||
|
|||||||
@@ -197,7 +197,15 @@
|
|||||||
"brave": "Brave",
|
"brave": "Brave",
|
||||||
"bing": "Bing",
|
"bing": "Bing",
|
||||||
"google": "Google",
|
"google": "Google",
|
||||||
"tavily": "Tavily"
|
"tavily": "Tavily",
|
||||||
|
"sogou": "搜狗",
|
||||||
|
"serper": "Serper",
|
||||||
|
"searxng": "SearXNG",
|
||||||
|
"jina": "Jina",
|
||||||
|
"exa": "Exa",
|
||||||
|
"bocha": "博查",
|
||||||
|
"duckduckgo": "DuckDuckGo",
|
||||||
|
"yandex": "Yandex"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mcp": {
|
"mcp": {
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import FormDialogShell from '@/components/form-dialog-shell/index.vue'
|
import FormDialogShell from '@/components/form-dialog-shell/index.vue'
|
||||||
import { useDialogMutation } from '@/composables/useDialogMutation'
|
import { useDialogMutation } from '@/composables/useDialogMutation'
|
||||||
|
|
||||||
const PROVIDER_TYPES = ['brave', 'bing', 'google', 'tavily'] as const
|
const PROVIDER_TYPES = ['brave', 'bing', 'google', 'tavily', 'sogou', 'serper', 'searxng', 'jina', 'exa', 'bocha', 'duckduckgo', 'yandex'] as const
|
||||||
|
|
||||||
const open = defineModel<boolean>('open')
|
const open = defineModel<boolean>('open')
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="bocha-api-key">API Key</Label>
|
||||||
|
<Input
|
||||||
|
id="bocha-api-key"
|
||||||
|
v-model="localConfig.api_key"
|
||||||
|
type="password"
|
||||||
|
aria-label="API Key"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="bocha-base-url">Base URL</Label>
|
||||||
|
<Input
|
||||||
|
id="bocha-base-url"
|
||||||
|
v-model="localConfig.base_url"
|
||||||
|
aria-label="Base URL"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="bocha-timeout-seconds">Timeout (seconds)</Label>
|
||||||
|
<Input
|
||||||
|
id="bocha-timeout-seconds"
|
||||||
|
v-model.number="localConfig.timeout_seconds"
|
||||||
|
type="number"
|
||||||
|
:min="1"
|
||||||
|
aria-label="Timeout (seconds)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, watch } from 'vue'
|
||||||
|
import { Input, Label } from '@memoh/ui'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: Record<string, unknown>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: Record<string, unknown>]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const localConfig = reactive({
|
||||||
|
api_key: '',
|
||||||
|
base_url: 'https://api.bochaai.com/v1/web-search',
|
||||||
|
timeout_seconds: 15,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
localConfig.api_key = String(val?.api_key ?? '')
|
||||||
|
localConfig.base_url = String(val?.base_url ?? 'https://api.bochaai.com/v1/web-search')
|
||||||
|
const timeout = Number(val?.timeout_seconds ?? 15)
|
||||||
|
localConfig.timeout_seconds = Number.isFinite(timeout) && timeout > 0 ? timeout : 15
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(localConfig, () => {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
api_key: localConfig.api_key,
|
||||||
|
base_url: localConfig.base_url,
|
||||||
|
timeout_seconds: localConfig.timeout_seconds,
|
||||||
|
})
|
||||||
|
}, { deep: true })
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="duckduckgo-base-url">Base URL</Label>
|
||||||
|
<Input
|
||||||
|
id="duckduckgo-base-url"
|
||||||
|
v-model="localConfig.base_url"
|
||||||
|
aria-label="Base URL"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="duckduckgo-timeout-seconds">Timeout (seconds)</Label>
|
||||||
|
<Input
|
||||||
|
id="duckduckgo-timeout-seconds"
|
||||||
|
v-model.number="localConfig.timeout_seconds"
|
||||||
|
type="number"
|
||||||
|
:min="1"
|
||||||
|
aria-label="Timeout (seconds)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, watch } from 'vue'
|
||||||
|
import { Input, Label } from '@memoh/ui'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: Record<string, unknown>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: Record<string, unknown>]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const localConfig = reactive({
|
||||||
|
base_url: 'https://html.duckduckgo.com/html/',
|
||||||
|
timeout_seconds: 15,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
localConfig.base_url = String(val?.base_url ?? 'https://html.duckduckgo.com/html/')
|
||||||
|
const timeout = Number(val?.timeout_seconds ?? 15)
|
||||||
|
localConfig.timeout_seconds = Number.isFinite(timeout) && timeout > 0 ? timeout : 15
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(localConfig, () => {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
base_url: localConfig.base_url,
|
||||||
|
timeout_seconds: localConfig.timeout_seconds,
|
||||||
|
})
|
||||||
|
}, { deep: true })
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="exa-api-key">API Key</Label>
|
||||||
|
<Input
|
||||||
|
id="exa-api-key"
|
||||||
|
v-model="localConfig.api_key"
|
||||||
|
type="password"
|
||||||
|
aria-label="API Key"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="exa-base-url">Base URL</Label>
|
||||||
|
<Input
|
||||||
|
id="exa-base-url"
|
||||||
|
v-model="localConfig.base_url"
|
||||||
|
aria-label="Base URL"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="exa-timeout-seconds">Timeout (seconds)</Label>
|
||||||
|
<Input
|
||||||
|
id="exa-timeout-seconds"
|
||||||
|
v-model.number="localConfig.timeout_seconds"
|
||||||
|
type="number"
|
||||||
|
:min="1"
|
||||||
|
aria-label="Timeout (seconds)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, watch } from 'vue'
|
||||||
|
import { Input, Label } from '@memoh/ui'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: Record<string, unknown>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: Record<string, unknown>]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const localConfig = reactive({
|
||||||
|
api_key: '',
|
||||||
|
base_url: 'https://api.exa.ai/search',
|
||||||
|
timeout_seconds: 15,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
localConfig.api_key = String(val?.api_key ?? '')
|
||||||
|
localConfig.base_url = String(val?.base_url ?? 'https://api.exa.ai/search')
|
||||||
|
const timeout = Number(val?.timeout_seconds ?? 15)
|
||||||
|
localConfig.timeout_seconds = Number.isFinite(timeout) && timeout > 0 ? timeout : 15
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(localConfig, () => {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
api_key: localConfig.api_key,
|
||||||
|
base_url: localConfig.base_url,
|
||||||
|
timeout_seconds: localConfig.timeout_seconds,
|
||||||
|
})
|
||||||
|
}, { deep: true })
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="jina-api-key">API Key</Label>
|
||||||
|
<Input
|
||||||
|
id="jina-api-key"
|
||||||
|
v-model="localConfig.api_key"
|
||||||
|
type="password"
|
||||||
|
aria-label="API Key"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="jina-base-url">Base URL</Label>
|
||||||
|
<Input
|
||||||
|
id="jina-base-url"
|
||||||
|
v-model="localConfig.base_url"
|
||||||
|
aria-label="Base URL"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="jina-timeout-seconds">Timeout (seconds)</Label>
|
||||||
|
<Input
|
||||||
|
id="jina-timeout-seconds"
|
||||||
|
v-model.number="localConfig.timeout_seconds"
|
||||||
|
type="number"
|
||||||
|
:min="1"
|
||||||
|
aria-label="Timeout (seconds)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, watch } from 'vue'
|
||||||
|
import { Input, Label } from '@memoh/ui'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: Record<string, unknown>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: Record<string, unknown>]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const localConfig = reactive({
|
||||||
|
api_key: '',
|
||||||
|
base_url: 'https://s.jina.ai/',
|
||||||
|
timeout_seconds: 15,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
localConfig.api_key = String(val?.api_key ?? '')
|
||||||
|
localConfig.base_url = String(val?.base_url ?? 'https://s.jina.ai/')
|
||||||
|
const timeout = Number(val?.timeout_seconds ?? 15)
|
||||||
|
localConfig.timeout_seconds = Number.isFinite(timeout) && timeout > 0 ? timeout : 15
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(localConfig, () => {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
api_key: localConfig.api_key,
|
||||||
|
base_url: localConfig.base_url,
|
||||||
|
timeout_seconds: localConfig.timeout_seconds,
|
||||||
|
})
|
||||||
|
}, { deep: true })
|
||||||
|
</script>
|
||||||
@@ -88,6 +88,30 @@
|
|||||||
<template v-else-if="form.values.provider === 'tavily'">
|
<template v-else-if="form.values.provider === 'tavily'">
|
||||||
<TavilySettings v-model="configProxy" />
|
<TavilySettings v-model="configProxy" />
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="form.values.provider === 'sogou'">
|
||||||
|
<SogouSettings v-model="configProxy" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="form.values.provider === 'serper'">
|
||||||
|
<SerperSettings v-model="configProxy" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="form.values.provider === 'searxng'">
|
||||||
|
<SearxngSettings v-model="configProxy" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="form.values.provider === 'jina'">
|
||||||
|
<JinaSettings v-model="configProxy" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="form.values.provider === 'exa'">
|
||||||
|
<ExaSettings v-model="configProxy" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="form.values.provider === 'bocha'">
|
||||||
|
<BochaSettings v-model="configProxy" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="form.values.provider === 'duckduckgo'">
|
||||||
|
<DuckduckgoSettings v-model="configProxy" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="form.values.provider === 'yandex'">
|
||||||
|
<YandexSettings v-model="configProxy" />
|
||||||
|
</template>
|
||||||
<div
|
<div
|
||||||
v-else-if="form.values.provider"
|
v-else-if="form.values.provider"
|
||||||
class="text-sm text-muted-foreground"
|
class="text-sm text-muted-foreground"
|
||||||
@@ -147,6 +171,14 @@ import BraveSettings from './brave-settings.vue'
|
|||||||
import BingSettings from './bing-settings.vue'
|
import BingSettings from './bing-settings.vue'
|
||||||
import GoogleSettings from './google-settings.vue'
|
import GoogleSettings from './google-settings.vue'
|
||||||
import TavilySettings from './tavily-settings.vue'
|
import TavilySettings from './tavily-settings.vue'
|
||||||
|
import SogouSettings from './sogou-settings.vue'
|
||||||
|
import SerperSettings from './serper-settings.vue'
|
||||||
|
import SearxngSettings from './searxng-settings.vue'
|
||||||
|
import JinaSettings from './jina-settings.vue'
|
||||||
|
import ExaSettings from './exa-settings.vue'
|
||||||
|
import BochaSettings from './bocha-settings.vue'
|
||||||
|
import DuckduckgoSettings from './duckduckgo-settings.vue'
|
||||||
|
import YandexSettings from './yandex-settings.vue'
|
||||||
import SearchProviderLogo from '@/components/search-provider-logo/index.vue'
|
import SearchProviderLogo from '@/components/search-provider-logo/index.vue'
|
||||||
import { computed, inject, ref, watch } from 'vue'
|
import { computed, inject, ref, watch } from 'vue'
|
||||||
import { toTypedSchema } from '@vee-validate/zod'
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
@@ -156,7 +188,7 @@ import { useMutation, useQueryCache } from '@pinia/colada'
|
|||||||
import { putSearchProvidersById, deleteSearchProvidersById } from '@memoh/sdk'
|
import { putSearchProvidersById, deleteSearchProvidersById } from '@memoh/sdk'
|
||||||
import type { SearchprovidersGetResponse } from '@memoh/sdk'
|
import type { SearchprovidersGetResponse } from '@memoh/sdk'
|
||||||
|
|
||||||
const PROVIDER_TYPES = ['brave', 'bing', 'google', 'tavily'] as const
|
const PROVIDER_TYPES = ['brave', 'bing', 'google', 'tavily', 'sogou', 'serper', 'searxng', 'jina', 'exa', 'bocha', 'duckduckgo', 'yandex'] as const
|
||||||
|
|
||||||
const curProvider = inject('curSearchProvider', ref<SearchprovidersGetResponse>())
|
const curProvider = inject('curSearchProvider', ref<SearchprovidersGetResponse>())
|
||||||
const curProviderId = computed(() => curProvider.value?.id)
|
const curProviderId = computed(() => curProvider.value?.id)
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="searxng-base-url">Base URL</Label>
|
||||||
|
<Input
|
||||||
|
id="searxng-base-url"
|
||||||
|
v-model="localConfig.base_url"
|
||||||
|
aria-label="Base URL"
|
||||||
|
placeholder="http://localhost:8080/search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="searxng-language">Language</Label>
|
||||||
|
<Input
|
||||||
|
id="searxng-language"
|
||||||
|
v-model="localConfig.language"
|
||||||
|
aria-label="Language"
|
||||||
|
placeholder="all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="searxng-safesearch">Safe Search</Label>
|
||||||
|
<Input
|
||||||
|
id="searxng-safesearch"
|
||||||
|
v-model="localConfig.safesearch"
|
||||||
|
aria-label="Safe Search"
|
||||||
|
placeholder="0, 1, or 2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="searxng-categories">Categories</Label>
|
||||||
|
<Input
|
||||||
|
id="searxng-categories"
|
||||||
|
v-model="localConfig.categories"
|
||||||
|
aria-label="Categories"
|
||||||
|
placeholder="general"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="searxng-timeout-seconds">Timeout (seconds)</Label>
|
||||||
|
<Input
|
||||||
|
id="searxng-timeout-seconds"
|
||||||
|
v-model.number="localConfig.timeout_seconds"
|
||||||
|
type="number"
|
||||||
|
:min="1"
|
||||||
|
aria-label="Timeout (seconds)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, watch } from 'vue'
|
||||||
|
import { Input, Label } from '@memoh/ui'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: Record<string, unknown>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: Record<string, unknown>]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const localConfig = reactive({
|
||||||
|
base_url: '',
|
||||||
|
language: 'all',
|
||||||
|
safesearch: '1',
|
||||||
|
categories: 'general',
|
||||||
|
timeout_seconds: 15,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
localConfig.base_url = String(val?.base_url ?? '')
|
||||||
|
localConfig.language = String(val?.language ?? 'all')
|
||||||
|
localConfig.safesearch = String(val?.safesearch ?? '1')
|
||||||
|
localConfig.categories = String(val?.categories ?? 'general')
|
||||||
|
const timeout = Number(val?.timeout_seconds ?? 15)
|
||||||
|
localConfig.timeout_seconds = Number.isFinite(timeout) && timeout > 0 ? timeout : 15
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(localConfig, () => {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
base_url: localConfig.base_url,
|
||||||
|
language: localConfig.language,
|
||||||
|
safesearch: localConfig.safesearch,
|
||||||
|
categories: localConfig.categories,
|
||||||
|
timeout_seconds: localConfig.timeout_seconds,
|
||||||
|
})
|
||||||
|
}, { deep: true })
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="serper-api-key">API Key</Label>
|
||||||
|
<Input
|
||||||
|
id="serper-api-key"
|
||||||
|
v-model="localConfig.api_key"
|
||||||
|
type="password"
|
||||||
|
aria-label="API Key"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="serper-base-url">Base URL</Label>
|
||||||
|
<Input
|
||||||
|
id="serper-base-url"
|
||||||
|
v-model="localConfig.base_url"
|
||||||
|
aria-label="Base URL"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="serper-timeout-seconds">Timeout (seconds)</Label>
|
||||||
|
<Input
|
||||||
|
id="serper-timeout-seconds"
|
||||||
|
v-model.number="localConfig.timeout_seconds"
|
||||||
|
type="number"
|
||||||
|
:min="1"
|
||||||
|
aria-label="Timeout (seconds)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, watch } from 'vue'
|
||||||
|
import { Input, Label } from '@memoh/ui'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: Record<string, unknown>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: Record<string, unknown>]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const localConfig = reactive({
|
||||||
|
api_key: '',
|
||||||
|
base_url: 'https://google.serper.dev/search',
|
||||||
|
timeout_seconds: 15,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
localConfig.api_key = String(val?.api_key ?? '')
|
||||||
|
localConfig.base_url = String(val?.base_url ?? 'https://google.serper.dev/search')
|
||||||
|
const timeout = Number(val?.timeout_seconds ?? 15)
|
||||||
|
localConfig.timeout_seconds = Number.isFinite(timeout) && timeout > 0 ? timeout : 15
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(localConfig, () => {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
api_key: localConfig.api_key,
|
||||||
|
base_url: localConfig.base_url,
|
||||||
|
timeout_seconds: localConfig.timeout_seconds,
|
||||||
|
})
|
||||||
|
}, { deep: true })
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="sogou-secret-id">Secret ID</Label>
|
||||||
|
<Input
|
||||||
|
id="sogou-secret-id"
|
||||||
|
v-model="localConfig.secret_id"
|
||||||
|
type="password"
|
||||||
|
aria-label="Secret ID"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="sogou-secret-key">Secret Key</Label>
|
||||||
|
<Input
|
||||||
|
id="sogou-secret-key"
|
||||||
|
v-model="localConfig.secret_key"
|
||||||
|
type="password"
|
||||||
|
aria-label="Secret Key"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="sogou-base-url">Base URL</Label>
|
||||||
|
<Input
|
||||||
|
id="sogou-base-url"
|
||||||
|
v-model="localConfig.base_url"
|
||||||
|
aria-label="Base URL"
|
||||||
|
placeholder="wsa.tencentcloudapi.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="sogou-timeout-seconds">Timeout (seconds)</Label>
|
||||||
|
<Input
|
||||||
|
id="sogou-timeout-seconds"
|
||||||
|
v-model.number="localConfig.timeout_seconds"
|
||||||
|
type="number"
|
||||||
|
:min="1"
|
||||||
|
aria-label="Timeout (seconds)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, watch } from 'vue'
|
||||||
|
import { Input, Label } from '@memoh/ui'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: Record<string, unknown>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: Record<string, unknown>]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const localConfig = reactive({
|
||||||
|
secret_id: '',
|
||||||
|
secret_key: '',
|
||||||
|
base_url: 'wsa.tencentcloudapi.com',
|
||||||
|
timeout_seconds: 15,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
localConfig.secret_id = String(val?.secret_id ?? '')
|
||||||
|
localConfig.secret_key = String(val?.secret_key ?? '')
|
||||||
|
localConfig.base_url = String(val?.base_url ?? 'wsa.tencentcloudapi.com')
|
||||||
|
const timeout = Number(val?.timeout_seconds ?? 15)
|
||||||
|
localConfig.timeout_seconds = Number.isFinite(timeout) && timeout > 0 ? timeout : 15
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(localConfig, () => {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
secret_id: localConfig.secret_id,
|
||||||
|
secret_key: localConfig.secret_key,
|
||||||
|
base_url: localConfig.base_url,
|
||||||
|
timeout_seconds: localConfig.timeout_seconds,
|
||||||
|
})
|
||||||
|
}, { deep: true })
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="yandex-api-key">API Key</Label>
|
||||||
|
<Input
|
||||||
|
id="yandex-api-key"
|
||||||
|
v-model="localConfig.api_key"
|
||||||
|
type="password"
|
||||||
|
aria-label="API Key"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="yandex-search-type">Search Type</Label>
|
||||||
|
<Input
|
||||||
|
id="yandex-search-type"
|
||||||
|
v-model="localConfig.search_type"
|
||||||
|
aria-label="Search Type"
|
||||||
|
placeholder="SEARCH_TYPE_RU"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="yandex-base-url">Base URL</Label>
|
||||||
|
<Input
|
||||||
|
id="yandex-base-url"
|
||||||
|
v-model="localConfig.base_url"
|
||||||
|
aria-label="Base URL"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="yandex-timeout-seconds">Timeout (seconds)</Label>
|
||||||
|
<Input
|
||||||
|
id="yandex-timeout-seconds"
|
||||||
|
v-model.number="localConfig.timeout_seconds"
|
||||||
|
type="number"
|
||||||
|
:min="1"
|
||||||
|
aria-label="Timeout (seconds)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, watch } from 'vue'
|
||||||
|
import { Input, Label } from '@memoh/ui'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: Record<string, unknown>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: Record<string, unknown>]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const localConfig = reactive({
|
||||||
|
api_key: '',
|
||||||
|
search_type: 'SEARCH_TYPE_RU',
|
||||||
|
base_url: 'https://searchapi.api.cloud.yandex.net/v2/web/search',
|
||||||
|
timeout_seconds: 15,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
localConfig.api_key = String(val?.api_key ?? '')
|
||||||
|
localConfig.search_type = String(val?.search_type ?? 'SEARCH_TYPE_RU')
|
||||||
|
localConfig.base_url = String(val?.base_url ?? 'https://searchapi.api.cloud.yandex.net/v2/web/search')
|
||||||
|
const timeout = Number(val?.timeout_seconds ?? 15)
|
||||||
|
localConfig.timeout_seconds = Number.isFinite(timeout) && timeout > 0 ? timeout : 15
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(localConfig, () => {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
api_key: localConfig.api_key,
|
||||||
|
search_type: localConfig.search_type,
|
||||||
|
base_url: localConfig.base_url,
|
||||||
|
timeout_seconds: localConfig.timeout_seconds,
|
||||||
|
})
|
||||||
|
}, { deep: true })
|
||||||
|
</script>
|
||||||
@@ -28,7 +28,7 @@ import ProviderSetting from './components/provider-setting.vue'
|
|||||||
import SearchProviderLogo from '@/components/search-provider-logo/index.vue'
|
import SearchProviderLogo from '@/components/search-provider-logo/index.vue'
|
||||||
import MasterDetailSidebarLayout from '@/components/master-detail-sidebar-layout/index.vue'
|
import MasterDetailSidebarLayout from '@/components/master-detail-sidebar-layout/index.vue'
|
||||||
|
|
||||||
const PROVIDER_TYPES = ['brave', 'bing', 'google', 'tavily'] as const
|
const PROVIDER_TYPES = ['brave', 'bing', 'google', 'tavily', 'sogou', 'serper', 'searxng', 'jina', 'exa', 'bocha', 'duckduckgo', 'yandex'] as const
|
||||||
|
|
||||||
const filterProvider = ref('')
|
const filterProvider = ref('')
|
||||||
const { data: providerData } = useQuery({
|
const { data: providerData } = useQuery({
|
||||||
|
|||||||
Reference in New Issue
Block a user