Files
Agent-System-Backend/internal/tools/http.go
T
weichen 0d74ec94dd Implement the backend: store/bus/runmgr core
Details (original message):
Implement the backend: store/bus/runmgr core, mock and LLM runners (agent-native client), HITL three-branch routing, subagent runs, builtin and declarative HTTP tools, ingest parsers (md/txt/docx/pdf-text), knowledge fulltext, layered settings, profiles, plugin host, REST+SSE API, single-binary entrypoint; e2e and unit tests.
2026-09-29 17:53:24 +00:00

177 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package tools
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
core "agent-system-core"
)
// execHTTPTool 见 HTTPExecutor:声明式 HTTP 工具在注册时绑定 spec 闭包执行。
func HTTPExecutor(spec core.HTTPToolSpec, settingsLookup func(key string) string) Executor {
return func(rc ToolExecCtx, args json.RawMessage) (string, error) {
params := map[string]any{}
if len(args) > 0 {
if err := json.Unmarshal(args, &params); err != nil {
return "", fmt.Errorf("http tool: bad args json: %w", err)
}
}
method := strings.ToUpper(spec.Method)
if method == "" {
method = http.MethodGet
}
urlStr := expandTemplate(spec.URL, params)
var body io.Reader
if spec.BodyTemplate != "" && (method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch) {
body = bytes.NewReader([]byte(expandTemplate(spec.BodyTemplate, params)))
}
req, err := http.NewRequest(method, urlStr, body)
if err != nil {
return "", err
}
for k, v := range spec.Headers {
req.Header.Set(k, expandTemplate(v, params))
}
if body != nil && req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/json")
}
if spec.AuthRef != "" {
token := authValue(spec.AuthRef, settingsLookup)
if token != "" {
if strings.HasPrefix(token, "Bearer ") || strings.Contains(token, " ") {
req.Header.Set("Authorization", token)
} else {
req.Header.Set("Authorization", "Bearer "+token)
}
}
}
timeout := time.Duration(spec.TimeoutMs) * time.Millisecond
if timeout <= 0 {
timeout = 15 * time.Second
}
client := &http.Client{Timeout: timeout}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("http tool: %w", err)
}
defer resp.Body.Close()
raw, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return "", fmt.Errorf("http tool: read body: %w", err)
}
if resp.StatusCode >= 400 {
return "", fmt.Errorf("http tool: status %d: %s", resp.StatusCode, truncateStr(string(raw), 400))
}
// JSON 则裁剪路径;否则原文返回。
var parsed any
if err := json.Unmarshal(raw, &parsed); err == nil {
if spec.ResultPath != "" {
if v, ok := pickPath(parsed, spec.ResultPath); ok {
return prettyJSON(v), nil
}
return "", fmt.Errorf("http tool: result path %q not found", spec.ResultPath)
}
return prettyJSON(parsed), nil
}
return truncateStr(string(raw), 20000), nil
}
}
// expandTemplate 替换 {{param.x.y}};未知变量替换为空串(防注入:无脚本语义)。
func expandTemplate(tpl string, params map[string]any) string {
out := tpl
for {
start := strings.Index(out, "{{")
if start < 0 {
break
}
end := strings.Index(out[start:], "}}")
if end < 0 {
break
}
end += start
key := strings.TrimSpace(out[start+2 : end])
key = strings.TrimPrefix(key, "param.") // {{param.x}} → 顶层参数 x
val := ""
if v, ok := lookup(params, key); ok {
val = fmt.Sprint(v)
}
out = out[:start] + val + out[end+2:]
}
return out
}
func lookup(params map[string]any, dotted string) (any, bool) {
parts := strings.Split(dotted, ".")
var cur any = params
for _, p := range parts {
m, ok := cur.(map[string]any)
if !ok {
return nil, false
}
cur, ok = m[p]
if !ok {
return nil, false
}
}
return cur, true
}
func pickPath(v any, dotted string) (any, bool) {
parts := strings.Split(dotted, ".")
cur := v
for _, p := range parts {
// 数组支持 "0" 数字索引
if arr, ok := cur.([]any); ok {
idx := -1
fmt.Sscanf(p, "%d", &idx)
if idx < 0 || idx >= len(arr) {
return nil, false
}
cur = arr[idx]
continue
}
m, ok := cur.(map[string]any)
if !ok {
return nil, false
}
cur, ok = m[p]
if !ok {
return nil, false
}
}
return cur, true
}
func prettyJSON(v any) string {
b, err := json.MarshalIndent(v, "", " ")
if err != nil {
return fmt.Sprint(v)
}
return string(b)
}
func truncateStr(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "…(截断)"
}
// authValue 解析 AuthRef:env:NAME 或 settings 点路径。
func authValue(ref string, settingsLookup func(key string) string) string {
if strings.HasPrefix(ref, "env:") {
return os.Getenv(strings.TrimPrefix(ref, "env:"))
}
if settingsLookup != nil {
return settingsLookup(ref)
}
return ""
}