Files
Cyrene_v2/backend/plugins/web-search/plugin.go
T
Aska 9d040fde0b feat(plugins): implement web-search, web-fetch, http and file tools
- web-search: real SearXNG /search?format=json query, top-5 formatted
  results (was a 'not configured' stub)
- web-fetch: fetch page, strip scripts/styles/tags, truncate to 4000
  runes for the LLM context
- http: generic request tool (GET/POST/PUT/DELETE/PATCH) with scheme
  and method validation, status + truncated body
- file: sandboxed read/write/list inside the data directory with path
  escape rejection; split into file_read/file_write/file_list
- all tool schemas converted to proper JSON Schema; httptest/tempdir
  tests for each plugin
2026-08-21 12:57:19 +08:00

92 lines
2.4 KiB
Go

package websearch
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/plugin"
)
type Plugin struct {
searxngURL string
client *http.Client
}
func New(url string) *Plugin {
return &Plugin{searxngURL: strings.TrimRight(url, "/"), client: &http.Client{Timeout: 15 * time.Second}}
}
func (p *Plugin) Name() string { return "web-search" }
func (p *Plugin) Version() string { return "1.0.0" }
func (p *Plugin) Description() string { return "网络搜索" }
func (p *Plugin) Init(_ context.Context, _ plugin.PluginConfig) error { return nil }
func (p *Plugin) Close() error { return nil }
func (p *Plugin) Enabled() bool { return p.searxngURL != "" }
func (p *Plugin) Tools() []plugin.ToolDefinition {
return []plugin.ToolDefinition{{
Name: "web_search",
Description: "Search the internet via SearXNG. Returns top results with title, URL and snippet.",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"query": map[string]string{"type": "string", "description": "Search query"},
},
"required": []string{"query"},
},
}}
}
type searxResult struct {
Title string `json:"title"`
URL string `json:"url"`
Content string `json:"content"`
}
func (p *Plugin) Execute(ctx context.Context, _ string, params map[string]any) (string, error) {
query, _ := params["query"].(string)
if query == "" {
return "", fmt.Errorf("missing query")
}
reqURL := fmt.Sprintf("%s/search?q=%s&format=json", p.searxngURL, url.QueryEscape(query))
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
if err != nil {
return "", err
}
resp, err := p.client.Do(req)
if err != nil {
return "", fmt.Errorf("search request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("search API %d", resp.StatusCode)
}
var result struct {
Results []searxResult `json:"results"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", fmt.Errorf("parse results: %w", err)
}
if len(result.Results) == 0 {
return fmt.Sprintf("搜索 %q 没有结果", query), nil
}
var sb strings.Builder
fmt.Fprintf(&sb, "搜索 %q 的结果:\n", query)
for i, r := range result.Results {
if i >= 5 {
break
}
fmt.Fprintf(&sb, "%d. %s\n %s\n %s\n", i+1, r.Title, r.URL, r.Content)
}
return sb.String(), nil
}