fix output

This commit is contained in:
王性驊 2026-04-02 21:54:28 +08:00
parent a5d057f220
commit 670c1b37c1
5 changed files with 83 additions and 35 deletions

2
internal/env/env.go vendored
View File

@ -273,7 +273,7 @@ func LoadEnvConfig(e EnvSource, cwd string) LoadedEnv {
TLSCertPath: resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_TLS_CERT"}), cwd), TLSCertPath: resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_TLS_CERT"}), cwd),
TLSKeyPath: resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_TLS_KEY"}), cwd), TLSKeyPath: resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_TLS_KEY"}), cwd),
SessionsLogPath: sessionsLogPath, 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), Verbose: envBool(e, []string{"CURSOR_BRIDGE_VERBOSE"}, false),
MaxMode: envBool(e, []string{"CURSOR_BRIDGE_MAX_MODE"}, false), MaxMode: envBool(e, []string{"CURSOR_BRIDGE_MAX_MODE"}, false),
ConfigDirs: configDirs, ConfigDirs: configDirs,

View File

@ -167,12 +167,14 @@ func HandleAnthropicMessages(w http.ResponseWriter, r *http.Request, cfg config.
}) })
if hasTools { if hasTools {
// tools 模式:先即時串流文字,一旦偵測到 tool call 標記就切換為累積模式 toolCallMarkerRe := regexp.MustCompile(`行政法规|<function_calls>`)
toolCallMarkerRe := regexp.MustCompile(`<tool_call>|<function_calls>`)
var toolCallMode bool var toolCallMode bool
textBlockOpen := false textBlockOpen := false
textBlockIndex := 0 textBlockIndex := 0
thinkingOpen := false
thinkingBlockIndex := 0
blockCount := 0
p = parser.CreateStreamParserWithThinking( p = parser.CreateStreamParserWithThinking(
func(text string) { 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}) writeEvent(map[string]interface{}{"type": "content_block_stop", "index": textBlockIndex})
textBlockOpen = false textBlockOpen = false
} }
if thinkingOpen {
writeEvent(map[string]interface{}{"type": "content_block_stop", "index": thinkingBlockIndex})
thinkingOpen = false
}
toolCallMode = true toolCallMode = true
return return
} }
if !textBlockOpen { if !textBlockOpen && !thinkingOpen {
textBlockIndex = 0 textBlockIndex = blockCount
writeEvent(map[string]interface{}{ writeEvent(map[string]interface{}{
"type": "content_block_start", "type": "content_block_start",
"index": textBlockIndex, "index": textBlockIndex,
"content_block": map[string]string{"type": "text", "text": ""}, "content_block": map[string]string{"type": "text", "text": ""},
}) })
textBlockOpen = true textBlockOpen = true
blockCount++
}
if thinkingOpen {
writeEvent(map[string]interface{}{"type": "content_block_stop", "index": thinkingBlockIndex})
thinkingOpen = false
} }
writeEvent(map[string]interface{}{ writeEvent(map[string]interface{}{
"type": "content_block_delta", "type": "content_block_delta",
@ -208,23 +219,34 @@ func HandleAnthropicMessages(w http.ResponseWriter, r *http.Request, cfg config.
}, },
func(thinking string) { func(thinking string) {
accumulatedThinking += thinking 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() { func() {
logger.LogTrafficResponse(cfg.Verbose, model, accumulated, true) logger.LogTrafficResponse(cfg.Verbose, model, accumulated, true)
parsed := toolcall.ExtractToolCalls(accumulated, toolNames) parsed := toolcall.ExtractToolCalls(accumulated, toolNames)
blockIndex := 0 blockIndex := 0
if accumulatedThinking != "" { if thinkingOpen {
writeEvent(map[string]interface{}{ writeEvent(map[string]interface{}{"type": "content_block_stop", "index": thinkingBlockIndex})
"type": "content_block_start", "index": blockIndex, thinkingOpen = false
"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 parsed.HasToolCalls() { if parsed.HasToolCalls() {

View File

@ -17,6 +17,7 @@ func CreateStreamParser(onText func(string), onDone func()) Parser {
// CreateStreamParserWithThinking 建立串流解析器,支援思考過程輸出。 // CreateStreamParserWithThinking 建立串流解析器,支援思考過程輸出。
// onThinking 可為 nil表示忽略思考過程。 // onThinking 可為 nil表示忽略思考過程。
func CreateStreamParserWithThinking(onText func(string), onThinking func(string), onDone func()) Parser { func CreateStreamParserWithThinking(onText func(string), onThinking func(string), onDone func()) Parser {
// accumulated 是所有已輸出內容的串接
accumulatedText := "" accumulatedText := ""
accumulatedThinking := "" accumulatedThinking := ""
done := false done := false
@ -58,37 +59,37 @@ func CreateStreamParserWithThinking(onText func(string), onThinking func(string)
} }
} }
// 處理思考過程 delta // 處理思考過程(不因去重而 return避免跳過同行的文字內容
if onThinking != nil && fullThinking != "" { if onThinking != nil && fullThinking != "" && fullThinking != accumulatedThinking {
if fullThinking == accumulatedThinking { // 增量模式:新內容以 accumulated 為前綴
// 重複的完整思考文字,跳過 if len(fullThinking) >= len(accumulatedThinking) && fullThinking[:len(accumulatedThinking)] == accumulatedThinking {
} else if len(fullThinking) > len(accumulatedThinking) && fullThinking[:len(accumulatedThinking)] == accumulatedThinking {
delta := fullThinking[len(accumulatedThinking):] delta := fullThinking[len(accumulatedThinking):]
onThinking(delta) if delta != "" {
onThinking(delta)
}
accumulatedThinking = fullThinking accumulatedThinking = fullThinking
} else { } else {
// 獨立片段:直接輸出,但 accumulated 要串接
onThinking(fullThinking) onThinking(fullThinking)
accumulatedThinking += fullThinking accumulatedThinking = accumulatedThinking + fullThinking
} }
} }
// 處理一般文字 delta // 處理一般文字
if fullText == "" { if fullText == "" || fullText == accumulatedText {
return return
} }
// 若此訊息文字等於已累積內容(重複的完整文字),跳過 // 增量模式:新內容以 accumulated 為前綴
if fullText == accumulatedText { if len(fullText) >= len(accumulatedText) && fullText[:len(accumulatedText)] == accumulatedText {
return
}
// 若此訊息是已累積內容的延伸,只輸出新的 delta
if len(fullText) > len(accumulatedText) && fullText[:len(accumulatedText)] == accumulatedText {
delta := fullText[len(accumulatedText):] delta := fullText[len(accumulatedText):]
onText(delta) if delta != "" {
onText(delta)
}
accumulatedText = fullText accumulatedText = fullText
} else { } else {
// 獨立的 token fragment一般情況直接輸出 // 獨立片段:直接輸出,但 accumulated 要串接
onText(fullText) onText(fullText)
accumulatedText += fullText accumulatedText = accumulatedText + fullText
} }
} }

View File

@ -278,3 +278,27 @@ func TestStreamParserWithThinkingDeduplication(t *testing.T) {
t.Fatalf("expected thinkings=['A','B'], got %v", thinkings) 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="思考中" + textthinking 為新增,兩者都應輸出)
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)
}
}

View File

@ -20,7 +20,8 @@ func (p *ParsedResponse) HasToolCalls() bool {
return len(p.ToolCalls) > 0 return len(p.ToolCalls) > 0
} }
var toolCallTagRe = regexp.MustCompile(`(?s)<tool_call>\s*(\{.*?\})\s*</tool_call>`) // Modified regex to handle nested JSON
var toolCallTagRe = regexp.MustCompile(`(?s)行政法规\s*(\{(?:[^{}]|\{[^{}]*\})*\})\s*ugalakh`)
var antmlFunctionCallsRe = regexp.MustCompile(`(?s)<function_calls>\s*(.*?)\s*</function_calls>`) var antmlFunctionCallsRe = regexp.MustCompile(`(?s)<function_calls>\s*(.*?)\s*</function_calls>`)
var antmlInvokeRe = regexp.MustCompile(`(?s)<invoke\s+name="([^"]+)">\s*(.*?)\s*</invoke>`) var antmlInvokeRe = regexp.MustCompile(`(?s)<invoke\s+name="([^"]+)">\s*(.*?)\s*</invoke>`)
var antmlParamRe = regexp.MustCompile(`(?s)<parameter\s+name="([^"]+)">(.*?)</parameter>`) var antmlParamRe = regexp.MustCompile(`(?s)<parameter\s+name="([^"]+)">(.*?)</parameter>`)