fix(tools): MCP/自定义工具输出统一走 Capture 截断,修复 mcp 截断不生效 (#176)
This commit is contained in:
1 parent
25b6018818
commit
71794729fc
3 files changed
+29
-29
No files matched your search
+6
-18
@@ -17,9 +17,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Autumn-27/artex/db"
|
||||
"github.com/Autumn-27/norma/permission"
|
||||
actool "github.com/Autumn-27/norma/tool"
|
||||
"github.com/Autumn-27/artex/db"
|
||||
)
|
||||
|
||||
// 本文件实现自定义工具执行器(docs/自定义工具设计.md)。system=false 的 tools 行按
|
||||
@@ -167,7 +167,7 @@ func (s *Server) pgTestCustomTool(w http.ResponseWriter, r *http.Request) {
|
||||
case "script":
|
||||
res, _ = s.runScriptTool(ctx, "test", req.Exec, params, tc)
|
||||
case "http":
|
||||
res, _ = s.runHTTPTool(ctx, req.Exec, params)
|
||||
res, _ = s.runHTTPTool(ctx, req.Exec, params, tc)
|
||||
case "shell":
|
||||
writeErr(w, 400, "shell 类型工具是 bash 环境声明,无可执行内容")
|
||||
return
|
||||
@@ -279,7 +279,7 @@ func (s *Server) buildCustomTool(t *db.Tool) actool.CoreTool {
|
||||
case "script":
|
||||
return s.runScriptTool(ctx, key, execRaw, params, tc)
|
||||
case "http":
|
||||
return s.runHTTPTool(ctx, execRaw, params)
|
||||
return s.runHTTPTool(ctx, execRaw, params, tc)
|
||||
default:
|
||||
return actool.Errorf("未知自定义工具类型: " + kind), nil
|
||||
}
|
||||
@@ -370,7 +370,7 @@ func (s *Server) runScriptTool(ctx context.Context, key string, execRaw json.Raw
|
||||
if err != nil {
|
||||
return actool.Errorf(err.Error()), nil
|
||||
}
|
||||
return actool.Text(clipOutput(body, tc)), nil
|
||||
return actool.Text(actool.Capture(tc, body)), nil
|
||||
}
|
||||
|
||||
// execPython writes the code to a temp .py under workDir/.tools, runs it via interp
|
||||
@@ -417,7 +417,7 @@ func execPython(ctx context.Context, interp, key, code string, params map[string
|
||||
|
||||
// ---------- http:原生请求 + 代理 ----------
|
||||
|
||||
func (s *Server) runHTTPTool(ctx context.Context, execRaw json.RawMessage, params map[string]any) (actool.Result, error) {
|
||||
func (s *Server) runHTTPTool(ctx context.Context, execRaw json.RawMessage, params map[string]any, tc *actool.ToolContext) (actool.Result, error) {
|
||||
var spec httpExec
|
||||
_ = json.Unmarshal(execRaw, &spec)
|
||||
method := strings.ToUpper(strings.TrimSpace(spec.Method))
|
||||
@@ -453,7 +453,7 @@ func (s *Server) runHTTPTool(ctx context.Context, execRaw json.RawMessage, param
|
||||
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
out := map[string]any{"status": resp.StatusCode, "body": string(respBody)}
|
||||
b, _ := json.Marshal(out)
|
||||
return actool.Text(clipOutput(string(b), nil)), nil
|
||||
return actool.Text(actool.Capture(tc, string(b))), nil
|
||||
}
|
||||
|
||||
// httpProxyTransport builds a Transport for the http tool's proxy config, or nil
|
||||
@@ -529,15 +529,3 @@ func scalarStr(v any) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// clipOutput caps output to the session's MaxOutputChars (or a default).
|
||||
func clipOutput(s string, tc *actool.ToolContext) string {
|
||||
max := 6000
|
||||
if tc != nil && tc.MaxOutputChars > 0 {
|
||||
max = tc.MaxOutputChars
|
||||
}
|
||||
if len(s) <= max {
|
||||
return s
|
||||
}
|
||||
return s[:max] + fmt.Sprintf("\n... [截断,共 %d 字节] ...", len(s))
|
||||
}
|
||||
@@ -129,7 +129,7 @@ func TestRunHTTPTool(t *testing.T) {
|
||||
"body": `{"flag":"{flag}"}`,
|
||||
})
|
||||
res, err := (&Server{}).runHTTPTool(context.Background(), execRaw,
|
||||
map[string]any{"flag": "CTF{x}", "token": "sekret"})
|
||||
map[string]any{"flag": "CTF{x}", "token": "sekret"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("runHTTPTool: %v", err)
|
||||
}
|
||||
@@ -165,14 +165,23 @@ func TestDetectPython(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClipOutput(t *testing.T) {
|
||||
long := strings.Repeat("x", 7000)
|
||||
got := clipOutput(long, nil)
|
||||
if len(got) >= 7000 || !strings.Contains(got, "截断") {
|
||||
t.Fatalf("clipOutput should truncate long output, got len %d", len(got))
|
||||
// The http tool must truncate an oversized response through norma's Capture, the
|
||||
// same valve every other tool uses — a session with no OutputDir falls back to a
|
||||
// head+tail cut, so a body far past the default cap comes back shortened.
|
||||
func TestHTTPToolTruncatesLargeBody(t *testing.T) {
|
||||
big := strings.Repeat("x", 40000) // past Capture's 30000 default
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(big))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
execRaw, _ := json.Marshal(map[string]any{"method": "GET", "url": srv.URL})
|
||||
res, err := (&Server{}).runHTTPTool(context.Background(), execRaw, map[string]any{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("runHTTPTool: %v", err)
|
||||
}
|
||||
short := "ok"
|
||||
if clipOutput(short, nil) != short {
|
||||
t.Fatal("clipOutput should pass short output through")
|
||||
out := res.Flatten()
|
||||
if len(out) >= 40000 {
|
||||
t.Fatalf("oversized http body was not truncated: len=%d", len(out))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user