Files
Agent-System-Backend/internal/tools/http.go
T
weichen f1302ed5b3 Enable canonical versioned Go module consumption
Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements.

Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate.

Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable.

Refs: Versioned dependencies U1/U3; feature/dependency-workspace
2026-10-13 15:47:16 +00:00

177 lines
4.4 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 "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git"
)
// 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 ""
}