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, ¶ms); 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 "" }