From 99a61640004b39ef5e9e722c6a952ee70d0c8bb4 Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Fri, 20 Mar 2026 02:31:37 +0800 Subject: [PATCH] Fix path corruption when parsing tool call JSON strings --- internal/util/toolcalls_parse.go | 57 ++++++++++++++++++++++++++++++++ internal/util/toolcalls_test.go | 21 ++++++++++++ 2 files changed, 78 insertions(+) diff --git a/internal/util/toolcalls_parse.go b/internal/util/toolcalls_parse.go index 3880c2e..08749a2 100644 --- a/internal/util/toolcalls_parse.go +++ b/internal/util/toolcalls_parse.go @@ -3,6 +3,7 @@ package util import ( "encoding/json" "strings" + "unicode" ) type ParsedToolCall struct { @@ -269,6 +270,7 @@ func parseToolCallInput(v any) map[string]any { } var parsed map[string]any if err := json.Unmarshal([]byte(raw), &parsed); err == nil && parsed != nil { + repairPathLikeControlChars(parsed) return parsed } // Try to repair invalid backslashes (common in Windows paths output by models) @@ -298,3 +300,58 @@ func parseToolCallInput(v any) map[string]any { return map[string]any{} } } + +func repairPathLikeControlChars(m map[string]any) { + for k, v := range m { + switch vv := v.(type) { + case map[string]any: + repairPathLikeControlChars(vv) + case []any: + for _, item := range vv { + if child, ok := item.(map[string]any); ok { + repairPathLikeControlChars(child) + } + } + case string: + if isPathLikeKey(k) && containsControlRune(vv) { + m[k] = escapeControlRunes(vv) + } + } + } +} + +func isPathLikeKey(key string) bool { + k := strings.ToLower(strings.TrimSpace(key)) + return strings.Contains(k, "path") || strings.Contains(k, "file") +} + +func containsControlRune(s string) bool { + for _, r := range s { + if unicode.IsControl(r) { + return true + } + } + return false +} + +func escapeControlRunes(s string) string { + var b strings.Builder + b.Grow(len(s) + 8) + for _, r := range s { + switch r { + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + b.WriteRune(r) + } + } + return b.String() +} diff --git a/internal/util/toolcalls_test.go b/internal/util/toolcalls_test.go index 2d29c1a..9701a46 100644 --- a/internal/util/toolcalls_test.go +++ b/internal/util/toolcalls_test.go @@ -419,6 +419,27 @@ func TestParseToolCallsWithMixedWindowsPaths(t *testing.T) { } } +func TestParseToolCallInputRepairsControlCharsInPath(t *testing.T) { + in := `{"path":"D:\tmp\new\readme.txt","content":"line1\nline2"}` + parsed := parseToolCallInput(in) + + path, ok := parsed["path"].(string) + if !ok { + t.Fatalf("expected path string in parsed input, got %#v", parsed["path"]) + } + if path != `D:\tmp\new\readme.txt` { + t.Fatalf("expected repaired windows path, got %q", path) + } + + content, ok := parsed["content"].(string) + if !ok { + t.Fatalf("expected content string in parsed input, got %#v", parsed["content"]) + } + if content != "line1\nline2" { + t.Fatalf("expected non-path field to keep decoded escapes, got %q", content) + } +} + func TestRepairLooseJSONWithNestedObjects(t *testing.T) { // 测试嵌套对象的修复:DeepSeek 幻觉输出,每个元素内部包含嵌套 {} // 注意:正则只支持单层嵌套,不支持更深层次的嵌套