From fab326eca12963accac5bb884ac87fad7ee51c01 Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Thu, 5 Mar 2026 18:20:42 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E6=B3=A8=E5=85=A5=20null=20=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E8=B0=83=E7=94=A8=E6=A0=BC=E5=BC=8F=E6=B7=B7=E4=B9=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/adapter/openai/message_normalize.go | 3 ++ .../adapter/openai/message_normalize_test.go | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/internal/adapter/openai/message_normalize.go b/internal/adapter/openai/message_normalize.go index 299f908..bbc5800 100644 --- a/internal/adapter/openai/message_normalize.go +++ b/internal/adapter/openai/message_normalize.go @@ -132,6 +132,9 @@ func formatToolResultForPrompt(msg map[string]any) string { } func normalizeOpenAIContentForPrompt(v any) string { + if v == nil { + return "" + } switch x := v.(type) { case string: return x diff --git a/internal/adapter/openai/message_normalize_test.go b/internal/adapter/openai/message_normalize_test.go index ba17a52..a4e1843 100644 --- a/internal/adapter/openai/message_normalize_test.go +++ b/internal/adapter/openai/message_normalize_test.go @@ -194,6 +194,36 @@ func TestNormalizeOpenAIMessagesForPrompt_PreservesConcatenatedToolArguments(t * } } +func TestNormalizeOpenAIMessagesForPrompt_AssistantNilContentDoesNotInjectNullLiteral(t *testing.T) { + raw := []any{ + map[string]any{ + "role": "assistant", + "content": nil, + "tool_calls": []any{ + map[string]any{ + "id": "call_screenshot", + "function": map[string]any{ + "name": "send_file_to_user", + "arguments": `{"file_path":"/tmp/a.png"}`, + }, + }, + }, + }, + } + + normalized := normalizeOpenAIMessagesForPrompt(raw, "") + if len(normalized) != 1 { + t.Fatalf("expected one normalized message, got %d", len(normalized)) + } + content, _ := normalized[0]["content"].(string) + if strings.Contains(content, "<|Assistant|>null") || strings.HasPrefix(strings.TrimSpace(content), "null") { + t.Fatalf("unexpected null literal injected into assistant tool history: %q", content) + } + if !strings.Contains(content, "function.name: send_file_to_user") { + t.Fatalf("expected tool history block preserved, got %q", content) + } +} + func TestNormalizeOpenAIMessagesForPrompt_DeveloperRoleMapsToSystem(t *testing.T) { raw := []any{ map[string]any{"role": "developer", "content": "必须先走工具调用"}, From 0e261ff0a0d54058df7ead7fec155c68ebcce463 Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Fri, 6 Mar 2026 18:25:27 +0800 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=E7=BB=9F=E4=B8=80=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E5=BD=92=E4=B8=80=E5=8C=96=E9=80=BB=E8=BE=91=E5=B9=B6?= =?UTF-8?q?=E8=A1=A5=E5=85=85=20nil=20=E5=9B=9E=E5=BD=92=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/adapter/openai/message_normalize.go | 31 ++------------------ internal/prompt/messages.go | 3 ++ internal/prompt/messages_test.go | 23 +++++++++++++++ 3 files changed, 28 insertions(+), 29 deletions(-) create mode 100644 internal/prompt/messages_test.go diff --git a/internal/adapter/openai/message_normalize.go b/internal/adapter/openai/message_normalize.go index bbc5800..724cb9f 100644 --- a/internal/adapter/openai/message_normalize.go +++ b/internal/adapter/openai/message_normalize.go @@ -6,6 +6,7 @@ import ( "strings" "ds2api/internal/config" + "ds2api/internal/prompt" ) func normalizeOpenAIMessagesForPrompt(raw []any, traceID string) []map[string]any { @@ -132,35 +133,7 @@ func formatToolResultForPrompt(msg map[string]any) string { } func normalizeOpenAIContentForPrompt(v any) string { - if v == nil { - return "" - } - switch x := v.(type) { - case string: - return x - case []any: - parts := make([]string, 0, len(x)) - for _, item := range x { - m, ok := item.(map[string]any) - if !ok { - continue - } - t := strings.ToLower(strings.TrimSpace(asString(m["type"]))) - if t != "text" && t != "output_text" && t != "input_text" { - continue - } - if text := asString(m["text"]); text != "" { - parts = append(parts, text) - continue - } - if text := asString(m["content"]); text != "" { - parts = append(parts, text) - } - } - return strings.Join(parts, "\n") - default: - return marshalToPromptString(v) - } + return prompt.NormalizeContent(v) } func normalizeOpenAIArgumentsForPrompt(v any) string { diff --git a/internal/prompt/messages.go b/internal/prompt/messages.go index 69cfe5a..5e124b4 100644 --- a/internal/prompt/messages.go +++ b/internal/prompt/messages.go @@ -51,6 +51,9 @@ func MessagesPrepare(messages []map[string]any) string { } func NormalizeContent(v any) string { + if v == nil { + return "" + } switch x := v.(type) { case string: return x diff --git a/internal/prompt/messages_test.go b/internal/prompt/messages_test.go new file mode 100644 index 0000000..0407552 --- /dev/null +++ b/internal/prompt/messages_test.go @@ -0,0 +1,23 @@ +package prompt + +import "testing" + +func TestNormalizeContentNilReturnsEmpty(t *testing.T) { + if got := NormalizeContent(nil); got != "" { + t.Fatalf("expected empty string for nil content, got %q", got) + } +} + +func TestMessagesPrepareNilContentNoNullLiteral(t *testing.T) { + messages := []map[string]any{ + {"role": "assistant", "content": nil}, + {"role": "user", "content": "ok"}, + } + got := MessagesPrepare(messages) + if got == "" { + t.Fatalf("expected non-empty output") + } + if got == "null" { + t.Fatalf("expected no null literal output, got %q", got) + } +} From 6c39c8e191e169ca5ba99b88d3cb51855b42cac4 Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Fri, 6 Mar 2026 21:24:26 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20text=20?= =?UTF-8?q?=E4=B8=BA=E7=A9=BA=E6=97=B6=20content=20=E5=9B=9E=E9=80=80?= =?UTF-8?q?=E4=B8=A2=E5=A4=B1=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/openai/message_normalize_test.go | 20 +++++++++++++++++++ internal/prompt/messages.go | 4 ++-- internal/prompt/messages_test.go | 9 +++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/internal/adapter/openai/message_normalize_test.go b/internal/adapter/openai/message_normalize_test.go index a4e1843..ecb3bbd 100644 --- a/internal/adapter/openai/message_normalize_test.go +++ b/internal/adapter/openai/message_normalize_test.go @@ -237,3 +237,23 @@ func TestNormalizeOpenAIMessagesForPrompt_DeveloperRoleMapsToSystem(t *testing.T t.Fatalf("expected developer role converted to system, got %#v", normalized[0]["role"]) } } + +func TestNormalizeOpenAIMessagesForPrompt_AssistantArrayContentFallbackWhenTextEmpty(t *testing.T) { + raw := []any{ + map[string]any{ + "role": "assistant", + "content": []any{ + map[string]any{"type": "text", "text": "", "content": "工具说明文本"}, + }, + }, + } + + normalized := normalizeOpenAIMessagesForPrompt(raw, "") + if len(normalized) != 1 { + t.Fatalf("expected one normalized message, got %d", len(normalized)) + } + content, _ := normalized[0]["content"].(string) + if content != "工具说明文本" { + t.Fatalf("expected content fallback text preserved, got %q", content) + } +} diff --git a/internal/prompt/messages.go b/internal/prompt/messages.go index 5e124b4..e86c391 100644 --- a/internal/prompt/messages.go +++ b/internal/prompt/messages.go @@ -67,11 +67,11 @@ func NormalizeContent(v any) string { typeStr, _ := m["type"].(string) typeStr = strings.ToLower(strings.TrimSpace(typeStr)) if typeStr == "text" || typeStr == "output_text" || typeStr == "input_text" { - if txt, ok := m["text"].(string); ok { + if txt, ok := m["text"].(string); ok && txt != "" { parts = append(parts, txt) continue } - if txt, ok := m["content"].(string); ok { + if txt, ok := m["content"].(string); ok && txt != "" { parts = append(parts, txt) } } diff --git a/internal/prompt/messages_test.go b/internal/prompt/messages_test.go index 0407552..9114d39 100644 --- a/internal/prompt/messages_test.go +++ b/internal/prompt/messages_test.go @@ -21,3 +21,12 @@ func TestMessagesPrepareNilContentNoNullLiteral(t *testing.T) { t.Fatalf("expected no null literal output, got %q", got) } } + +func TestNormalizeContentArrayFallsBackToContentWhenTextEmpty(t *testing.T) { + got := NormalizeContent([]any{ + map[string]any{"type": "text", "text": "", "content": "from-content"}, + }) + if got != "from-content" { + t.Fatalf("expected fallback to content when text is empty, got %q", got) + } +}