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
This commit is contained in:
1 parent
d80710d85a
commit
9d040fde0b
8 files changed
+599
-44
No files matched your search
@@ -1,19 +1,88 @@
|
||||
package http_plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/plugin"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/plugin"
|
||||
)
|
||||
type Plugin struct{}
|
||||
func New() *Plugin { return &Plugin{} }
|
||||
func (p *Plugin) Name() string { return "http" }
|
||||
func (p *Plugin) Version() string { return "1.0.0" }
|
||||
|
||||
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) Close() error { return nil }
|
||||
func (p *Plugin) Enabled() bool { return true }
|
||||
|
||||
func (p *Plugin) Tools() []plugin.ToolDefinition {
|
||||
return []plugin.ToolDefinition{{Name:"http", Description:"发送HTTP请求", Parameters:map[string]any{"url": map[string]string{"type":"string"},"method":map[string]string{"type":"string"}}}}
|
||||
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(_ context.Context, _ string, _ map[string]any) (string, error) {
|
||||
return "[HTTP] 功能占位", nil
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package http_plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
t.Errorf("method = %s", r.Method)
|
||||
}
|
||||
w.Write([]byte("hello"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := New()
|
||||
out, err := p.Execute(context.Background(), "http_request", map[string]any{"url": srv.URL})
|
||||
if err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
if !strings.Contains(out, "HTTP 200") || !strings.Contains(out, "hello") {
|
||||
t.Errorf("unexpected output: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostWithBody(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
if r.Method != "POST" || string(body) != `{"a":1}` {
|
||||
t.Errorf("method=%s body=%s", r.Method, body)
|
||||
}
|
||||
w.WriteHeader(201)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := New()
|
||||
out, err := p.Execute(context.Background(), "http_request", map[string]any{
|
||||
"url": srv.URL, "method": "post", "body": `{"a":1}`,
|
||||
})
|
||||
if err != nil || !strings.Contains(out, "HTTP 201") {
|
||||
t.Errorf("out=%q err=%v", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectsBadInput(t *testing.T) {
|
||||
p := New()
|
||||
if _, err := p.Execute(context.Background(), "http_request", map[string]any{"url": "ftp://x"}); err == nil {
|
||||
t.Error("bad scheme should fail")
|
||||
}
|
||||
if _, err := p.Execute(context.Background(), "http_request", map[string]any{"url": "http://x", "method": "TRACE"}); err == nil {
|
||||
t.Error("bad method should fail")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user