From f6f4640c5e465a1d80f2d7f7ed05e8049811098e Mon Sep 17 00:00:00 2001 From: wykk-12138 Date: Thu, 9 Apr 2026 13:50:49 +0800 Subject: [PATCH] fix: use sjson to build system blocks, avoid raw newlines in JSON The previous commit used fmt.Sprintf with %s to insert multi-line string constants into JSON strings. Go raw string literals contain actual newline bytes, which produce invalid JSON (control characters in string values). Replace with buildTextBlock() helper that uses sjson.SetBytes to properly escape text content for JSON serialization. --- internal/runtime/executor/claude_executor.go | 25 ++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index 0d288ff8..e3b5b7c6 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -1302,11 +1302,14 @@ func checkSystemInstructionsWithSigningMode(payload []byte, strictMode bool, exp billingBlock := fmt.Sprintf(`{"type":"text","text":"%s"}`, billingText) // Build system blocks matching real Claude Code structure. + // Use buildTextBlock instead of fmt.Sprintf to properly escape multi-line text. // Cache control scopes: 'org' for agent block, 'global' for core prompt. - agentBlock := fmt.Sprintf(`{"type":"text","text":"You are Claude Code, Anthropic's official CLI for Claude.","cache_control":{"type":"ephemeral","scope":"org"}}`) - introBlock := fmt.Sprintf(`{"type":"text","text":"%s","cache_control":{"type":"ephemeral","scope":"global"}}`, claudeCodeIntro) - systemBlock := fmt.Sprintf(`{"type":"text","text":"%s"}`, claudeCodeSystem) - doingTasksBlock := fmt.Sprintf(`{"type":"text","text":"%s"}`, claudeCodeDoingTasks) + agentBlock := buildTextBlock("You are Claude Code, Anthropic's official CLI for Claude.", + map[string]string{"type": "ephemeral", "scope": "org"}) + introBlock := buildTextBlock(claudeCodeIntro, + map[string]string{"type": "ephemeral", "scope": "global"}) + systemBlock := buildTextBlock(claudeCodeSystem, nil) + doingTasksBlock := buildTextBlock(claudeCodeDoingTasks, nil) systemResult := "[" + billingBlock + "," + agentBlock + "," + introBlock + "," + systemBlock + "," + doingTasksBlock + "]" payload, _ = sjson.SetRawBytes(payload, "system", []byte(systemResult)) @@ -1337,6 +1340,20 @@ func checkSystemInstructionsWithSigningMode(payload []byte, strictMode bool, exp return payload } +// buildTextBlock constructs a JSON text block object with proper escaping. +// Uses sjson.SetBytes to handle multi-line text, quotes, and control characters. +// cacheControl is optional; pass nil to omit cache_control. +func buildTextBlock(text string, cacheControl map[string]string) string { + block := []byte(`{"type":"text"}`) + block, _ = sjson.SetBytes(block, "text", text) + if cacheControl != nil { + for k, v := range cacheControl { + block, _ = sjson.SetBytes(block, "cache_control."+k, v) + } + } + return string(block) +} + // prependToFirstUserMessage prepends text content to the first user message. // This avoids putting non-Claude-Code system instructions in system[] which // triggers Anthropic's extra usage billing for OAuth-proxied requests.