Files
Cyrene_v2/backend/plugins/http/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

89 lines
2.6 KiB
Go

package http_plugin
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/plugin"
)
type Plugin struct{ client *http.Client }
func New() *Plugin { return &Plugin{client: &http.Client{Timeout: 15 * time.Second}} }
func (p *Plugin) Name() string { return "http" }
func (p *Plugin) Version() string { return "1.0.0" }
func (p *Plugin) Description() string { return "HTTP请求" }
func (p *Plugin) Init(_ context.Context, _ plugin.PluginConfig) error { return nil }
func (p *Plugin) Close() error { return nil }
func (p *Plugin) Enabled() bool { return true }
func (p *Plugin) Tools() []plugin.ToolDefinition {
return []plugin.ToolDefinition{{
Name: "http_request",
Description: "Send an HTTP request and return the status code and body (truncated).",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"url": map[string]string{"type": "string", "description": "Request URL (http/https)"},
"method": map[string]string{"type": "string", "description": "GET | POST | PUT | DELETE (default GET)"},
"body": map[string]string{"type": "string", "description": "Optional request body"},
},
"required": []string{"url"},
},
}}
}
func (p *Plugin) Execute(ctx context.Context, _ string, params map[string]any) (string, error) {
rawURL, _ := params["url"].(string)
if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
return "", fmt.Errorf("url must start with http:// or https://")
}
method := strings.ToUpper(strings.TrimSpace(paramStr(params, "method")))
switch method {
case "":
method = "GET"
case "GET", "POST", "PUT", "DELETE", "PATCH":
default:
return "", fmt.Errorf("unsupported method %q", method)
}
var bodyReader io.Reader
if body := paramStr(params, "body"); body != "" {
bodyReader = strings.NewReader(body)
}
req, err := http.NewRequestWithContext(ctx, method, rawURL, bodyReader)
if err != nil {
return "", err
}
if bodyReader != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := p.client.Do(req)
if err != nil {
return "", fmt.Errorf("request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return "", fmt.Errorf("read body: %w", err)
}
text := string(body)
if runes := []rune(text); len(runes) > 4000 {
text = string(runes[:4000]) + "\n…(响应过长已截断)"
}
return fmt.Sprintf("HTTP %d\n%s", resp.StatusCode, text), nil
}
func paramStr(params map[string]any, key string) string {
v, _ := params[key].(string)
return v
}