package tools import ( "context" "fmt" "io" "net/http" "strings" "time" "github.com/yourname/cyrene-ai/tool-engine/internal/model" ) // HTTPTool sends arbitrary HTTP requests, more flexible than web_fetch. type HTTPTool struct { client *http.Client } // NewHTTPTool creates an HTTP request tool. func NewHTTPTool() *HTTPTool { return &HTTPTool{ client: &http.Client{ Timeout: 10 * time.Second, }, } } // Definition returns the tool definition for LLM function calling. func (t *HTTPTool) Definition() model.ToolDefinition { return model.ToolDefinition{ Name: "http_request", Description: "发送任意HTTP请求。比web_fetch更灵活,支持自定义请求方法、请求头和请求体。返回状态码、响应头和响应体。", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "url": map[string]interface{}{ "type": "string", "description": "请求URL,必须是完整的 http:// 或 https:// 链接", }, "method": map[string]interface{}{ "type": "string", "enum": []string{"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}, "description": "HTTP方法,默认GET", }, "headers": map[string]interface{}{ "type": "object", "description": "请求头,键值对格式,如 {\"Content-Type\": \"application/json\", \"Authorization\": \"Bearer token123\"}", }, "body": map[string]interface{}{ "type": "string", "description": "请求体内容", }, "timeout": map[string]interface{}{ "type": "number", "description": "超时秒数,默认10秒", }, }, "required": []string{"url"}, }, } } // Execute sends an HTTP request. func (t *HTTPTool) Execute(ctx context.Context, arguments map[string]interface{}) (*model.ToolResult, error) { url, ok := arguments["url"].(string) if !ok || url == "" { return &model.ToolResult{ID: "", Error: "缺少 url 参数"}, nil } if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { return &model.ToolResult{ID: "", Error: "仅支持 http:// 或 https:// 链接"}, nil } method, _ := arguments["method"].(string) if method == "" { method = "GET" } method = strings.ToUpper(method) validMethods := map[string]bool{ "GET": true, "POST": true, "PUT": true, "DELETE": true, "PATCH": true, "HEAD": true, "OPTIONS": true, } if !validMethods[method] { return &model.ToolResult{ID: "", Error: fmt.Sprintf("不支持的HTTP方法: %s", method)}, nil } timeoutSec := 10.0 if timeoutVal, ok := arguments["timeout"].(float64); ok && timeoutVal > 0 { timeoutSec = timeoutVal } client := &http.Client{ Timeout: time.Duration(timeoutSec * float64(time.Second)), } var bodyReader io.Reader bodyStr, _ := arguments["body"].(string) if bodyStr != "" { bodyReader = strings.NewReader(bodyStr) } req, err := http.NewRequestWithContext(ctx, method, url, bodyReader) if err != nil { return &model.ToolResult{ID: "", Error: fmt.Sprintf("创建请求失败: %v", err)}, nil } req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; CyreneBot/1.0)") if headersRaw, ok := arguments["headers"].(map[string]interface{}); ok { for k, v := range headersRaw { val, ok := v.(string) if !ok { val = fmt.Sprintf("%v", v) } req.Header.Set(k, val) } } resp, err := client.Do(req) if err != nil { return &model.ToolResult{ID: "", Error: fmt.Sprintf("请求失败: %v", err)}, nil } defer resp.Body.Close() const maxBodySize = 50 * 1024 bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, int64(maxBodySize))) if err != nil { return &model.ToolResult{ID: "", Error: fmt.Sprintf("读取响应失败: %v", err)}, nil } var headerLines []string for k, vals := range resp.Header { for _, v := range vals { headerLines = append(headerLines, fmt.Sprintf("%s: %s", k, v)) } } headersStr := strings.Join(headerLines, "\n") bodyTruncated := "" if len(bodyBytes) > maxBodySize { bodyTruncated = fmt.Sprintf("\n... [响应体已截断,原大小约 %d bytes]", len(bodyBytes)) } result := fmt.Sprintf( "请求: %s %s\n状态: %d %s\n响应头:\n%s\n\n响应体 (%d bytes):\n%s%s", method, url, resp.StatusCode, resp.Status, headersStr, len(bodyBytes), string(bodyBytes), bodyTruncated, ) return &model.ToolResult{ ID: "", Output: result, }, nil }