78e3f450c2
- Fix: Session history flash (race condition + WS guard) - Fix: Chat background overlay + sidebar transparency - Fix: IoT device control (Chinese action names, status field) - Feat: Independent memory-service (port 8091, 13 endpoints) - Feat: Independent tool-engine service (port 8092, 13 tools) - Feat: Tool call logs with paginated DevTools panel - Feat: Thinking log records with DevTools panel - Feat: Future development roadmap document - Chore: Updated .gitignore, go.work, DevTools config - Chore: 5-service health check, project review docs
158 lines
4.3 KiB
Go
158 lines
4.3 KiB
Go
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
|
|
}
|