Files
Cyrene/backend/ai-core/internal/tools/http_tool.go
T
AskaEth b6ec36886c feat: 第四轮功能增强 - LLM 思维记忆优化、DevTools 记忆UI、9个新工具、5分钟自我思考
- 优化 LLM 思维方式和记忆方法(类别/重要性/关键词/相似度合并/衰减)
- DevTools 记忆查询 UI 重新设计(类别筛选/排序/星标/搜索)
- 新增 9 个 LLM 工具:calculator, datetime, file_ops, http_request, json_ops, text, random, crypto, markdown
- 管理员主对话 5 分钟自我思考增强(工具调用/记忆提取/记忆维护)
2026-05-18 12:13:49 +08:00

191 lines
4.9 KiB
Go

package tools
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// HTTPTool sends arbitrary HTTP requests, more flexible than web_fetch.
// Supports custom methods, headers, and body.
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() ToolDefinition {
return 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{}) (*ToolResult, error) {
url, ok := arguments["url"].(string)
if !ok || url == "" {
return &ToolResult{
ToolName: "http_request",
Success: false,
Error: "缺少 url 参数",
}, nil
}
// Security: only allow HTTP/HTTPS
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
return &ToolResult{
ToolName: "http_request",
Success: false,
Error: "仅支持 http:// 或 https:// 链接",
}, nil
}
method, _ := arguments["method"].(string)
if method == "" {
method = "GET"
}
method = strings.ToUpper(method)
// Validate method
validMethods := map[string]bool{
"GET": true, "POST": true, "PUT": true, "DELETE": true,
"PATCH": true, "HEAD": true, "OPTIONS": true,
}
if !validMethods[method] {
return &ToolResult{
ToolName: "http_request",
Success: false,
Error: fmt.Sprintf("不支持的HTTP方法: %s", method),
}, nil
}
// Handle timeout
timeoutSec := 10.0
if timeoutVal, ok := arguments["timeout"].(float64); ok && timeoutVal > 0 {
timeoutSec = timeoutVal
}
// Create a client with the specified timeout
client := &http.Client{
Timeout: time.Duration(timeoutSec * float64(time.Second)),
}
// Build body reader
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 &ToolResult{
ToolName: "http_request",
Success: false,
Error: fmt.Sprintf("创建请求失败: %v", err),
}, nil
}
// Set default User-Agent
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; CyreneBot/1.0)")
// Parse custom headers
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 &ToolResult{
ToolName: "http_request",
Success: false,
Error: fmt.Sprintf("请求失败: %v", err),
}, nil
}
defer resp.Body.Close()
// Read response body (limited to 50KB)
const maxBodySize = 50 * 1024
bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, int64(maxBodySize)))
if err != nil {
return &ToolResult{
ToolName: "http_request",
Success: false,
Error: fmt.Sprintf("读取响应失败: %v", err),
}, nil
}
// Build response headers string
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 &ToolResult{
ToolName: "http_request",
Success: resp.StatusCode < 500,
Data: result,
}, nil
}