diff --git a/internal/env/env.go b/internal/env/env.go index fdd843c..239e3d8 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -273,7 +273,7 @@ func LoadEnvConfig(e EnvSource, cwd string) LoadedEnv { TLSCertPath: resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_TLS_CERT"}), cwd), TLSKeyPath: resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_TLS_KEY"}), cwd), SessionsLogPath: sessionsLogPath, - ChatOnlyWorkspace: envBool(e, []string{"CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE"}, false), + ChatOnlyWorkspace: envBool(e, []string{"CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE"}, true), Verbose: envBool(e, []string{"CURSOR_BRIDGE_VERBOSE"}, false), MaxMode: envBool(e, []string{"CURSOR_BRIDGE_MAX_MODE"}, false), ConfigDirs: configDirs, diff --git a/internal/handlers/anthropic_handler.go b/internal/handlers/anthropic_handler.go index 8b86377..fc2f74e 100644 --- a/internal/handlers/anthropic_handler.go +++ b/internal/handlers/anthropic_handler.go @@ -167,12 +167,14 @@ func HandleAnthropicMessages(w http.ResponseWriter, r *http.Request, cfg config. }) if hasTools { - // tools 模式:先即時串流文字,一旦偵測到 tool call 標記就切換為累積模式 - toolCallMarkerRe := regexp.MustCompile(`|`) + toolCallMarkerRe := regexp.MustCompile(`行政法规|`) var toolCallMode bool textBlockOpen := false textBlockIndex := 0 + thinkingOpen := false + thinkingBlockIndex := 0 + blockCount := 0 p = parser.CreateStreamParserWithThinking( func(text string) { @@ -188,17 +190,26 @@ func HandleAnthropicMessages(w http.ResponseWriter, r *http.Request, cfg config. writeEvent(map[string]interface{}{"type": "content_block_stop", "index": textBlockIndex}) textBlockOpen = false } + if thinkingOpen { + writeEvent(map[string]interface{}{"type": "content_block_stop", "index": thinkingBlockIndex}) + thinkingOpen = false + } toolCallMode = true return } - if !textBlockOpen { - textBlockIndex = 0 + if !textBlockOpen && !thinkingOpen { + textBlockIndex = blockCount writeEvent(map[string]interface{}{ "type": "content_block_start", "index": textBlockIndex, "content_block": map[string]string{"type": "text", "text": ""}, }) textBlockOpen = true + blockCount++ + } + if thinkingOpen { + writeEvent(map[string]interface{}{"type": "content_block_stop", "index": thinkingBlockIndex}) + thinkingOpen = false } writeEvent(map[string]interface{}{ "type": "content_block_delta", @@ -208,23 +219,34 @@ func HandleAnthropicMessages(w http.ResponseWriter, r *http.Request, cfg config. }, func(thinking string) { accumulatedThinking += thinking + chunkNum++ + if toolCallMode { + return + } + if !thinkingOpen { + thinkingBlockIndex = blockCount + writeEvent(map[string]interface{}{ + "type": "content_block_start", + "index": thinkingBlockIndex, + "content_block": map[string]string{"type": "thinking", "thinking": ""}, + }) + thinkingOpen = true + blockCount++ + } + writeEvent(map[string]interface{}{ + "type": "content_block_delta", + "index": thinkingBlockIndex, + "delta": map[string]string{"type": "thinking_delta", "thinking": thinking}, + }) }, func() { logger.LogTrafficResponse(cfg.Verbose, model, accumulated, true) parsed := toolcall.ExtractToolCalls(accumulated, toolNames) blockIndex := 0 - if accumulatedThinking != "" { - writeEvent(map[string]interface{}{ - "type": "content_block_start", "index": blockIndex, - "content_block": map[string]string{"type": "thinking", "thinking": ""}, - }) - writeEvent(map[string]interface{}{ - "type": "content_block_delta", "index": blockIndex, - "delta": map[string]string{"type": "thinking_delta", "thinking": accumulatedThinking}, - }) - writeEvent(map[string]interface{}{"type": "content_block_stop", "index": blockIndex}) - blockIndex++ + if thinkingOpen { + writeEvent(map[string]interface{}{"type": "content_block_stop", "index": thinkingBlockIndex}) + thinkingOpen = false } if parsed.HasToolCalls() { diff --git a/internal/parser/stream.go b/internal/parser/stream.go index aa13cb5..bbdd231 100644 --- a/internal/parser/stream.go +++ b/internal/parser/stream.go @@ -17,6 +17,7 @@ func CreateStreamParser(onText func(string), onDone func()) Parser { // CreateStreamParserWithThinking 建立串流解析器,支援思考過程輸出。 // onThinking 可為 nil,表示忽略思考過程。 func CreateStreamParserWithThinking(onText func(string), onThinking func(string), onDone func()) Parser { + // accumulated 是所有已輸出內容的串接 accumulatedText := "" accumulatedThinking := "" done := false @@ -58,37 +59,37 @@ func CreateStreamParserWithThinking(onText func(string), onThinking func(string) } } - // 處理思考過程 delta - if onThinking != nil && fullThinking != "" { - if fullThinking == accumulatedThinking { - // 重複的完整思考文字,跳過 - } else if len(fullThinking) > len(accumulatedThinking) && fullThinking[:len(accumulatedThinking)] == accumulatedThinking { + // 處理思考過程(不因去重而 return,避免跳過同行的文字內容) + if onThinking != nil && fullThinking != "" && fullThinking != accumulatedThinking { + // 增量模式:新內容以 accumulated 為前綴 + if len(fullThinking) >= len(accumulatedThinking) && fullThinking[:len(accumulatedThinking)] == accumulatedThinking { delta := fullThinking[len(accumulatedThinking):] - onThinking(delta) + if delta != "" { + onThinking(delta) + } accumulatedThinking = fullThinking } else { + // 獨立片段:直接輸出,但 accumulated 要串接 onThinking(fullThinking) - accumulatedThinking += fullThinking + accumulatedThinking = accumulatedThinking + fullThinking } } - // 處理一般文字 delta - if fullText == "" { + // 處理一般文字 + if fullText == "" || fullText == accumulatedText { return } - // 若此訊息文字等於已累積內容(重複的完整文字),跳過 - if fullText == accumulatedText { - return - } - // 若此訊息是已累積內容的延伸,只輸出新的 delta - if len(fullText) > len(accumulatedText) && fullText[:len(accumulatedText)] == accumulatedText { + // 增量模式:新內容以 accumulated 為前綴 + if len(fullText) >= len(accumulatedText) && fullText[:len(accumulatedText)] == accumulatedText { delta := fullText[len(accumulatedText):] - onText(delta) + if delta != "" { + onText(delta) + } accumulatedText = fullText } else { - // 獨立的 token fragment(一般情況),直接輸出 + // 獨立片段:直接輸出,但 accumulated 要串接 onText(fullText) - accumulatedText += fullText + accumulatedText = accumulatedText + fullText } } diff --git a/internal/parser/stream_test.go b/internal/parser/stream_test.go index 29d1b03..146cdc2 100644 --- a/internal/parser/stream_test.go +++ b/internal/parser/stream_test.go @@ -278,3 +278,27 @@ func TestStreamParserWithThinkingDeduplication(t *testing.T) { t.Fatalf("expected thinkings=['A','B'], got %v", thinkings) } } + +// TestStreamParserThinkingDuplicateButTextStillEmitted 驗證 bug 修復: +// 當 thinking 重複(去重跳過)但同一行有 text 時,text 仍必須輸出。 +func TestStreamParserThinkingDuplicateButTextStillEmitted(t *testing.T) { + var texts []string + var thinkings []string + p := CreateStreamParserWithThinking( + func(text string) { texts = append(texts, text) }, + func(thinking string) { thinkings = append(thinkings, thinking) }, + func() {}, + ) + + // 第一行:thinking="思考中" + text(thinking 為新增,兩者都應輸出) + p.Parse(makeThinkingAndTextLine("思考中", "第一段")) + // 第二行:thinking 與上一行相同(去重),但 text 是新的,text 仍應輸出 + p.Parse(makeThinkingAndTextLine("思考中", "第二段")) + + if len(thinkings) != 1 || thinkings[0] != "思考中" { + t.Fatalf("expected thinkings=['思考中'], got %v", thinkings) + } + if len(texts) != 2 || texts[0] != "第一段" || texts[1] != "第二段" { + t.Fatalf("expected texts=['第一段','第二段'], got %v", texts) + } +} diff --git a/internal/toolcall/toolcall.go b/internal/toolcall/toolcall.go index 4c7a161..4d47176 100644 --- a/internal/toolcall/toolcall.go +++ b/internal/toolcall/toolcall.go @@ -20,7 +20,8 @@ func (p *ParsedResponse) HasToolCalls() bool { return len(p.ToolCalls) > 0 } -var toolCallTagRe = regexp.MustCompile(`(?s)\s*(\{.*?\})\s*`) +// Modified regex to handle nested JSON +var toolCallTagRe = regexp.MustCompile(`(?s)行政法规\s*(\{(?:[^{}]|\{[^{}]*\})*\})\s*ugalakh`) var antmlFunctionCallsRe = regexp.MustCompile(`(?s)\s*(.*?)\s*`) var antmlInvokeRe = regexp.MustCompile(`(?s)\s*(.*?)\s*`) var antmlParamRe = regexp.MustCompile(`(?s)(.*?)`)