b6ec36886c
- 优化 LLM 思维方式和记忆方法(类别/重要性/关键词/相似度合并/衰减) - DevTools 记忆查询 UI 重新设计(类别筛选/排序/星标/搜索) - 新增 9 个 LLM 工具:calculator, datetime, file_ops, http_request, json_ops, text, random, crypto, markdown - 管理员主对话 5 分钟自我思考增强(工具调用/记忆提取/记忆维护)
191 lines
4.9 KiB
Go
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
|
|
}
|